import React from 'react'; import type { Components } from 'react-markdown'; import Mermaid from '../../Mermaid'; import { ANCHOR } from '../../../chat/styles/bubbleTokens'; import { CodeBlock, CodeBlockFallback } from './CodeBlock'; import { extractTextFromChildren } from './plainText'; /** Body / heading size classes per `size` level. */ const SIZE_CLASSES: Record<'xs' | 'sm' | 'base', { text: string; headingBase: string; headingSm: string; }> = { xs: { text: 'text-xs', headingBase: 'text-sm', headingSm: 'text-xs' }, sm: { text: 'text-sm', headingBase: 'text-base', headingSm: 'text-sm' }, base: { text: 'text-base', headingBase: 'text-lg', headingSm: 'text-base' }, }; /** * Build the chat-tuned markdown component map. * * Body text size follows `size` (xs/sm/base → 12/14/16px). Heading * sizes are scaled a notch above the body so inline-in-chat headings * stand out without dominating. */ export function createMarkdownComponents( isUser: boolean = false, size: 'xs' | 'sm' | 'base' = 'sm', codeTheme: 'dark' | 'light' = 'dark', ): Components { const { text: textSize, headingBase, headingSm } = SIZE_CLASSES[size]; // Code blocks / diagrams only have a binary compact mode — treat the // smallest text level as compact, larger ones as roomy. const isCompact = size === 'xs'; return { h1: ({ children }) => (
{children}
), ul: ({ children }) => (: let pre handle styling.
if (className?.includes('language-')) {
return {children};
}
// Inline `` uses the design system's `--code-inline`
// token. One semantic chip surface across both themes; on a
// user bubble we still palette-switch with `text-primary-
// foreground` because the inline chip blends INTO the bubble
// — there's no panel boundary like a fence has. We trade a
// perfect "code surface" tone here for legible body inheritance,
// matching ChatGPT's behaviour in coloured user bubbles.
const inlineCodeClass = isUser
? 'bg-primary-foreground/15 text-primary-foreground'
: 'bg-code-inline text-code-inline-foreground';
return (
{extractTextFromChildren(children)}
);
},
// Modern chat convention drops italic on blockquotes — italic +
// tight bubble = hard to read. Border-left at 2px (4px reads
// heavy in a 320–480px bubble). On the saturated user bubble we
// use a primary-foreground tint; on the assistant bubble we use
// the muted-foreground role for de-emphasis.
blockquote: ({ children }) => {
const cls = isUser
? 'border-primary-foreground/40 text-primary-foreground/80'
: 'border-border text-muted-foreground';
return (
{children}
);
},
// Tables: outer wrapper handles overflow, inner ``
// inherits the chat-density text size. Borders / header use
// semantic tokens — `border-code-border` for the assistant
// (matches the code-fence panel for visual cohesion when both
// appear in the same reply); primary-foreground/N for the user
// bubble so lines read against the saturated `bg-primary`.
table: ({ children }) => (
{children}
),
thead: ({ children }) => (
{children}
),
tbody: ({ children }) => {children},
tr: ({ children }) => (
{children}
),
th: ({ children }) => {
const borderCls = isUser ? 'border-primary-foreground/25' : 'border-border';
return (
{children}
);
},
td: ({ children }) => {children} ,
// Soft separator. ChatGPT / Slack / Linear strip the visible
// line, Claude.ai keeps a hairline. We follow Claude — present
// but quiet. Palette switches by role so the hairline reads on
// both surfaces.
hr: () => (
),
strong: ({ children }) => {children},
em: ({ children }) => {children},
};
}