Message

Data Display

assistant-ui 메시지를 감싸는 역할별 버블 컨테이너.

Usage

ThreadPrimitive.Messages 안에서 user·assistant 메시지 버블을 렌더할 때 사용한다.

import

import

import { Message } from '@mildang/design-system/unofficial/Chat';

Message 하나만 가져오면 Message.Content · Message.Header · Message.SenderName · Message.Meta · Message.Parts · Message.GroupedParts · Message.Attachments · Message.Error · Message.If · Message.DirectiveText · Message.Reasoning · Message.Sources 를 그 아래에서 쓸 수 있다.

예제를 복사해 쓸 때 필요한 준비

@mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.

외부 패키지를 따로 설치한다. DS 의 전이 의존성에 기대지 않는다.

Anatomy

tsx

import { Message } from '@mildang/design-system/unofficial/Chat';

export default function Example() {
  return (
    <Message.Header>
      <Message.SenderName />
    </Message.Header>
    <Message.Content>
      <Message.Parts>
        <Message.DirectiveText />
        <Message.Reasoning />
        <Message.Sources />
      </Message.Parts>
      <Message.GroupedParts />
      <Message.Attachments />
      <Message.Meta />
    </Message.Content>
    <Message.Error />
    <Message.If />
  );
}

부품

필수 여부

반복

위치

Message.Header

선택

Message 안에 둔다(멀티 에이전트·사람 채팅 등 예외 문맥 전용).

Message.SenderName

선택

Message.Header 안에 둔다.

Message.Content

필수

Message 안에 둔다.

Message.Parts

선택

Message.Content 안에 둔다.

Message.DirectiveText

선택

Message.Parts 의 components.Text 로 전달한다.

Message.Reasoning

선택

Message.Parts 의 components.Reasoning 으로 전달한다.

Message.Sources

선택

Message.Parts 의 components.Source 로 전달한다.

Message.GroupedParts

선택

Message.Content 안에서 Parts 대신 쓴다.

Message.Attachments

선택

Message.Content 안에 둔다.

Message.Meta

선택

Message.Content 안에 둔다.

Message.Error

선택

Message 안에 둔다.

Message.If

선택

Message 안 어디든 조건부로 둔다.

  • 루트의 스타일 variant role(user·assistant)은 버블 정렬을, density는 메시지 간 여백을, grouping은 연속 버블의 모서리 위치를 조정한다. 아래 역할·밀도·그룹 예제에서 조합을 확인한다.
  • Content 로 본문을 감싸고 그 안에서 Parts 또는 GroupedParts 로 파트를 렌더한다.
  • Header/SenderName/Meta 는 챗봇 관례상 생략하고, 멀티 에이전트·사람 채팅처럼 필요한 문맥에서만 조립한다.
  • DirectiveText/Reasoning/Sources 는 Parts 의 components 맵으로 연결해야 실제로 렌더된다.

API Reference

Message.Header

아바타·발신자명 같은 메타 영역이다.

공개 Props 없음

Message.SenderName

발신자 이름을 표시한다.

공개 Props 없음

Message.Content

버블 배경·패딩을 가진 본문 컨테이너다.

공개 Props 없음

Message.Parts

텍스트·reasoning·source 등 메시지 파트를 순회 렌더한다.

Prop

Type

Default

children

(value: { part: EnrichedPartState; }) => ReactNode

지정 안 함

components

