/** * Chat Panel Component * Main chat interface with markdown rendering and message actions */ import { useRef, useEffect, useState, type KeyboardEvent } from 'react'; import Markdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; // KaTeX CSS is loaded from CDN in index.html to reduce bundle size import { Send, Square, Trash2, MessageSquare, User, Bot, RefreshCw, Pencil, Check, X, Loader2, Brain, ChevronDown, AlertCircle } from 'lucide-react'; import { useChat, useI18n } from '@/contexts'; import type { ChatMessage } from '@/types'; import styles from './ChatPanel.module.css'; /** * Custom hook for elapsed time tracking with capture. * Starts counting when isActive becomes true, stops and captures final value when it becomes false. * Returns: { liveMs, finalMs, displayMs } where displayMs shows live during active, final after. */ function useElapsedTimer(isActive: boolean): { liveMs: number; finalMs: number; displayMs: number } { const startTimeRef = useRef(null); const [liveMs, setLiveMs] = useState(0); const [finalMs, setFinalMs] = useState(0); const wasActiveRef = useRef(false); // Handle activation/deactivation useEffect(() => { if (isActive && !wasActiveRef.current) { // Just became active - start timer startTimeRef.current = Date.now(); setLiveMs(0); } else if (!isActive && wasActiveRef.current) { // Just became inactive - capture final time if (startTimeRef.current !== null) { const elapsed = Date.now() - startTimeRef.current; setFinalMs(elapsed); setLiveMs(elapsed); startTimeRef.current = null; } } wasActiveRef.current = isActive; }, [isActive]); // Tick interval while active useEffect(() => { if (!isActive || startTimeRef.current === null) return; const interval = setInterval(() => { if (startTimeRef.current !== null) { setLiveMs(Date.now() - startTimeRef.current); } }, 100); // Update every 100ms for smooth display return () => clearInterval(interval); }, [isActive]); // displayMs: live while active, final after const displayMs = isActive ? liveMs : finalMs; return { liveMs, finalMs, displayMs }; } export function ChatPanel() { const { t } = useI18n(); const { messages, isStreaming, currentStatus, sendMessage, clearMessages, stopGeneration, regenerateFrom, editMessage } = useChat(); const [input, setInput] = useState(''); const messagesEndRef = useRef(null); const textareaRef = useRef(null); // Auto-scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Auto-resize textarea useEffect(() => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = 'auto'; textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; } }, [input]); const handleSend = async () => { if (!input.trim() || isStreaming) return; const content = input; setInput(''); await sendMessage(content); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (
{/* Message List */}
{messages.length === 0 ? (

{t.chat.startConversation}

{t.chat.startDescription}

) : ( messages.map((message, index) => ( editMessage(message.id, newContent)} onRegenerate={() => regenerateFrom(message.id)} disabled={isStreaming} /> )) )}
{/* Input Area */}