"use client"; import { useCallback, useState } from "react"; import type { Message } from "@/hooks/useChat"; interface MessageListProps { messages: Message[]; isLoading: boolean; loadingQuip?: string | null; } /** * Parse thinking content from message. * Extracts ... or ... blocks. */ function parseThinking(content: string): { thinking: string | null; response: string } { // Match ... or ... blocks const thinkingRegex = /<(?:thinking|think)>([\s\S]*?)<\/(?:thinking|think)>/gi; const matches = content.match(thinkingRegex); if (!matches) { return { thinking: null, response: content }; } // Extract thinking content let thinking = ""; for (const match of matches) { const innerContent = match.replace(/<\/?(?:thinking|think)>/gi, ""); thinking += innerContent.trim() + "\n"; } // Remove thinking blocks from response const response = content.replace(thinkingRegex, "").trim(); const trimmedThinking = thinking.trim(); return { thinking: trimmedThinking.length > 0 ? trimmedThinking : null, response, }; } export function MessageList({ messages, isLoading, loadingQuip, }: MessageListProps): React.JSX.Element { return (
{messages.map((message) => ( ))} {isLoading && }
); } function MessageBubble({ message }: { message: Message }): React.JSX.Element { const isUser = message.role === "user"; const [copied, setCopied] = useState(false); const [thinkingExpanded, setThinkingExpanded] = useState(false); // Parse thinking from content (or use pre-parsed thinking field) const { thinking, response } = message.thinking ? { thinking: message.thinking, response: message.content } : parseThinking(message.content); const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(response); setCopied(true); setTimeout(() => { setCopied(false); }, 2000); } catch (err) { // Silently fail - clipboard copy is non-critical void err; } }, [response]); return (
{/* Avatar */} {/* Message Content */}
{/* Message Header */}
{isUser ? "You" : "AI Assistant"} {/* Model indicator for assistant messages */} {!isUser && message.model && ( {message.model} )} {/* Token usage indicator for assistant messages */} {!isUser && message.totalTokens !== undefined && message.totalTokens > 0 && ( {formatTokenCount(message.totalTokens)} tokens )} {formatTime(message.createdAt)}
{/* Thinking Section - Collapsible */} {thinking && !isUser && (
{thinkingExpanded && (
{thinking}
)}
)} {/* Message Body */}

{response}

{/* Copy Button - appears on hover */}
); } function LoadingIndicator({ quip }: { quip?: string | null }): React.JSX.Element { return (
{/* Avatar */} {/* Loading Content */}
AI Assistant thinking...
{quip && ( {quip} )}
); } function formatTime(isoString: string): string { try { const date = new Date(isoString); return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return ""; } } function formatTokenCount(tokens: number): string { if (tokens >= 1000000) { return `${(tokens / 1000000).toFixed(1)}M`; } else if (tokens >= 1000) { return `${(tokens / 1000).toFixed(1)}K`; } return tokens.toString(); }