import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import type { ChatMessage } from './types'; import { ToolActivity } from './ToolActivity'; import { QuestionActivity } from './QuestionActivity'; const FRONTMATTER_RE = /^([\s\S]*?)<\/frontmatter>\s*/; const CollapsibleBlock = ({ label, content }: { label: string; content: string }) => (
{label}
{content}
); type MessageBubbleProps = { message: ChatMessage; onAnswer?: (text: string) => void; }; export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { switch (message.role) { case 'user': { const match = message.text.match(FRONTMATTER_RE); const metadata = match ? match[1]!.trim() : null; const text = match ? message.text.slice(match[0]!.length) : message.text; return ( <> {metadata && }
{message.images?.map((img, i) => ( {img.filename} ))}
{text}
); } case 'system': return ; case 'assistant': if (!message.text) return null; return (
{message.text}
); case 'tool': if (message.toolName === 'question' && onAnswer) { return ; } return ; case 'result': return (
Done · ${message.costUsd.toFixed(3)} · {(message.durationMs / 1000).toFixed(1)}s · {message.numTurns} turn {message.numTurns !== 1 ? 's' : ''} {message.isError ? ' (with errors)' : ''}
); case 'error': return (
{message.text}
); } }; type StreamingBubbleProps = { text: string; }; export const StreamingBubble = ({ text }: StreamingBubbleProps) => { if (!text) return null; return (
{text}
); };