diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index 68b0c546..317394f5 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -10,6 +10,7 @@ import { CopilotChat } from "@copilotkit/react-ui"; import { useCallback, useEffect, useState } from "react"; import "@copilotkit/react-ui/styles.css"; import CitationBlock from "./CitationBlock"; +import ContextPanel from "./ContextPanel"; import DbInfo from "./DbInfo"; import SettingsPanel, { STORAGE_KEY } from "./SettingsPanel"; @@ -34,11 +35,17 @@ interface QAResponse { citations: Citation[]; } +interface SessionContext { + summary: string; + last_updated: string | null; +} + interface ChatSessionState { session_id: string; citations: Citation[]; qa_history: QAResponse[]; background_context: string | null; + session_context: SessionContext | null; } // AG-UI state is namespaced under AGUI_STATE_KEY @@ -152,6 +159,31 @@ function SettingsIcon() { ); } +function BrainIcon() { + return ( + + + + + + + + + + + + ); +} + function ToolCallIndicator({ toolName, status, @@ -365,6 +397,7 @@ function ChatContentInner({ setBackgroundContext: (value: string) => void; }) { const [settingsOpen, setSettingsOpen] = useState(false); + const [contextOpen, setContextOpen] = useState(false); const handleSaveContext = useCallback( (value: string) => { @@ -378,7 +411,7 @@ function ChatContentInner({ [setBackgroundContext], ); - useCoAgent({ + const { state: agentState } = useCoAgent({ name: "chat_agent", initialState: { [AGUI_STATE_KEY]: { @@ -386,10 +419,14 @@ function ChatContentInner({ citations: [], qa_history: [], background_context: backgroundContext || null, + session_context: null, }, }, }); + // Extract session context from agent state + const sessionContext = agentState?.[AGUI_STATE_KEY]?.session_context ?? null; + useCoAgentStateRender({ name: "chat_agent", render: ({ state }) => { @@ -470,11 +507,12 @@ function ChatContentInner({ .chat-header { display: flex; justify-content: flex-end; + gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e2e8f0; background: #f8fafc; } - .settings-btn { + .header-btn { display: flex; align-items: center; gap: 0.375rem; @@ -487,17 +525,17 @@ function ChatContentInner({ font-size: 0.8125rem; transition: all 0.15s; } - .settings-btn:hover { + .header-btn:hover { background: #f1f5f9; border-color: #cbd5e1; color: #475569; } - .settings-btn.has-context { + .header-btn.has-content { background: #eff6ff; border-color: #bfdbfe; color: #2563eb; } - .settings-btn.has-context:hover { + .header-btn.has-content:hover { background: #dbeafe; border-color: #93c5fd; } @@ -517,7 +555,20 @@ function ChatContentInner({
+
@@ -547,6 +598,11 @@ function ChatContentInner({ onSave={handleSaveContext} currentValue={backgroundContext} /> + setContextOpen(false)} + sessionContext={sessionContext} + /> ); } diff --git a/app/frontend/components/ContextPanel.tsx b/app/frontend/components/ContextPanel.tsx new file mode 100644 index 00000000..3ffcc789 --- /dev/null +++ b/app/frontend/components/ContextPanel.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useCallback, useId } from "react"; + +interface SessionContext { + summary: string; + last_updated: string | null; +} + +interface ContextPanelProps { + isOpen: boolean; + onClose: () => void; + sessionContext: SessionContext | null; +} + +function formatRelativeTime(isoString: string): string { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + + if (diffSec < 60) { + return "just now"; + } + if (diffMin < 60) { + return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`; + } + if (diffHour < 24) { + return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`; + } + return date.toLocaleDateString(); +} + +function BrainIcon() { + return ( + + + + + + + + + + + + ); +} + +export default function ContextPanel({ + isOpen, + onClose, + sessionContext, +}: ContextPanelProps) { + const titleId = useId(); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }, + [onClose], + ); + + if (!isOpen) { + return null; + } + + const hasContext = sessionContext?.summary && sessionContext.summary.trim(); + + return ( + <> + +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: modal content wrapper */} +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > +
+
+ +
+

+ Session Context +

+
+

+ This is what the assistant has learned from your conversation so + far. It uses this context to provide more relevant answers. +

+ {hasContext ? ( +
{sessionContext.summary}
+ ) : ( +
+
+ +
+
+ No context yet. Ask some questions to build context. +
+
+ )} +
+ + {sessionContext?.last_updated + ? `Last updated: ${formatRelativeTime(sessionContext.last_updated)}` + : ""} + + +
+
+
+ + ); +}