import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router"; import { PanelRightOpen } from "lucide-react"; import { toast } from "sonner"; import KnowledgeManagement from "../KnowledgeManagement"; import { createAnalysisMessage, deleteAnalysis, getAnalysis, getAnalysisMessages, getDatabaseClients, getDocuments, listAnalyses, updateAnalysisDataBind, type Analysis, type AnalysisMessage, type DataBindItem, } from "@/services/orchestrationApi"; import { AgenticApiError, getObservability, streamChat, streamHelp, type AgentStreamEvent } from "@/services/agenticApi"; import { AnalysisHeader } from "./AnalysisHeader"; import { AnalysisSidebar } from "./AnalysisSidebar"; import { AppNavigation, type AppMenuKey } from "./AppNavigation"; import { ChatInput } from "./ChatInput"; import { HomeDashboard } from "./HomeDashboard"; import { MessageList } from "./MessageList"; import { NewAnalysisDialog } from "./NewAnalysisDialog"; import { ReportSidebar } from "./ReportSidebar"; import { clearSession, getCurrentSession } from "./session"; import type { StreamState, UiMessage } from "./types"; import { cx, makeLocalId } from "./utils"; function toUiMessage(message: AnalysisMessage): UiMessage { return { id: message.id, role: message.role, content: message.content, created_at: message.created_at, status: "complete", }; } const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); export function AnalysisShell() { const navigate = useNavigate(); const session = useMemo(() => getCurrentSession(), []); const [activeMenu, setActiveMenu] = useState("home"); const [analyses, setAnalyses] = useState([]); const [activeAnalysis, setActiveAnalysis] = useState(null); const [messages, setMessages] = useState([]); const [loadingAnalyses, setLoadingAnalyses] = useState(true); const [loadingMessages, setLoadingMessages] = useState(false); const [streamState, setStreamState] = useState("idle"); const [newAnalysisOpen, setNewAnalysisOpen] = useState(false); const [reportCollapsed, setReportCollapsed] = useState(false); const [staleSources, setStaleSources] = useState([]); const selectAnalysis = async (analysis: Analysis) => { setActiveAnalysis(analysis); setLoadingMessages(true); try { const [detail, history] = await Promise.all([getAnalysis(analysis.id), getAnalysisMessages(analysis.id)]); setActiveAnalysis(detail); setAnalyses((prev) => prev.map((item) => (item.id === detail.id ? detail : item))); setMessages(history.map(toUiMessage)); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to open analysis"); setMessages([]); } finally { setLoadingMessages(false); } }; const loadAnalyses = async () => { setLoadingAnalyses(true); try { const result = await listAnalyses({ status: "active", page: 1, limit: 50 }); setAnalyses(result.analyses); if (!activeAnalysis && result.analyses.length > 0) { await selectAnalysis(result.analyses[0]); } } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to load analyses"); } finally { setLoadingAnalyses(false); } }; useEffect(() => { loadAnalyses(); }, []); useEffect(() => { const checkStaleSources = async () => { if (!session?.user_id || !activeAnalysis?.data_bind.length) { setStaleSources([]); return; } try { const [documents, databaseClients] = await Promise.all([getDocuments(session.user_id), getDatabaseClients(session.user_id)]); const documentIds = new Set(documents.map((document) => document.id)); const databaseIds = new Set(databaseClients.map((client) => client.id)); setStaleSources( activeAnalysis.data_bind.filter((source) => { if (source.group_type === "document") return !documentIds.has(source.id); if (source.group_type === "database") return !databaseIds.has(source.id); return false; }) ); } catch { setStaleSources([]); } }; checkStaleSources(); }, [activeAnalysis?.id, activeAnalysis?.data_bind_version, activeMenu, session?.user_id]); const fetchTraceability = async (analysisId: string, messageId: string | undefined, uiMessageId: string) => { if (!messageId) return; setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: true, traceabilityError: null } : msg))); for (let attempt = 0; attempt < 3; attempt += 1) { try { const traceability = await getObservability(analysisId, messageId); setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceability, traceabilityLoading: false } : msg))); return; } catch (err) { if (err instanceof AgenticApiError && err.status === 404 && attempt < 2) { await wait(900); continue; } const traceabilityError = err instanceof AgenticApiError && err.status === 404 ? "Traceability is still being prepared. Try again after the next answer." : err instanceof Error ? err.message : "Failed to load traceability"; setMessages((prev) => prev.map((msg) => (msg.id === uiMessageId ? { ...msg, traceabilityLoading: false, traceabilityError } : msg))); return; } } }; const runAgentStream = async (kind: "chat" | "help", prompt?: string) => { if (!session?.user_id || !activeAnalysis || streamState !== "idle") return; const aiId = makeLocalId(kind === "chat" ? "ai" : "help"); let finalContent = ""; let doneMessageId: string | undefined; let streamError: string | null = null; setMessages((prev) => [ ...prev, { id: aiId, role: "ai", content: "", created_at: new Date().toISOString(), status: "streaming", statusText: kind === "chat" ? "Reading analysis context" : "Preparing help", }, ]); setStreamState(kind === "chat" ? "streaming-chat" : "streaming-help"); try { const onEvent = (event: AgentStreamEvent) => { if (event.type === "chunk") { finalContent += event.data; setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, content: finalContent, statusText: undefined } : msg))); } if (event.type === "status") { setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, statusText: event.data } : msg))); } if (event.type === "sources") { setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, sources: event.data } : msg))); } if (event.type === "done") { doneMessageId = typeof event.data.message_id === "string" ? event.data.message_id : undefined; } if (event.type === "error") { streamError = event.data; setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, status: "error", statusText: event.data } : msg))); } }; if (kind === "chat" && prompt) { await streamChat({ user_id: session.user_id, analysis_id: activeAnalysis.id, message: prompt }, onEvent); } else { await streamHelp({ user_id: session.user_id, analysis_id: activeAnalysis.id }, onEvent); } if (streamError) return; let traceTargetId = aiId; setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, status: "complete", messageId: doneMessageId, traceabilityLoading: Boolean(doneMessageId) } : msg)) ); if (finalContent.trim()) { const saved = await createAnalysisMessage(activeAnalysis.id, { role: "ai", content: finalContent.trim() }); traceTargetId = saved.id; setMessages((prev) => prev.map((msg) => msg.id === aiId ? { ...toUiMessage(saved), messageId: doneMessageId, sources: msg.sources, traceabilityLoading: Boolean(doneMessageId) } : msg ) ); } await fetchTraceability(activeAnalysis.id, doneMessageId, traceTargetId); } catch (err) { const message = err instanceof Error ? err.message : "Agent stream failed"; setMessages((prev) => prev.map((msg) => (msg.id === aiId ? { ...msg, status: "error", statusText: message, traceabilityLoading: false } : msg))); } finally { setStreamState("idle"); } }; const handleSend = async (content: string) => { if (!activeAnalysis || !session?.user_id || streamState !== "idle") return; const localId = makeLocalId("user"); setMessages((prev) => [ ...prev, { id: localId, role: "user", content, created_at: new Date().toISOString(), status: "sending" }, ]); try { const saved = await createAnalysisMessage(activeAnalysis.id, { role: "user", content }); setMessages((prev) => prev.map((msg) => (msg.id === localId ? toUiMessage(saved) : msg))); } catch (err) { setMessages((prev) => prev.map((msg) => (msg.id === localId ? { ...msg, status: "error", statusText: "Failed to save message" } : msg))); toast.error(err instanceof Error ? err.message : "Failed to save message"); return; } await runAgentStream("chat", content); }; const handleHelp = async () => { await runAgentStream("help"); }; const handleCreated = (analysis: Analysis) => { setAnalyses((prev) => [analysis, ...prev.filter((item) => item.id !== analysis.id)]); setActiveMenu("analysis-agent"); selectAnalysis(analysis); }; const handleDelete = async (analysis: Analysis) => { if (!window.confirm(`Delete analysis "${analysis.analysis_title}"?`)) return; try { await deleteAnalysis(analysis.id); const next = analyses.filter((item) => item.id !== analysis.id); setAnalyses(next); if (activeAnalysis?.id === analysis.id) { setActiveAnalysis(null); setMessages([]); setStaleSources([]); if (next[0]) selectAnalysis(next[0]); } } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete analysis"); } }; const handleUpdateDataBind = async (items: DataBindItem[]) => { if (!activeAnalysis) return; const updated = await updateAnalysisDataBind(activeAnalysis.id, activeAnalysis.data_bind_version, items); setActiveAnalysis(updated); setAnalyses((prev) => prev.map((item) => (item.id === updated.id ? updated : item))); setStaleSources([]); }; const logout = () => { clearSession(); navigate("/login", { replace: true }); }; const renderAnalysisAgent = () => (
setNewAnalysisOpen(true)} onSelect={selectAnalysis} onDelete={handleDelete} />
{loadingMessages ? (
Loading messages
) : ( )}
{reportCollapsed ? ( ) : ( )}
); return (
{activeMenu === "home" && 0} onNavigate={setActiveMenu} />} {activeMenu === "knowledge" && setActiveMenu("home")} variant="page" />} {activeMenu === "analysis-agent" && renderAnalysisAgent()}
setNewAnalysisOpen(false)} onCreated={handleCreated} />
); }