Markdown
Data Display
스트리밍 마크다운 본문 렌더러.
Usage
assistant-ui 텍스트 파트의 마크다운을 코드 하이라이팅·수식과 함께 렌더링할 때 사용합니다. 스트리밍 도중에는 부드러운 글자 등장 효과가 자동 적용됩니다.
import
import
import { Markdown } from '@mildang/design-system/unofficial/Chat';예제를 복사해 쓸 때 필요한 준비
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
• 외부 패키지를 따로 설치한다. DS 의 전이 의존성에 기대지 않는다.
API Reference
Markdown Props
Prop
Type
Default
string
지정 안 함
boolean | SmoothOptions
true
같은 패밀리
@mildang/design-system/unofficial/Chat 에서 같이 내보내는 컴포넌트다.
Attachment
메시지에 첨부된 파일을 보여주는 카드.
ChainOfThought
AI 사고 과정을 접고 펼치는 타임라인.
ChatAgentCard
에이전트 이름·역할·스킬을 보여주는 소개 카드.
ChatAgentPlan
진행 단계를 순서대로 보여주는 체크리스트.
ChatCodeDiff
파일 변경 내용을 줄 단위로 보여주는 뷰어.
ChatCodeRunner
코드와 실행 결과를 함께 보여주는 블록.
ChatDiffStat
추가·삭제 줄 수를 요약하는 표시.
ChatDocumentReference
업로드 문서의 페이지·인용 구간을 보여주는 카드.
ChatInlineCitation
본문 안에 삽입하는 출처 각주 마커.
ChatResultSummaryCard
작업 결과 요약과 후속 액션 카드.
ChatRetrievalChunks
검색으로 찾은 청크 목록과 점수 표시.
ChatTerminalBlock
명령과 실행 결과를 보여주는 읽기 전용 터미널 블록.
ChatTodoList
항목별 상태를 보여주는 할 일 목록.
Message
assistant-ui 메시지를 감싸는 역할별 버블 컨테이너.
SwapLabel
두 라벨을 폭 애니메이션과 함께 교차 전환하는 표시부.
SystemMessage
대화 흐름 중간에 넣는 저채도 시스템 안내.
Tool
도구 호출 렌더 컴포넌트의 네임스페이스.
ToolFallback
등록되지 않은 도구 호출을 보여주는 기본 카드.
ToolGroup
연속된 도구 호출을 하나로 묶어 접는 래퍼.
ToolGroupByIndices
구 버전 ToolGroup 계약을 위한 인덱스 기반 어댑터.
예제
기본 마크다운 렌더링
코드
// MarkdownDemo.example.tsx
import { css } from "@mildang/styled-system/css";
import { MessagePrimitive, ThreadPrimitive } from "@assistant-ui/react";
import { Markdown } 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>;
}
// MarkdownDemo.example.tsx
var markdownContent = `# 마크다운 제목 (h1)
일반 문단입니다. **굵게**, *기울임*, \`인라인 코드\`, 그리고 [링크](https://mildang.kr)를 포함합니다.
## 소제목 (h2)
- 순서 없는 목록 1
- 순서 없는 목록 2
1. 순서 있는 목록 1
2. 순서 있는 목록 2
> 인용문 블록입니다.
\`\`\`ts
const greet = (name: string) => \`안녕, \${name}\`;
\`\`\`
| 이름 | 값 |
| --- | --- |
| 알파 | 1 |
| 베타 | 2 |
`;
var messages = [{ role: "assistant", content: markdownContent }];
var MarkdownText = () => <Markdown />;
var AssistantMessage = () => <MessagePrimitive.Root>
<MessagePrimitive.Parts components={{ Text: MarkdownText }} />
</MessagePrimitive.Root>;
var UserMessage = () => null;
var wrapper = css({ maxWidth: "chat.thread.maxWidth" });
var MarkdownDemoExample = () => <MockRuntimeProvider initialMessages={messages}>
<div className={wrapper}>
<ThreadPrimitive.Root>
<ThreadPrimitive.Messages components={{ AssistantMessage, UserMessage }} />
</ThreadPrimitive.Root>
</div>
</MockRuntimeProvider>;
var MarkdownDemo_example_default = MarkdownDemoExample;
export {
MarkdownDemo_example_default as default
};
코드 블록 하이라이팅
코드
// MarkdownCodeBlock.example.tsx
import { css } from "@mildang/styled-system/css";
import { MessagePrimitive, ThreadPrimitive } from "@assistant-ui/react";
import { Markdown } 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>;
}
// MarkdownCodeBlock.example.tsx
var codeContent = `언어가 지정된 코드블록은 shiki로 하이라이팅되고, 헤더에 언어 라벨과 복사 버튼이 붙습니다.
\`\`\`tsx
export const add = (a: number, b: number) => a + b;
console.log(add(1, 2));
\`\`\`
언어가 없는 코드블록도 복사 버튼은 제공됩니다.
\`\`\`
$ pnpm --filter @mildang/design-system storybook
\`\`\`
`;
var codeMessages = [{ role: "assistant", content: codeContent }];
var MarkdownText = () => <Markdown />;
var AssistantMessage = () => <MessagePrimitive.Root>
<MessagePrimitive.Parts components={{ Text: MarkdownText }} />
</MessagePrimitive.Root>;
var UserMessage = () => null;
var wrapper = css({ maxWidth: "chat.thread.maxWidth" });
var MarkdownCodeBlockExample = () => <MockRuntimeProvider initialMessages={codeMessages}>
<div className={wrapper}>
<ThreadPrimitive.Root>
<ThreadPrimitive.Messages components={{ AssistantMessage, UserMessage }} />
</ThreadPrimitive.Root>
</div>
</MockRuntimeProvider>;
var MarkdownCodeBlock_example_default = MarkdownCodeBlockExample;
export {
MarkdownCodeBlock_example_default as default
};
스트리밍 중 렌더링
코드
import { TextMessagePartProvider } from '@assistant-ui/react';
import { Markdown } from '@mildang/design-system/unofficial/Chat';
import { css } from '@mildang/styled-system/css';
import { useEffect, useState } from 'react';
const markdownContent = `# 마크다운 제목 (h1)
일반 문단입니다. **굵게**, *기울임*, \`인라인 코드\`, 그리고 [링크](https://mildang.kr)를 포함합니다.
## 소제목 (h2)
- 순서 없는 목록 1
- 순서 없는 목록 2
1. 순서 있는 목록 1
2. 순서 있는 목록 2
> 인용문 블록입니다.
\`\`\`ts
const greet = (name: string) => \`안녕, \${name}\`;
\`\`\`
| 이름 | 값 |
| --- | --- |
| 알파 | 1 |
| 베타 | 2 |
`;
const wrapper = css({ maxWidth: 'chat.thread.maxWidth' });
// ── 스트리밍 데모 ─────────────────────────────────────────────────────────────
// 실제 LLM처럼 청크(여러 글자) 단위로 도착하는 텍스트를 Markdown이 어떻게 렌더하는지 시연.
// TextMessagePartProvider(text + isRunning)로 part 컨텍스트를 직접 공급하므로 런타임이 필요 없다.
// Markdown의 smooth(기본 true, useSmooth)가 청크를 글자 단위 타이프라이터로 풀어낸다.
const CHUNK_SIZE = 16;
const CHUNK_INTERVAL_MS = 220;
const useStreamingLength = () => {
const [length, setLength] = useState(0);
const done = length >= markdownContent.length;
useEffect(() => {
if (done) return;
const id = setInterval(() => setLength((prev) => prev + CHUNK_SIZE), CHUNK_INTERVAL_MS);
return () => clearInterval(id);
}, [done]);
return { done, length };
};
const StreamingDemo = ({ smooth }: { smooth: boolean }) => {
const { done, length } = useStreamingLength();
return (
<div className={wrapper}>
<TextMessagePartProvider text={markdownContent.slice(0, length)} isRunning={!done}>
<Markdown smooth={smooth} />
</TextMessagePartProvider>
</div>
);
};
const MarkdownStreamingExample = () => <StreamingDemo smooth />;
export default MarkdownStreamingExample;
부드러운 효과 끈 스트리밍
코드
import { TextMessagePartProvider } from '@assistant-ui/react';
import { Markdown } from '@mildang/design-system/unofficial/Chat';
import { css } from '@mildang/styled-system/css';
import { useEffect, useState } from 'react';
const markdownContent = `# 마크다운 제목 (h1)
일반 문단입니다. **굵게**, *기울임*, \`인라인 코드\`, 그리고 [링크](https://mildang.kr)를 포함합니다.
## 소제목 (h2)
- 순서 없는 목록 1
- 순서 없는 목록 2
1. 순서 있는 목록 1
2. 순서 있는 목록 2
> 인용문 블록입니다.
\`\`\`ts
const greet = (name: string) => \`안녕, \${name}\`;
\`\`\`
| 이름 | 값 |
| --- | --- |
| 알파 | 1 |
| 베타 | 2 |
`;
const wrapper = css({ maxWidth: 'chat.thread.maxWidth' });
// ── 스트리밍 데모 ─────────────────────────────────────────────────────────────
// 실제 LLM처럼 청크(여러 글자) 단위로 도착하는 텍스트를 Markdown이 어떻게 렌더하는지 시연.
// TextMessagePartProvider(text + isRunning)로 part 컨텍스트를 직접 공급하므로 런타임이 필요 없다.
// Markdown의 smooth(기본 true, useSmooth)가 청크를 글자 단위 타이프라이터로 풀어낸다.
const CHUNK_SIZE = 16;
const CHUNK_INTERVAL_MS = 220;
const useStreamingLength = () => {
const [length, setLength] = useState(0);
const done = length >= markdownContent.length;
useEffect(() => {
if (done) return;
const id = setInterval(() => setLength((prev) => prev + CHUNK_SIZE), CHUNK_INTERVAL_MS);
return () => clearInterval(id);
}, [done]);
return { done, length };
};
const StreamingDemo = ({ smooth }: { smooth: boolean }) => {
const { done, length } = useStreamingLength();
return (
<div className={wrapper}>
<TextMessagePartProvider text={markdownContent.slice(0, length)} isRunning={!done}>
<Markdown smooth={smooth} />
</TextMessagePartProvider>
</div>
);
};
const MarkdownStreamingWithoutSmoothExample = () => <StreamingDemo smooth={false} />;
export default MarkdownStreamingWithoutSmoothExample;
스트리밍 효과 비교
코드
import { useEffect, useState } from 'react';
import { HStack, VStack } from '@mildang/styled-system/jsx';
import { Button } from '@mildang/design-system/Button';
import { TextMessagePartProvider } from '@assistant-ui/react';
import { Markdown } from '@mildang/design-system/unofficial/Chat';
import { css } from '@mildang/styled-system/css';
const markdownContent = `# 마크다운 제목 (h1)
일반 문단입니다. **굵게**, *기울임*, \`인라인 코드\`, 그리고 [링크](https://mildang.kr)를 포함합니다.
## 소제목 (h2)
- 순서 없는 목록 1
- 순서 없는 목록 2
1. 순서 있는 목록 1
2. 순서 있는 목록 2
> 인용문 블록입니다.
\`\`\`ts
const greet = (name: string) => \`안녕, \${name}\`;
\`\`\`
| 이름 | 값 |
| --- | --- |
| 알파 | 1 |
| 베타 | 2 |
`;
const comparisonColumn = css({
flex: '1',
minWidth: '0',
padding: '12',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'neutral.border.low',
borderRadius: 'md',
});
const comparisonTitle = css({
display: 'block',
marginBlockEnd: '8',
color: 'neutral.text.low',
textStyle: 'chat-small-text-M',
});
// ── 스트리밍 데모 ─────────────────────────────────────────────────────────────
// 실제 LLM처럼 청크(여러 글자) 단위로 도착하는 텍스트를 Markdown이 어떻게 렌더하는지 시연.
// TextMessagePartProvider(text + isRunning)로 part 컨텍스트를 직접 공급하므로 런타임이 필요 없다.
// Markdown의 smooth(기본 true, useSmooth)가 청크를 글자 단위 타이프라이터로 풀어낸다.
const CHUNK_SIZE = 16;
const CHUNK_INTERVAL_MS = 220;
const useStreamingLength = () => {
const [length, setLength] = useState(0);
const done = length >= markdownContent.length;
useEffect(() => {
if (done) return;
const id = setInterval(() => setLength((prev) => prev + CHUNK_SIZE), CHUNK_INTERVAL_MS);
return () => clearInterval(id);
}, [done]);
return { done, length };
};
const StreamingComparisonRun = () => {
const { done, length } = useStreamingLength();
const text = markdownContent.slice(0, length);
return (
<HStack width="100%" gap="16" alignItems="flex-start">
<div className={comparisonColumn}>
<span className={comparisonTitle}>GPT형 smooth</span>
<TextMessagePartProvider text={text} isRunning={!done}>
<Markdown />
</TextMessagePartProvider>
</div>
<div className={comparisonColumn}>
<span className={comparisonTitle}>원본 청크 (smooth=false)</span>
<TextMessagePartProvider text={text} isRunning={!done}>
<Markdown smooth={false} />
</TextMessagePartProvider>
</div>
</HStack>
);
};
const StreamingComparisonDemo = () => {
const [runId, setRunId] = useState(0);
const handleReplay = () => {
setRunId((previousRunId) => previousRunId + 1);
};
return (
<VStack width="100%" maxWidth="chat.thread.maxWidth" gap="12" alignItems="flex-start">
<Button variant="tertiary" size="sm" onClick={handleReplay}>
다시 재생
</Button>
<StreamingComparisonRun key={runId} />
</VStack>
);
};
const MarkdownStreamingComparisonExample = () => <StreamingComparisonDemo />;
export default MarkdownStreamingComparisonExample;