Render the assistant's replies as Markdown
The assistant answers in Markdown — tables, lists, bold, links — but the chat bubble showed the raw source (whitespace-pre-wrap), so a table came through as a wall of pipes. - New MarkdownMessage: react-markdown + remark-gfm (for GFM tables). It does NOT enable raw HTML (no rehype-raw), so a model reply can't inject markup; every element the assistant actually uses is Tailwind-styled for a chat bubble, and wide content (tables, code) scrolls in its own box so the bubble can't blow out. - Only ASSISTANT bubbles render as Markdown; the user's own text stays literal (their `*` shouldn't become a bullet) and the assistant bubble goes full-width so a table has room. - Lazy-loaded: the renderer + its ecosystem (~150 KB) is its own async chunk, loaded only when an assistant message renders — not for everyone who opens the editor. To keep it out of the eager first paint, manualChunks now vendors only the app-wide core (react/react-dom/scheduler kept together — splitting react-dom breaks React 19 at load) and lets everything else ride with its importer. Tested via renderToStaticMarkup (node, no DOM): a GFM table renders with styled cells + horizontal scroll; **bold**/lists render; raw <script>/<b> are escaped not emitted; links get rel="noopener noreferrer". App re-verified to mount cleanly with the re-chunk (no React-19 init-order break). tsc + vitest (99) + build green, no >500 KB warning. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { Suspense, lazy, useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { Alert } from '@/components/ui/Alert'
|
||||
import { errorMessage } from '@/lib/api'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
import { useUndo } from '@/lib/history'
|
||||
import { UndoButton } from './UndoButton'
|
||||
|
||||
// Lazy so the markdown renderer + its ecosystem (~150 KB) loads only when an
|
||||
// assistant message actually renders, not for everyone who opens the editor.
|
||||
const MarkdownMessage = lazy(() =>
|
||||
import('./MarkdownMessage').then((m) => ({ default: m.MarkdownMessage })),
|
||||
)
|
||||
|
||||
/**
|
||||
* Talk to the garden assistant, in the editor beside the canvas.
|
||||
*
|
||||
@@ -227,11 +233,24 @@ function Bubble({
|
||||
<div className={cn('flex flex-col', mine ? 'items-end' : 'items-start')}>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[90%] whitespace-pre-wrap rounded-lg px-2.5 py-2 text-sm',
|
||||
mine ? 'bg-accent/15 text-fg' : 'border border-border text-fg',
|
||||
'rounded-lg px-2.5 py-2 text-sm',
|
||||
// The user's own text is literal (their `*` shouldn't become a bullet)
|
||||
// and hugs the right; the assistant's Markdown is rendered and gets the
|
||||
// full width so a table has room.
|
||||
mine
|
||||
? 'max-w-[90%] whitespace-pre-wrap bg-accent/15 text-fg'
|
||||
: 'w-full border border-border text-fg',
|
||||
)}
|
||||
>
|
||||
{body}
|
||||
{mine ? (
|
||||
body
|
||||
) : (
|
||||
// Show the raw text until the renderer chunk arrives, so a message never
|
||||
// flashes blank.
|
||||
<Suspense fallback={<span className="whitespace-pre-wrap">{body}</span>}>
|
||||
<MarkdownMessage>{body}</MarkdownMessage>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user