import { useEffect, useRef } from "react"; import type { ChatMessage, ToolState } from "./derive"; const PREVIEW_LEN: number = 120; function oneLine(text: string): string { const flat = text.replace(/\s+/g, " ").trim(); return flat.length > PREVIEW_LEN ? `${flat.slice(0, PREVIEW_LEN)}…` : flat; } function ToolCard({ tool }: { tool: ToolState }) { const status: string = tool.running ? "running…" : tool.isError ? "error" : "done"; const statusClass: string = tool.isError ? "tool-status-err" : "tool-status-ok"; return (
🛠 {tool.name} {tool.running ? "working…" : status}
args
{tool.args}
result
{tool.preview}
); } function Thinking({ text }: { text: string }) { return (
thinking
{text}
); } export function Bubble({ msg, tools }: { msg: ChatMessage; tools: Map }) { const rowClass = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : "system"; const msgTools: ToolState[] = []; if (msg.role === "assistant") { for (const c of msg.toolCalls) { const t = tools.get(c.id); if (t !== undefined) msgTools.push(t); } } return (
{msg.thinking !== null && msg.thinking.length > 0 && } {msgTools.map((t) => ( ))} {msg.role === "toolResult" ? (
result {oneLine(msg.text)}
{msg.text}
) : ( msg.text.length > 0 &&
{msg.text}
)} {msg.streaming && }
); } export function TypingIndicator() { return (
); } interface Props { messages: ChatMessage[]; tools: Map; busy: boolean; } export default function ChatStream({ messages, tools, busy }: Props) { const scrollRef = useRef(null); const pinnedRef = useRef(true); const onScroll = (): void => { const el = scrollRef.current; if (el === null) return; pinnedRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80; }; useEffect(() => { const el = scrollRef.current; if (el !== null && pinnedRef.current) el.scrollTop = el.scrollHeight; }, [messages, busy]); useEffect(() => { pinnedRef.current = true; const el = scrollRef.current; if (el !== null) el.scrollTop = el.scrollHeight; }, []); const last: ChatMessage | undefined = messages[messages.length - 1]; const streamingOpen: boolean = last !== undefined && last.streaming; const showTyping: boolean = busy && !streamingOpen; return (
{messages.map((m) => ( ))} {showTyping && }
); }