import React, { useState, useRef, useEffect } from 'react'; import { Paperclip, Send, MessageSquare, RefreshCw } from 'lucide-react'; import { motion } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { ChatItem } from '../types'; const Chat: React.FC = () => { const { t } = useTranslation(); const { q, sessions } = useAppContext(); const [chat, setChat] = useState([]); const [msg, setMsg] = useState(''); const [fileSelected, setFileSelected] = useState(false); const [sessionKey, setSessionKey] = useState('main'); const chatEndRef = useRef(null); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chat]); const loadHistory = async () => { try { const qs = q ? `${q}&session=${encodeURIComponent(sessionKey)}` : `?session=${encodeURIComponent(sessionKey)}`; const r = await fetch(`/webui/api/chat/history${qs}`); if (!r.ok) return; const j = await r.json(); const arr = Array.isArray(j.messages) ? j.messages : []; const mapped: ChatItem[] = arr.map((m: any) => { const baseRole = String(m.role || 'assistant'); let role: ChatItem['role'] = 'assistant'; if (baseRole === 'user') role = 'user'; else if (baseRole === 'tool') role = 'tool'; else if (baseRole === 'system') role = 'system'; let text = m.content || ''; let label = role === 'user' ? 'User' : role === 'tool' ? 'Exec' : role === 'system' ? 'System' : 'Agent'; if (Array.isArray(m.tool_calls) && m.tool_calls.length > 0) { role = 'exec'; label = 'Exec'; text = `${text}\n[tool calls: ${m.tool_calls.map((x: any) => x?.function?.name || x?.name).filter(Boolean).join(', ')}]`; } if (baseRole === 'tool') { text = `[tool output]\n${text}`; } return { role, text, label }; }); setChat(mapped); } catch (e) { console.error(e); } }; async function send() { if (!msg.trim() && !fileSelected) return; let media = ''; const input = document.getElementById('file') as HTMLInputElement | null; const f = input?.files?.[0]; if (f) { const fd = new FormData(); fd.append('file', f); try { const ur = await fetch(`/webui/api/upload${q}`, { method: 'POST', body: fd }); const uj = await ur.json(); media = uj.path || ''; } catch (e) { console.error('L0053', e); } } const userText = msg + (media ? `\n[Attached File: ${f?.name}]` : ''); setChat((prev) => [...prev, { role: 'user', text: userText, label: 'User' }]); const currentMsg = msg; setMsg(''); setFileSelected(false); if (input) input.value = ''; try { const response = await fetch(`/webui/api/chat/stream${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionKey, message: currentMsg, media }), }); if (!response.ok || !response.body) throw new Error('Chat request failed'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let assistantText = ''; setChat((prev) => [...prev, { role: 'assistant', text: '', label: 'Agent' }]); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); assistantText += chunk; setChat((prev) => { const next = [...prev]; next[next.length - 1] = { role: 'assistant', text: assistantText, label: 'Agent' }; return next; }); } // refresh full persisted history (includes tool/internal traces) loadHistory(); } catch (e) { setChat((prev) => [...prev, { role: 'system', text: 'Error: Failed to get response from server.', label: 'System' }]); } } useEffect(() => { loadHistory(); }, [q, sessionKey]); useEffect(() => { if (!sessions || sessions.length === 0) return; if (!sessions.some(s => s.key === sessionKey)) { setSessionKey(sessions[0].key); } }, [sessions]); return (

Session

{chat.length === 0 ? (

{t('startConversation')}

) : ( chat.map((m, i) => { const isUser = m.role === 'user'; const isExec = m.role === 'tool' || m.role === 'exec'; const isSystem = m.role === 'system'; const avatar = isUser ? 'U' : isExec ? 'E' : isSystem ? 'S' : 'A'; const avatarClass = isUser ? 'bg-indigo-600/90 text-white' : isExec ? 'bg-amber-600/80 text-white' : isSystem ? 'bg-zinc-700 text-zinc-100' : 'bg-emerald-600/80 text-white'; const bubbleClass = isUser ? 'bg-indigo-600 text-white rounded-br-sm' : isExec ? 'bg-amber-500/10 text-amber-100 rounded-bl-sm border border-amber-500/30' : isSystem ? 'bg-zinc-700/40 text-zinc-100 rounded-bl-sm border border-zinc-600/40' : 'bg-zinc-800/80 text-zinc-200 rounded-bl-sm border border-zinc-700/50'; return (
{avatar}
{m.label || (isUser ? 'User' : isExec ? 'Exec' : isSystem ? 'System' : 'Agent')}

{m.text}

); }) )}
setFileSelected(!!e.target.files?.[0])} /> setMsg(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && send()} placeholder={t('typeMessage')} className="w-full bg-zinc-900 border border-zinc-800 rounded-full pl-14 pr-14 py-3.5 text-[15px] focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all placeholder:text-zinc-500 shadow-sm" />
); }; export default Chat;