SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
2.5 KB · 73 lines tsx
Raw Blame History
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