Python 64.6%
TypeScript 33.7%
CSS 0.8%
1import { memo, useMemo } from 'react';2import ReactMarkdown from 'react-markdown';3import remarkGfm from 'remark-gfm';4import remarkMath from 'remark-math';5import rehypeKatex from 'rehype-katex';6import { CodeBlock } from './code-block';7import { cn } from '@/lib/cn';8import { guardCurrency } from '@/lib/math-guard';910/** Splits markdown into stable blocks so only the last block re-renders during streaming. */11function splitBlocks(md: string): string[] {12 const blocks: string[] = [];13 let buf: string[] = [];14 let inFence = false;15 for (const line of md.split('\n')) {16 if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;17 buf.push(line);18 if (!inFence && line.trim() === '' && buf.length > 1) {19 blocks.push(buf.join('\n'));20 buf = [];21 }22 }23 if (buf.length) blocks.push(buf.join('\n'));24 return blocks;25}2627const Block = memo(function Block({ md, conversationId, streaming }: { md: string; conversationId?: string; streaming?: boolean }) {28 return (29 <ReactMarkdown30 remarkPlugins={[remarkGfm, remarkMath]}31 rehypePlugins={[[rehypeKatex, { strict: false, throwOnError: false, output: 'html' }]]}32 components={{33 code({ className, children, ...props }) {34 const m = /language-(\w+)/.exec(className || '');35 const text = String(children).replace(/\n$/, '');36 if (m || text.includes('\n')) return <CodeBlock code={text} lang={m?.[1] || ''} conversationId={conversationId} streaming={streaming} />;37 return (38 <code className={className} {...props}>39 {children}40 </code>41 );42 },43 pre({ children }) {44 return <>{children}</>;45 },46 a({ href, children }) {47 return (48 <a href={href} target="_blank" rel="noopener noreferrer">49 {children}50 </a>51 );52 },53 table({ children }) {54 return <table>{children}</table>;55 },56 }}57 >58 {md}59 </ReactMarkdown>60 );61});6263export function StreamingText({ text, streaming, conversationId, className }: { text: string; streaming?: boolean; conversationId?: string; className?: string }) {64 const blocks = useMemo(() => splitBlocks(text).map(guardCurrency), [text]);65 return (66 <div className={cn('md', streaming && text.length > 0 && 'streaming-cursor', className)} aria-live={streaming ? 'polite' : undefined}>67 {blocks.map((b, i) => (68 <Block key={i} md={b} conversationId={conversationId} streaming={streaming && i === blocks.length - 1} />69 ))}70 </div>71 );72}73