"use client"; import { CopilotKit, useCoAgent, useCoAgentStateRender, useCopilotAction, } from "@copilotkit/react-core"; import { CopilotChat } from "@copilotkit/react-ui"; import { useState } from "react"; import "@copilotkit/react-ui/styles.css"; import CitationBlock from "./CitationBlock"; import ContextPanel from "./ContextPanel"; import DbInfo from "./DbInfo"; import DocumentFilter from "./DocumentFilter"; // Must match AGUI_STATE_KEY from haiku.rag.agents.chat const AGUI_STATE_KEY = "haiku.rag.chat"; interface Citation { index: number; document_id: string; chunk_id: string; document_uri: string; document_title: string | null; page_numbers: number[]; headings: string[] | null; content: string; } interface QAResponse { question: string; answer: string; confidence: number; citations: Citation[]; } interface SessionContext { summary: string; last_updated: string | null; } interface ChatSessionState { session_id: string; initial_context: string | null; citations: Citation[]; qa_history: QAResponse[]; session_context: SessionContext | null; document_filter: string[]; citation_registry: Record; } // AG-UI state is namespaced under AGUI_STATE_KEY interface AgentState { [AGUI_STATE_KEY]?: ChatSessionState; } function SpinnerIcon() { return ( ); } function CheckIcon() { return ( ); } function SearchIcon() { return ( ); } function MessageIcon() { return ( ); } function FileIcon() { return ( ); } function BrainIcon() { return ( ); } function ToolCallIndicator({ toolName, status, args, }: { toolName: string; status: string; args: Record; }) { const isComplete = status === "complete"; const getToolIcon = () => { switch (toolName) { case "search": return ; case "ask": return ; case "get_document": return ; default: return ; } }; const getToolLabel = () => { switch (toolName) { case "search": return "Search"; case "ask": return "Ask"; case "get_document": return "Document"; default: return toolName; } }; const getDescription = () => { switch (toolName) { case "search": { const query = args.query as string; const docName = args.document_name as string | undefined; return ( <> {query} {docName && ( {" "} in {docName} )} ); } case "ask": { const question = args.question as string; const docName = args.document_name as string | undefined; return ( <> {question} {docName && ( {" "} from {docName} )} ); } case "get_document": return {args.query as string}; default: return Processing...; } }; return (
{isComplete ? : }
{getToolIcon()} {getToolLabel()} {isComplete ? "Done" : "Working..."}
{getDescription()}
); } function FilterIcon() { return ( ); } function ChatContentInner() { const [contextOpen, setContextOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false); const { state: agentState, setState: setAgentState } = useCoAgent( { name: "chat_agent", initialState: { [AGUI_STATE_KEY]: { session_id: "", initial_context: null, citations: [], qa_history: [], session_context: null, document_filter: [], citation_registry: {}, }, }, }, ); const normalizeChatState = ( state: ChatSessionState | undefined, ): ChatSessionState => ({ session_id: state?.session_id ?? "", initial_context: state?.initial_context ?? null, citations: state?.citations ?? [], qa_history: state?.qa_history ?? [], session_context: state?.session_context ?? null, document_filter: state?.document_filter ?? [], citation_registry: state?.citation_registry ?? {}, }); const mergeChatState = (partial: Partial) => { const current = normalizeChatState(agentState?.[AGUI_STATE_KEY]); setAgentState({ ...agentState, [AGUI_STATE_KEY]: { ...current, ...partial, }, }); }; // Extract session context, document filter, and initial context from agent state const sessionContext = agentState?.[AGUI_STATE_KEY]?.session_context ?? null; const documentFilter = agentState?.[AGUI_STATE_KEY]?.document_filter ?? []; const initialContext = agentState?.[AGUI_STATE_KEY]?.initial_context ?? ""; // Context is locked after first message (qa_history has entries) const isContextLocked = (agentState?.[AGUI_STATE_KEY]?.qa_history?.length ?? 0) > 0; const handleFilterApply = (selected: string[]) => { mergeChatState({ document_filter: selected }); }; const handleInitialContextChange = (value: string) => { if (isContextLocked) return; mergeChatState({ initial_context: value || null }); }; useCoAgentStateRender({ name: "chat_agent", render: ({ state }) => { const chatState = state[AGUI_STATE_KEY]; if (chatState?.citations.length) { return ; } return null; }, }); useCopilotAction({ name: "search", available: "disabled", parameters: [ { name: "query", type: "string" }, { name: "document_name", type: "string" }, ], render: ({ status, args }) => ( } /> ), }); useCopilotAction({ name: "ask", available: "disabled", parameters: [ { name: "question", type: "string" }, { name: "document_name", type: "string" }, ], render: ({ status, args }) => ( } /> ), }); useCopilotAction({ name: "get_document", available: "disabled", parameters: [{ name: "query", type: "string" }], render: ({ status, args }) => ( } /> ), }); return ( <>
setContextOpen(false)} sessionContext={sessionContext} initialContext={initialContext} onInitialContextChange={handleInitialContextChange} isLocked={isContextLocked} /> setFilterOpen(false)} selected={documentFilter} onApply={handleFilterApply} /> ); } function ChatContent() { return ; } export default function Chat() { return ( ); }