({ Empty?: EmptyMessagePartComponent | undefined; Text?: TextMessagePartComponent | undefined; Source?: SourceMessagePartComponent | undefined; Image?: ImageMessagePartComponent | undefined; File?: FileMessagePartComponent | undefined; Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined; data?: { by_name?: Record<string, DataMessagePartComponent | undefined> | undefined; Fallback?: DataMessagePartComponent | undefined; } | undefined; Quote?: QuoteMessagePartComponent | undefined; generativeUI?: { components: GenerativeUIComponentRegistry; Fallback?: ComponentType<{ component: string; props?: unknown; }> | undefined; } | undefined; } & { Reasoning?: ReasoningMessagePartComponent | undefined; tools?: ({ by_name?: Record<string, ToolCallMessagePartComponent | undefined> | undefined; Fallback?: ComponentType<ToolCallMessagePartProps> | undefined; } | { Override: ComponentType<ToolCallMessagePartProps>; }) | undefined; ToolGroup?: ComponentType<PropsWithChildren<{ startIndex: number; endIndex: number; }>>; ReasoningGroup?: ReasoningGroupComponent; ChainOfThought?: never; }) | ({ Empty?: EmptyMessagePartComponent | undefined; Text?: TextMessagePartComponent | undefined; Source?: SourceMessagePartComponent | undefined; Image?: ImageMessagePartComponent | undefined; File?: FileMessagePartComponent | undefined; Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined; data?: { by_name?: Record<string, DataMessagePartComponent | undefined> | undefined; Fallback?: DataMessagePartComponent | undefined; } | undefined; Quote?: QuoteMessagePartComponent | undefined; generativeUI?: { components: GenerativeUIComponentRegistry; Fallback?: ComponentType<{ component: string; props?: unknown; }> | undefined; } | undefined; } & { ChainOfThought: ComponentType; Reasoning?: never; tools?: never; ToolGroup?: never; ReasoningGroup?: never; })

지정 안 함

unstable_showEmptyOnNonTextEnd

boolean

true

Message.DirectiveText

user 메시지의 directive 문법을 Chip 으로 렌더하는 Text 파트 컴포넌트다.

공개 Props 없음

Message.Reasoning

reasoning 파트를 ChainOfThought 시각으로 렌더한다.

Prop

Type

Default

status
필수

({ readonly type: "running"; } & { readonly type: "running"; }) | ({ readonly type: "complete"; } & { readonly type: "complete"; }) | ({ readonly type: "incomplete"; readonly reason: "cancelled" | "length" | "content-filter" | "other" | "error"; } & { readonly type: "incomplete"; readonly reason: "cancelled" | "length" | "content-filter" | "other" | "error"; readonly error?: unknown; })

지정 안 함

text
필수

string

지정 안 함

type
필수

"reasoning"

지정 안 함

parentId

string

지정 안 함

providerMetadata

PartProviderMetadata

지정 안 함

unstable_summary

string

지정 안 함

Message.Sources

source 파트를 DS Chip 링크로 렌더한다. 기본 렌더러가 favicon·제목을 자동 조립하며, Source/SourceIcon/SourceTitle(Message/Sources) 을 직접 조합하면 커스텀 구성도 가능하다.

공개 Props 없음

Message.GroupedParts

groupBy 로 인접 파트를 묶어 렌더하는 경로다.

Prop

Type

Default

children
필수

(info: RenderInfo<TKey>) => ReactNode

지정 안 함

groupBy
필수

(part: PartState, context: GroupByContext) => readonly TKey[] | null

지정 안 함

indicator

IndicatorMode

no-text

Message.Attachments

첨부 파일 목록을 렌더한다.

Prop

Type

Default

children

(value: { attachment: CompleteAttachment; }) => ReactNode

지정 안 함

components

MessageAttachmentsComponentConfig

지정 안 함

Message.Meta

시간·읽음 같은 보조 정보를 표시한다.

공개 Props 없음

Message.Error

메시지 오류 상태를 렌더하는 조건부 파트다.

공개 Props 없음

Message.If

메시지 상태 조건부 렌더 유틸이다.

Prop

Type

Default

assistant

boolean

지정 안 함

copied

boolean

지정 안 함

hasAttachments

boolean

지정 안 함

hasBranches

boolean

지정 안 함

hasContent

boolean

지정 안 함

last

boolean

지정 안 함

lastOrHover

boolean

지정 안 함

speaking

boolean

지정 안 함

submittedFeedback

"positive" | "negative" | null

지정 안 함

system

boolean

지정 안 함

user

boolean

지정 안 함

같은 패밀리

@mildang/design-system/unofficial/Chat 에서 같이 내보내는 컴포넌트다.

예제

Roles

user·assistant 두 역할의 버블 정렬과 배경을 비교합니다.

코드

// MessageRoles.example.tsx
import { css } from "@mildang/styled-system/css";
import { ThreadPrimitive } from "@assistant-ui/react";
import { Message } from "@mildang/design-system/unofficial/Chat";

