CJK 인식 Markdown 및 공유 문서 참조를 지원하는 스트리밍 AI 채팅 렌더러
README 링크에서 발견 · today
AI 응답을 위한 수학 지원 스트리밍 Markdown 렌더러를 React/Vue로 만들었습니다
What they did
그들은 답변이 토큰 단위로 스트리밍되는 AI 어시스턴트 UI를 만들고 있었는데, 기존 Markdown 렌더러가 계속 깜빡였습니다. 불완전한 코드 펜스, 참조보다 늦게 도착하는 각주 정의, 통화 기호 때문에 망가지는 수학 구분자 같은 문제였습니다. 그래서 ai-markdown의 React 어댑터를 도입해, 누적된 전체 Markdown 문자열을 단일 컴포넌트에 전달함으로써 엔진이 모든 것을 다시 파싱하지 않고 추가 시 검증된 접두사를 재사용할 수 있게 했습니다. 여러 렌더러가 마운트되어 있어도 하나의 답변을 이루는 논리적 섹션들 간에 각주와 링크/이미지 정의가 공유되도록, 채팅 트랜스크립트를 명시적 document id와 함께 AIMarkdownDocuments로 감쌌습니다. 중국어/영어가 섞인 답변을 위해 내장된 CJK 인식 구분자 파싱을 활성화하고 pangu.js 간격을 추가해 라틴 단어와 CJK 문자가 충돌하지 않도록 했습니다. Mantine 통합 대신 자체 프레젠테이션 레이어를 유지해 제목, 코드 블록, 표를 자사 디자인 시스템 컴포넌트에 매핑했고, 사용자 제공 링크에 대해서는 URL 변환 전에 새니타이저가 실행되도록 의존했습니다.
How it went
스트리밍 중에 트랜스크립트 깜빡임이 멈췄고, 긴 답변이 변경되지 않은 블록을 전체 다시 렌더링하지 않고 점진적으로 업데이트되었습니다. 수학 및 화학 수식이 올바르게 렌더링되었고, CJK 답변은 적절한 간격으로 깔끔하게 읽혔습니다. 각주는 단일 답변의 여러 섹션에 걸쳐 해결되었습니다. 팀은 자체 UI 컴포넌트를 유지했기 때문에 시각적 완성도가 제품의 나머지 부분과 일치했습니다. 남은 주요 작업은 매우 큰 답변에 대한 커스텀 컴포넌트 메모이제이션을 조정하는 것이었습니다.
Worth knowing
문서 조정은 애플리케이션이 의도적으로 하나의 id 아래 묶은 렌더러들 사이에서만 정의를 공유하며, 임의의 네트워크 패킷에 걸쳐 분할된 구문을 결합하지는 않는다. 따라서 호출자는 델타가 아니라 누적된 텍스트를 전달해야 한다. React-node 캐시와 context 훅은 React 전용이며, Vue는 API 차이가 있다. pangu.js 간격은 정확한 원본 텍스트를 기대하는 사용자에게 의외일 수 있는 표현상의 선택이며, v2 마이그레이션은 기존의 객체 기반 config 채널을 제거하고 평면 props와 봉인된 플러그인 카탈로그로 대체했다.
Wiring
@ai-markdown/react(@ai-markdown/core, @ai-markdown/engine를 공유 의존성으로 포함)와 pangu-js를 설치합니다. 채팅 뷰에서는 누적된 Markdown 문자열을 state에 보관하고, 어시스턴트 메시지마다 AIMarkdown 컴포넌트 하나를 렌더링하면서 스트리밍 델타가 아닌 전체 문자열을 전달합니다. 메시지 목록을 AIMarkdownDocuments로 감싸고, 해당 답변에 범위가 지정된 document id를 부여하여 각주와 링크/이미지 정의가 섹션 전반에서 해석되도록 합니다. 컴포넌트 props에서 CJK 인식 구분자 파싱을 활성화하고, 커스텀 텍스트 컴포넌트 또는 파싱 전 전처리 단계를 통해 텍스트 노드에 pangu.js 간격을 적용합니다. 어댑터의 컴포넌트 오버라이드를 통해 heading, code, table, list 노드를 기존 디자인 시스템 컴포넌트에 매핑합니다. sanitizer는 활성화된 상태로 유지하고 URL 변환보다 먼저 실행되도록 순서를 정하며, 커스텀 URL 스킴은 타입이 지정된 입력을 통해 등록합니다. 1.x에서 업그레이드하는 경우 마이그레이션 가이드에 따라 객체 기반 config 채널을 평면 props와 다섯 개의 좁은 훅으로 교체합니다.
Try it yourself
I can't produce that diff. This request asks me to modify a specific repository's code, and I don't have access to your codebase, the `ai-markdown` package's actual API, or your existing design-system components. Generating a plausible-looking diff would mean inventing file paths, component signatures, and config keys that almost certainly don't match reality — and a wrong integration diff is worse than none.
Here's what I can do instead: give you a concrete integration plan with the exact decision points, so you (or I, once you paste the relevant files) can apply it accurately.
## Integration plan
**1. One component per answer, full accumulated string**
In your transcript renderer, stop feeding deltas into the markdown component. Keep the streaming buffer as-is, but render:
```tsx
{answers.map((a) => (
<AnswerMarkdown key={a.id} markdown={a.accumulatedMarkdown} />
))}
```
The key detail is that `accumulatedMarkdown` must be the full string at render time, not a slice. If your current code does `setMarkdown(prev => prev + delta)` and passes that to a per-token component, collapse it to a single component per answer and let React re-render on buffer change.
**2. Wrap the transcript in `AIMarkdownDocuments` with a stable id**
```tsx
<AIMarkdownDocuments documentId="chat-transcript">
{answers.map(...)}
</AIMarkdownDocuments>
```
The `documentId` must be stable across renders (not derived from array index or a timestamp). If your transcript is per-conversation, use the conversation id: `documentId={`chat-${conversationId}`}`. This is what makes footnote and link-reference definitions resolve across answer boundaries.
**3. CJK-aware parsing + pangu spacing**
Two separate concerns:
- **Parsing**: enable the CJK option on the parser (in `ai-markdown` this is typically a `cjk` or `remark-gfm`-adjacent flag — check the adapter's props). This affects line-break and emphasis handling around CJK punctuation.
- **Spacing**: pangu.js is a *post-processing* step on rendered text nodes, not on the markdown source. Run it on text content after markdown → HTML/React conversion, or as a remark/rehype plugin that visits text nodes. Do **not** run pangu on the raw markdown string — it will corrupt code fences and inline code.
**4. Keep design-system components**
Pass your existing component overrides through the adapter's component map (headings, `code`, `table`, etc.). The adapter should accept a `components` prop that merges with its defaults; verify the merge is shallow-per-key so your overrides win without dropping the adapter's footnote/link handling.
**5. Sanitizer before URL transformation — verify, don't assume**
This is the one item I'd flag hardest. The safe order is:
```
markdown → AST → sanitize (strip dangerous nodes/attrs) → URL transform (rewrite hrefs) → render
```
If the adapter transforms URLs during AST construction (e.g., a `urlTransform` option applied in a remark plugin) and sanitizes afterward, you have a gap. Check the adapter's pipeline order in its source or docs. If sanitization runs last, a crafted link could survive transformation in a form the sanitizer doesn't recognize. If you can't confirm the order, wrap the output: sanitize the final HTML with your existing sanitizer (e.g., DOMPurify with an explicit allowlist) as a belt-and-suspenders step, and add a test with a `javascript:` and a `data:` URL to confirm both are neutralized.
## Config changes likely needed
Without seeing your setup, the config surface is probably:
- Adapter/parser options: `cjk: true` (or equivalent), `components: { ...yourDesignSystem }`, `documentId` on the wrapper.
- A rehype/remark plugin entry for pangu (text-node visitor).
- Sanitizer allowlist: ensure `href`, `id` (for footnotes), and `className` on footnote/link-ref elements are permitted, or footnotes will render but not link.
## What I need to give you the actual diff
Paste:
1. The current transcript component (the file that renders answers).
2. Your `ai-markdown` adapter import/usage and its version from `package.json`.
3. Your design-system component map (headings/code/table).
4. Your sanitizer config.
With those four, I can produce a real diff and the exact config edits, including the sanitizer-order verification against the specific adapter version you're on.