From 09e6324add539c33bbff7287b5698eeb40a80cb4 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 20 Feb 2026 11:03:53 +0200 Subject: [PATCH] Adapt frontend --- app/frontend/components/Chat.tsx | 165 +++++++++-------------- app/frontend/components/ContextPanel.tsx | 131 ------------------ app/frontend/lib/sessionStorage.ts | 57 +++++--- 3 files changed, 95 insertions(+), 258 deletions(-) delete mode 100644 app/frontend/components/ContextPanel.tsx diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index 3401fc52..b9a03257 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -17,27 +17,27 @@ import { useMemo, useState, } from "react"; -import { BrainIcon, FilterIcon } from "../lib/icons"; -import type { ChatSessionState } from "../lib/sessionStorage"; +import { FilterIcon } from "../lib/icons"; +import type { RAGState } from "../lib/sessionStorage"; import { createSession, + deriveCitationsHistory, getActiveSessionId, getSession, - normalizeChatState, + normalizeRAGState, updateSessionMessages, } from "../lib/sessionStorage"; import CitationBlock from "./CitationBlock"; -import ContextPanel from "./ContextPanel"; import DbInfo from "./DbInfo"; import DocumentFilter from "./DocumentFilter"; import SessionManager from "./SessionManager"; -// Must match AGUI_STATE_KEY from haiku.rag.agents.chat -const AGUI_STATE_KEY = "haiku.rag.chat"; +// Must match state_namespace from haiku.rag.skills.rag +const AGUI_STATE_KEY = "rag"; // AG-UI state is namespaced under AGUI_STATE_KEY interface AgentState { - [AGUI_STATE_KEY]?: ChatSessionState; + [AGUI_STATE_KEY]?: RAGState; } // biome-ignore lint/suspicious/noExplicitAny: CopilotKit message objects vary at runtime @@ -165,6 +165,10 @@ function ToolCallIndicator({ return "Ask"; case "get_document": return "Document"; + case "analyze": + return "Analyze"; + case "research": + return "Research"; default: return toolName; } @@ -174,36 +178,18 @@ function ToolCallIndicator({ switch (toolName) { case "search": { const query = args.query as string; - const docName = args.document_name as string | undefined; - return ( - <> - {query} - {docName && ( - - {" "} - in {docName} - - )} - - ); + return {query}; } case "ask": { const question = args.question as string; - const docName = args.document_name as string | undefined; - return ( - <> - {question} - {docName && ( - - {" "} - from {docName} - - )} - - ); + return {question}; } case "get_document": return {args.query as string}; + case "analyze": + return {args.question as string}; + case "research": + return {args.question as string}; default: return Processing...; } @@ -231,7 +217,7 @@ function ToolCallIndicator({ } // Context for sharing chat state with the message view -const ChatStateContext = createContext(null); +const ChatStateContext = createContext(null); // Wildcard tool call renderer for all server-side tools const toolCallRenderers = [ @@ -258,7 +244,8 @@ function MessageViewWithCitations({ messages: any[]; isRunning: boolean; }) { - const chatState = useContext(ChatStateContext); + const ragState = useContext(ChatStateContext); + const citationsHistory = ragState ? deriveCitationsHistory(ragState) : []; const cursor = isRunning ? (
@@ -271,7 +258,7 @@ function MessageViewWithCitations({ return ( {({ messageElements }) => { - if (!chatState?.citations_history?.length) { + if (!citationsHistory.length) { return ( <> {messageElements} @@ -284,9 +271,9 @@ function MessageViewWithCitations({ // message (tool messages produce nothing). We correlate elements with // messages to inject CitationBlocks after the right assistant responses. // - // Both search and ask tools append to citations_history in order, + // Both search and ask tools append to citations via qa_history, // so after each assistant text response that followed tool calls, - // we inject the next citations_history entry. + // we inject the next citations entry. const result: React.ReactNode[] = []; let citIdx = 0; let seenToolCalls = false; @@ -317,10 +304,10 @@ function MessageViewWithCitations({ } // After an assistant text response that followed tool calls, - // inject the next citations_history entry (one per turn) + // inject the next citations entry (one per turn) if (msg.role === "assistant" && msg.content && seenToolCalls) { - if (citIdx < chatState.citations_history.length) { - const citations = chatState.citations_history[citIdx]; + if (citIdx < citationsHistory.length) { + const citations = citationsHistory[citIdx]; if (citations?.length) { result.push( void; }) { - const [contextOpen, setContextOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false); + // Track selected document names locally (frontend-only) + const [selectedDocuments, setSelectedDocuments] = useState([]); const { agent } = useAgent({ agentId: "chat_agent", @@ -376,23 +364,10 @@ function ChatContentInner({ agent.threadId = sessionId; }, [agent, sessionId]); - const chatState = normalizeChatState( + const ragState = normalizeRAGState( (agent.state as AgentState)?.[AGUI_STATE_KEY], ); - const mergeChatState = (partial: Partial) => { - const current = normalizeChatState( - (agent.state as AgentState)?.[AGUI_STATE_KEY], - ); - agent.setState({ - ...agent.state, - [AGUI_STATE_KEY]: { - ...current, - ...partial, - }, - }); - }; - // Restore session from localStorage when agent reference changes. // useAgent returns a provisional agent initially, then the real agent // after runtime connects — re-run restore each time so messages stick. @@ -400,9 +375,9 @@ function ChatContentInner({ if (agent.messages.length > 0) return; const session = getSession(sessionId); if (!session) return; - if (session.chatState) { + if (session.ragState) { agent.setState({ - [AGUI_STATE_KEY]: normalizeChatState(session.chatState), + [AGUI_STATE_KEY]: normalizeRAGState(session.ragState), }); } if (session.messages.length > 0) { @@ -412,21 +387,21 @@ function ChatContentInner({ }, [agent, sessionId]); // Persist messages and state to localStorage. - // Read chatState from agent.state at effect time (not render time) so that + // Read ragState from agent.state at effect time (not render time) so that // restore and persist effects in the same commit see consistent state. // biome-ignore lint/correctness/useExhaustiveDependencies: JSON.stringify tracks content changes useEffect(() => { if (sessionId && agent.messages.length > 0) { - const currentChatState = normalizeChatState( + const currentRagState = normalizeRAGState( (agent.state as AgentState)?.[AGUI_STATE_KEY], ); updateSessionMessages( sessionId, serializeMessages(agent.messages), - currentChatState, + currentRagState, ); } - }, [JSON.stringify(agent.messages), chatState, sessionId]); + }, [JSON.stringify(agent.messages), ragState, sessionId]); // biome-ignore lint/correctness/useExhaustiveDependencies: stable identity via agent ref const messages = useMemo( @@ -458,24 +433,29 @@ function ChatContentInner({ } }, [agent, ck]); - const sessionContext = chatState.session_context; - const documentFilter = chatState.document_filter; - const initialContext = chatState.initial_context ?? ""; - - // Context is locked after first message (qa_history has entries) - const isContextLocked = (chatState.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 }); + setSelectedDocuments(selected); + // Convert selected document names to SQL filter for the backend + const filter = + selected.length > 0 + ? selected + .map( + (name) => + `(title LIKE '%${name.replace(/'/g, "''")}%' OR uri LIKE '%${name.replace(/'/g, "''")}%')`, + ) + .join(" OR ") + : null; + agent.setState({ + ...agent.state, + [AGUI_STATE_KEY]: { + ...ragState, + document_filter: filter, + }, + }); }; return ( - +
@@ -485,36 +465,19 @@ function ChatContentInner({ /> -
- setContextOpen(false)} - sessionContext={sessionContext} - initialContext={initialContext} - onInitialContextChange={handleInitialContextChange} - isLocked={isContextLocked} - /> setFilterOpen(false)} - selected={documentFilter} + selected={selectedDocuments} onApply={handleFilterApply} /> diff --git a/app/frontend/components/ContextPanel.tsx b/app/frontend/components/ContextPanel.tsx deleted file mode 100644 index 69a8f7c7..00000000 --- a/app/frontend/components/ContextPanel.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useId, useState } from "react"; -import { formatRelativeTime } from "../lib/format"; -import { BrainIcon } from "../lib/icons"; -import type { SessionContext } from "../lib/sessionStorage"; - -interface ContextPanelProps { - isOpen: boolean; - onClose: () => void; - sessionContext: SessionContext | null; - initialContext?: string; - onInitialContextChange?: (value: string) => void; - isLocked?: boolean; -} - -export default function ContextPanel({ - isOpen, - onClose, - sessionContext, - initialContext = "", - onInitialContextChange, - isLocked = false, -}: ContextPanelProps) { - const titleId = useId(); - const [localValue, setLocalValue] = useState(initialContext); - - useEffect(() => { - if (isOpen) { - setLocalValue(initialContext); - } - }, [isOpen, initialContext]); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - onClose(); - } - }, - [onClose], - ); - - const handleSave = useCallback(() => { - onInitialContextChange?.(localValue); - onClose(); - }, [localValue, onInitialContextChange, onClose]); - - if (!isOpen) { - return null; - } - - const hasSessionContext = sessionContext?.summary?.trim(); - // Show edit mode when: not locked AND no session context yet - const isEditMode = !isLocked && !hasSessionContext; - - return ( -
- {/* biome-ignore lint/a11y/noStaticElementInteractions: modal content wrapper */} -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > -
-
- -
-

- {isEditMode ? "Initial Context" : "Session Context"} -

-
-

- {isEditMode - ? "Set background context to guide the conversation. This will be locked after you send your first message." - : "This is what the assistant has learned from your conversation so far. It uses this context to provide more relevant answers."} -

- {isEditMode ? ( -