// ../../examples/mockRuntime.tsx
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
  useRemoteThreadListRuntime
} from "@assistant-ui/react";
import { useState } from "react";
var mockAdapter = {
  async run() {
    return {
      content: [{ type: "text", text: "안녕하세요! 무엇을 도와드릴까요? (mock 응답)" }]
    };
  }
};
var sampleMessages = [
  { role: "user", content: "밀당 채팅 컴포넌트 잘 붙었어?" },
  {
    role: "assistant",
    content: "네! assistant-ui 프리미티브를 Panda CSS 슬롯 레시피로 스타일링했습니다. Thread / Composer / Message 3종이 렌더됩니다."
  }
];
function MockRuntimeProvider({
  children,
  initialMessages
}) {
  const runtime = useLocalRuntime(mockAdapter, { initialMessages });
  return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}

// MessageRoles.example.tsx
var column = css({
  display: "flex",
  flexDirection: "column",
  gap: "16"
});
var UserMessage = () => <Message role="user">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
  </Message>;
var AssistantMessage = () => <Message role="assistant">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
  </Message>;
var MessageRolesExample = () => <MockRuntimeProvider initialMessages={sampleMessages}>
    <div className={column}>
      <ThreadPrimitive.Root>
        <ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }} />
      </ThreadPrimitive.Root>
    </div>
  </MockRuntimeProvider>;
var MessageRoles_example_default = MessageRolesExample;
export {
  MessageRoles_example_default as default
};

추론 렌더러 연결

Message.Reasoning 을 Parts 의 Reasoning 렌더러로 전달하는 운영 계약입니다.

코드

// MessageReasoningRenderer.example.tsx
import { css } from "@mildang/styled-system/css";
import { ThreadPrimitive } from "@assistant-ui/react";
import { Message } from "@mildang/design-system/unofficial/Chat";

// ../../examples/mockRuntime.tsx
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
  useRemoteThreadListRuntime
} from "@assistant-ui/react";
import { useState } from "react";
var mockAdapter = {
  async run() {
    return {
      content: [{ type: "text", text: "안녕하세요! 무엇을 도와드릴까요? (mock 응답)" }]
    };
  }
};
function MockRuntimeProvider({
  children,
  initialMessages
}) {
  const runtime = useLocalRuntime(mockAdapter, { initialMessages });
  return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}

// MessageReasoningRenderer.example.tsx
var column = css({
  display: "flex",
  flexDirection: "column",
  gap: "16"
});
var reasoningMessages = [
  {
    role: "assistant",
    content: [
      {
        type: "reasoning",
        text: "질문의 핵심 조건을 확인하고 가장 이해하기 쉬운 설명 순서를 정했습니다."
      },
      {
        type: "text",
        text: "조건을 순서대로 살펴보면 답을 구할 수 있어요."
      }
    ]
  }
];
var UserMessage = () => <Message role="user">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
  </Message>;
var AssistantMessageWithReasoning = () => <Message role="assistant">
    <Message.Content>
      <Message.Parts components={{ Reasoning: Message.Reasoning }} />
    </Message.Content>
  </Message>;
var MessageReasoningRendererExample = () => <MockRuntimeProvider initialMessages={reasoningMessages}>
    <div className={column}>
      <ThreadPrimitive.Root>
        <ThreadPrimitive.Messages
  components={{ UserMessage, AssistantMessage: AssistantMessageWithReasoning }}
/>
      </ThreadPrimitive.Root>
    </div>
  </MockRuntimeProvider>;
var MessageReasoningRenderer_example_default = MessageReasoningRendererExample;
export {
  MessageReasoningRenderer_example_default as default
};

메타 슬롯 조립

Message.Meta 슬롯에 시간·읽음 데이터를 앱이 직접 조립합니다.

코드

// MessageWithMeta.example.tsx
import { css } from "@mildang/styled-system/css";
import { ThreadPrimitive } from "@assistant-ui/react";
import { Message } from "@mildang/design-system/unofficial/Chat";

