import { useState, useRef, useEffect, useCallback } from "react"; import { Streamdown } from "streamdown"; import { createMathPlugin } from "@streamdown/math"; import { Pencil, X, Check, RotateCcw, Copy, ClipboardCheck, Gauge } from "lucide-react"; import { useLLM } from "../hooks/useLLM"; import type { ChatMessage } from "../hooks/LLMContext"; const math = createMathPlugin({ singleDollarTextMath: true }); interface MessageBubbleProps { msg: ChatMessage; index: number; isStreaming?: boolean; isGenerating: boolean; } const MATH_COMMANDS: { prefix: string; args: number }[] = [ { prefix: "\\boxed{", args: 1 }, { prefix: "\\text{", args: 1 }, { prefix: "\\textbf{", args: 1 }, { prefix: "\\mathbf{", args: 1 }, { prefix: "\\mathrm{", args: 1 }, { prefix: "\\frac{", args: 2 }, ]; function skipBraceGroup(content: string, start: number): number { let depth = 1; let j = start; while (j < content.length && depth > 0) { if (content[j] === "{") depth++; else if (content[j] === "}") depth--; j++; } return j; } function wrapLatexMath(content: string): string { let result = ""; let i = 0; let mathContext: null | "$" | "$$" = null; while (i < content.length) { const cmd = !mathContext ? MATH_COMMANDS.find((candidate) => content.startsWith(candidate.prefix, i)) : undefined; if (cmd) { let j = skipBraceGroup(content, i + cmd.prefix.length); for (let argIndex = 1; argIndex < cmd.args; argIndex++) { if (content[j] === "{") { j = skipBraceGroup(content, j + 1); } } const expr = content.slice(i, j); result += "$" + expr + "$"; i = j; } else if (content[i] === "$") { const isDouble = content[i + 1] === "$"; const token = isDouble ? "$$" : "$"; if (mathContext === token) mathContext = null; else if (!mathContext) mathContext = token; result += token; i += token.length; } else { result += content[i]; i++; } } return result; } function prepareForMathDisplay(content: string): string { return wrapLatexMath( content .replace(/(?(null); const handleCopy = useCallback(async () => { await navigator.clipboard.writeText(msg.content); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [msg.content]); const handleRetry = useCallback(() => { retryMessage(index); }, [retryMessage, index]); useEffect(() => { if (isEditing && textareaRef.current) { textareaRef.current.focus(); textareaRef.current.style.height = "auto"; textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"; } }, [isEditing]); const handleEdit = useCallback(() => { setEditValue(msg.content); setIsEditing(true); }, [msg.content]); const handleCancel = useCallback(() => { setIsEditing(false); setEditValue(msg.content); }, [msg.content]); const handleSave = useCallback(() => { const trimmed = editValue.trim(); if (!trimmed) return; setIsEditing(false); editMessage(index, trimmed); }, [editValue, editMessage, index]); const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === "Escape") handleCancel(); if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSave(); } }, [handleCancel, handleSave], ); const displayTps = !isStreaming && isAssistant && msg.tps ? msg.tps : null; if (isEditing) { return (
No response
) : null}