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 { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { MessageBubble } from "@/components/chat/ChatMessage"; import { ToolCallBubble } from "@/components/chat/ToolCallBubble"; import { ChatStartingState } from "@/components/chat/ChatStartingState"; import type { ChatMessage } from "@/types/chat"; import { getEventIdsFromSearchObjectsToolCalls, streamChatCompletion, } from "@/utils/chatUtil"; 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 submitConversation = useCallback( async (messagesToSend: ChatMessage[]) => { if (isLoading) return; const last = messagesToSend[messagesToSend.length - 1]; if (!last || last.role !== "user" || !last.content.trim()) return; setError(null); const assistantPlaceholder: ChatMessage = { role: "assistant", content: "", toolCalls: undefined, }; setMessages([...messagesToSend, assistantPlaceholder]); setIsLoading(true); const apiMessages = messagesToSend.map((m) => ({ role: m.role, content: m.content, })); const baseURL = axios.defaults.baseURL ?? ""; const url = `${baseURL}chat/completion`; const headers: Record = { "Content-Type": "application/json", ...(axios.defaults.headers.common as Record), }; await streamChatCompletion(url, headers, apiMessages, { updateMessages: (updater) => setMessages(updater), onError: (message) => setError(message), onDone: () => setIsLoading(false), defaultErrorMessage: t("error"), }); }, [isLoading, t], ); const sendMessage = useCallback(() => { const text = input.trim(); if (!text || isLoading) return; setInput(""); submitConversation([...messages, { role: "user", content: text }]); }, [input, isLoading, messages, submitConversation]); const handleEditSubmit = useCallback( (messageIndex: number, newContent: string) => { const newList: ChatMessage[] = [ ...messages.slice(0, messageIndex), { role: "user", content: newContent }, ]; submitConversation(newList); }, [messages, submitConversation], ); return (
{messages.length === 0 ? ( { setInput(""); submitConversation([{ role: "user", content: message }]); }} /> ) : (
{messages.map((msg, i) => { const isStreamingPlaceholder = i === messages.length - 1 && msg.role === "assistant" && isLoading && !msg.content?.trim() && !(msg.toolCalls && msg.toolCalls.length > 0); if (isStreamingPlaceholder) { return
; } return (
{msg.role === "assistant" && msg.toolCalls && ( <> {msg.toolCalls.map((tc, tcIdx) => (
{tc.response && ( )}
))} )} {msg.role === "assistant" && (() => { const isComplete = !isLoading || i < messages.length - 1; if (!isComplete) return null; const events = getEventIdsFromSearchObjectsToolCalls( msg.toolCalls, ); return ; })()}
); })} {(() => { const lastMsg = messages[messages.length - 1]; const showProcessing = isLoading && lastMsg?.role === "assistant" && !lastMsg.content?.trim() && !(lastMsg.toolCalls && lastMsg.toolCalls.length > 0); return showProcessing ? (
{t("processing")}
) : null; })()} {error && (

{error}

)}
)} {messages.length > 0 && ( )}
); } 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} aria-busy={isLoading} />
); }