// ../../examples/mockRuntime.tsx
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
  useRemoteThreadListRuntime
} from "@assistant-ui/react";
import { useState } from "react";
var mockAdapter = {
  async run() {
    return {
      content: [{ type: "text", text: "안녕하세요! 무엇을 도와드릴까요? (mock 응답)" }]
    };
  }
};
var sampleMessages = [
  { role: "user", content: "밀당 채팅 컴포넌트 잘 붙었어?" },
  {
    role: "assistant",
    content: "네! assistant-ui 프리미티브를 Panda CSS 슬롯 레시피로 스타일링했습니다. Thread / Composer / Message 3종이 렌더됩니다."
  }
];
function MockRuntimeProvider({
  children,
  initialMessages
}) {
  const runtime = useLocalRuntime(mockAdapter, { initialMessages });
  return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}

// MessageWithMeta.example.tsx
var column = css({
  display: "flex",
  flexDirection: "column",
  gap: "16"
});
var AssistantMessageWithMeta = () => <Message role="assistant">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
    <Message.Meta>오후 3:24</Message.Meta>
  </Message>;
var UserMessageWithMeta = () => <Message role="user">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
    <Message.Meta>오후 3:25 · 읽음</Message.Meta>
  </Message>;
var MessageWithMetaExample = () => <MockRuntimeProvider initialMessages={sampleMessages}>
    <div className={column}>
      <ThreadPrimitive.Root>
        <ThreadPrimitive.Messages
  components={{ UserMessage: UserMessageWithMeta, AssistantMessage: AssistantMessageWithMeta }}
/>
      </ThreadPrimitive.Root>
    </div>
  </MockRuntimeProvider>;
var MessageWithMeta_example_default = MessageWithMetaExample;
export {
  MessageWithMeta_example_default as default
};

소요시간 배선

Message.Meta 슬롯에 소요시간을 붙이는 예시입니다.

코드

// MessageWithDuration.example.tsx
import { css } from "@mildang/styled-system/css";
import { ThreadPrimitive } from "@assistant-ui/react";
import { Message } from "@mildang/design-system/unofficial/Chat";

// ../../examples/mockRuntime.tsx
import {
  AssistantRuntimeProvider,
  useLocalRuntime,
  useRemoteThreadListRuntime
} from "@assistant-ui/react";
import { useState } from "react";
var mockAdapter = {
  async run() {
    return {
      content: [{ type: "text", text: "안녕하세요! 무엇을 도와드릴까요? (mock 응답)" }]
    };
  }
};
var sampleMessages = [
  { role: "user", content: "밀당 채팅 컴포넌트 잘 붙었어?" },
  {
    role: "assistant",
    content: "네! assistant-ui 프리미티브를 Panda CSS 슬롯 레시피로 스타일링했습니다. Thread / Composer / Message 3종이 렌더됩니다."
  }
];
function MockRuntimeProvider({
  children,
  initialMessages
}) {
  const runtime = useLocalRuntime(mockAdapter, { initialMessages });
  return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}

// MessageWithDuration.example.tsx
var column = css({
  display: "flex",
  flexDirection: "column",
  gap: "16"
});
var UserMessage = () => <Message role="user">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
  </Message>;
var AssistantMessageWithDuration = () => <Message role="assistant">
    <Message.Content>
      <Message.Parts />
    </Message.Content>
    <Message.Meta>4.2초 만에 생성 · 오후 3:24</Message.Meta>
  </Message>;
var MessageWithDurationExample = () => <MockRuntimeProvider initialMessages={sampleMessages}>
    <div className={column}>
      <ThreadPrimitive.Root>
        <ThreadPrimitive.Messages
  components={{ UserMessage, AssistantMessage: AssistantMessageWithDuration }}
/>
      </ThreadPrimitive.Root>
    </div>
  </MockRuntimeProvider>;
var MessageWithDuration_example_default = MessageWithDurationExample;
export {
  MessageWithDuration_example_default as default
};

Density

코드

import { chatMessage } from '@mildang/styled-system/recipes';
import { css } from '@mildang/styled-system/css';

const column = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
});

const swatchLabel = css({ textStyle: 'caption-lg-medium', color: 'neutral.text.low' });

const rowGroup = css({ display: 'flex', flexDirection: 'column', gap: '4' });

const matrixText =
  '밀당 채팅 컴포넌트가 role 토큰으로 잘 번역되었는지 확인하는 예시 메시지입니다.';

