import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { FaArrowUpLong } from "react-icons/fa6"; import { useTranslation } from "react-i18next"; import { useState, useCallback } from "react"; import axios from "axios"; import { MessageBubble } from "@/components/chat/ChatMessage"; import { ToolCallBubble } from "@/components/chat/ToolCallBubble"; import type { ChatMessage, ToolCall } from "@/types/chat"; export default function ChatPage() { const { t } = useTranslation(["views/chat"]); const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const sendMessage = useCallback(async () => { const text = input.trim(); if (!text || isLoading) return; const userMessage: ChatMessage = { role: "user", content: text }; setInput(""); setError(null); setMessages((prev) => [...prev, userMessage]); setIsLoading(true); const apiMessages = [...messages, userMessage].map((m) => ({ role: m.role, content: m.content, })); try { const baseURL = axios.defaults.baseURL ?? ""; const url = `${baseURL}chat/completion`; const headers: Record = { "Content-Type": "application/json", ...(axios.defaults.headers.common as Record), }; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ messages: apiMessages, stream: true }), }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error( (errBody as { error?: string }).error ?? res.statusText, ); } const reader = res.body?.getReader(); const decoder = new TextDecoder(); if (!reader) throw new Error("No response body"); const assistantMessage: ChatMessage = { role: "assistant", content: "", toolCalls: undefined, }; setMessages((prev) => [...prev, assistantMessage]); let buffer = ""; let hadStreamError = false; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; let data: { type: string; tool_calls?: ToolCall[]; delta?: string }; try { data = JSON.parse(trimmed) as { type: string; tool_calls?: ToolCall[]; delta?: string; }; } catch { continue; } if (data.type === "error" && "error" in data) { setError((data as { error?: string }).error ?? t("error")); setMessages((prev) => prev.filter((m) => !(m.role === "assistant" && m.content === "")), ); hadStreamError = true; break; } if (data.type === "tool_calls" && data.tool_calls?.length) { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, toolCalls: data.tool_calls, }; return next; }); } else if (data.type === "content" && data.delta !== undefined) { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, content: last.content + data.delta, }; return next; }); } } if (hadStreamError) break; } if (hadStreamError) { // already set error and cleaned up } else if (buffer.trim()) { try { const data = JSON.parse(buffer.trim()) as { type: string; tool_calls?: ToolCall[]; delta?: string; }; if (data.type === "content" && data.delta !== undefined) { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant") next[next.length - 1] = { ...last, content: last.content + data.delta, }; return next; }); } } catch { // ignore final malformed chunk } } if (!hadStreamError) { setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last?.role === "assistant" && last.content === "") next[next.length - 1] = { ...last, content: " " }; return next; }); } } catch { setError(t("error")); setMessages((prev) => prev.filter((m) => !(m.role === "assistant" && m.content === "")), ); } finally { setIsLoading(false); } }, [input, isLoading, messages, t]); return (
{messages.map((msg, i) => (
{msg.role === "assistant" && msg.toolCalls && ( <> {msg.toolCalls.map((tc, tcIdx) => (
{tc.response && ( )}
))} )}
))} {isLoading && (
{t("processing")}
)} {error && (

{error}

)}
); } type ChatEntryProps = { input: string; setInput: (value: string) => void; sendMessage: () => void; isLoading: boolean; placeholder: string; }; function ChatEntry({ input, setInput, sendMessage, isLoading, placeholder, }: ChatEntryProps) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; return (
setInput(e.target.value)} onKeyDown={handleKeyDown} disabled={isLoading} />
); }