// 이하 recipe-direct 매트릭스 — MessagePrimitive 는 메시지/스레드 컨텍스트가 필요하므로
// variant 시각 QA 는 chatMessage 레시피를 순수 div 에 직접 적용해 나란히 비교한다.
const Bubble = ({
  cls,
  text = matrixText,
}: {
  cls: ReturnType<typeof chatMessage>;
  text?: string;
}) => (
  <div className={cls.root}>
    <div className={cls.content}>{text}</div>
  </div>
);

const MessageDensityExample = () => (
    <div className={column}>
      {(['compact', 'balanced', 'spacious'] as const).map((density) => (
        <div key={density} className={rowGroup}>
          <span className={swatchLabel}>density: {density}</span>
          <Bubble cls={chatMessage({ role: 'user', density })} />
        </div>
      ))}
    </div>
  );

export default MessageDensityExample;

Grouping

연속된 user 버블의 발신자쪽 모서리를 타이트닝합니다.

코드

import { chatMessage } from '@mildang/styled-system/recipes';
import { css } from '@mildang/styled-system/css';

const column = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
});

const swatchLabel = css({ textStyle: 'caption-lg-medium', color: 'neutral.text.low' });

const rowGroup = css({ display: 'flex', flexDirection: 'column', gap: '4' });

const matrixText =
  '밀당 채팅 컴포넌트가 role 토큰으로 잘 번역되었는지 확인하는 예시 메시지입니다.';

// 이하 recipe-direct 매트릭스 — MessagePrimitive 는 메시지/스레드 컨텍스트가 필요하므로
// variant 시각 QA 는 chatMessage 레시피를 순수 div 에 직접 적용해 나란히 비교한다.
const Bubble = ({
  cls,
  text = matrixText,
}: {
  cls: ReturnType<typeof chatMessage>;
  text?: string;
}) => (
  <div className={cls.root}>
    <div className={cls.content}>{text}</div>
  </div>
);

const MessageGroupingExample = () => (
    <div className={column}>
      <div className={rowGroup}>
        <span className={swatchLabel}>single</span>
        <Bubble cls={chatMessage({ role: 'user', grouping: 'single' })} text="단일 버블" />
      </div>
      <div className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
        <span className={swatchLabel}>first → middle → last (연속 흐름)</span>
        <Bubble cls={chatMessage({ role: 'user', grouping: 'first' })} text="첫 번째 메시지" />
        <Bubble cls={chatMessage({ role: 'user', grouping: 'middle' })} text="중간 메시지" />
        <Bubble cls={chatMessage({ role: 'user', grouping: 'last' })} text="마지막 메시지" />
      </div>
    </div>
  );

export default MessageGroupingExample;

States

코드

import { chatMessage } from '@mildang/styled-system/recipes';
import { css } from '@mildang/styled-system/css';

const column = css({
  display: 'flex',
  flexDirection: 'column',
  gap: '16',
});

const swatchLabel = css({ textStyle: 'caption-lg-medium', color: 'neutral.text.low' });

const rowGroup = css({ display: 'flex', flexDirection: 'column', gap: '4' });

const matrixText =
  '밀당 채팅 컴포넌트가 role 토큰으로 잘 번역되었는지 확인하는 예시 메시지입니다.';

// 이하 recipe-direct 매트릭스 — MessagePrimitive 는 메시지/스레드 컨텍스트가 필요하므로
// variant 시각 QA 는 chatMessage 레시피를 순수 div 에 직접 적용해 나란히 비교한다.
const Bubble = ({
  cls,
  text = matrixText,
}: {
  cls: ReturnType<typeof chatMessage>;
  text?: string;
}) => (
  <div className={cls.root}>
    <div className={cls.content}>{text}</div>
  </div>
);

const MessageStatesExample = () => (
    <div className={column}>
      <div className={rowGroup}>
        <span className={swatchLabel}>idle</span>
        <Bubble cls={chatMessage({ role: 'user', state: 'idle' })} />
      </div>
      <div className={rowGroup}>
        <span className={swatchLabel}>loading (shimmer)</span>
        <Bubble cls={chatMessage({ role: 'user', state: 'loading' })} text="응답 생성 중…" />
      </div>
      <div className={rowGroup}>
        <span className={swatchLabel}>failed (opacity)</span>
        <Bubble cls={chatMessage({ role: 'user', state: 'failed' })} text="전송에 실패했습니다" />
      </div>
    </div>
  );

export default MessageStatesExample;