/** * InvestigationSummaryPanel — Phase 4 * Displays key investigation metrics in a compact card. * Some fields are currently mocked; TODO comments identify what the * reasoning engine must eventually provide. */ /* ── Helpers ──────────────────────────────────────────────── */ import React from "react"; function formatTimestamp(iso) { if (!iso) return "—"; try { const d = new Date(iso); if (isNaN(d)) return iso; const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } catch { return iso; } } function humaniseDuration(seconds) { if (!seconds || seconds < 0) return "—"; const mins = Math.floor(seconds / 60); const secs = seconds % 60; if (mins === 0) return `${secs}s`; return `${mins}m ${secs}s`; } /* ── Component ────────────────────────────────────────────── */ function InvestigationSummaryPanel({ graph, selectedQuestion, result, updateStatus }) { // ── Current status ──────────────────────────────────────────── // TODO: reasoning should emit an explicit status field such as // "investigating", "evidence_limit_reached", "resolution_achieved". // Currently derived heuristically from graph state. const isInvestigating = Boolean(selectedQuestion); const hasGraph = Boolean(graph); let currentStatus; if (updateStatus === "loading") { currentStatus = { label: "Reasoning", level: "investigating" }; } else if (!hasGraph) { currentStatus = { label: "Not started", level: "idle" }; } else if (isInvestigating) { currentStatus = { label: "Investigation in progress", level: "investigating" }; } else if (graph.resolvedNodeIds?.length > 0 && graph.nodes) { const unresolvedUnknowns = graph.nodes.filter( (n) => n.kind === "unknown" && !graph.resolvedNodeIds.includes(n.id) ); if (unresolvedUnknowns.length === 0) { currentStatus = { label: "Investigation complete", level: "complete" }; } else { // TODO: reasoning should emit a terminal "evidence_limit_reached" // status when it stops selecting questions because no unknown has // sufficient upstream evidence. Currently we infer this from the // absence of an active question combined with unresolved unknowns. currentStatus = { label: "Current evidence limit reached", level: "limit" }; } } else { currentStatus = { label: "Analysis complete", level: "complete" }; } const statusColors = { idle: { border: "border-gray-200/60", bg: "bg-gray-50/40", text: "text-gray-400" }, investigating: { border: "border-blue-200/60", bg: "bg-blue-50/30", text: "text-blue-600" }, complete: { border: "border-green-200/60", bg: "bg-green-50/30", text: "text-green-600" }, limit: { border: "border-gray-200", bg: "bg-gray-50/40", text: "text-gray-400" }, }; const colors = statusColors[currentStatus.level] || statusColors.idle; // ── Current understanding ──────────────────────────────── // TODO: reasoning should provide a durable summary field that is // guaranteed to be the latest plain-language synthesis. // Currently falls back to graph.currentSummary which may not exist // in all mock scenarios. const currentUnderstanding = result?.summary || result?.updatedSituationGraph?.currentSummary || graph?.currentSummary || null; // ── Questions answered / remaining ─────────────────────── // TODO: reasoning should emit a list of resolved unknown node IDs // and the total set of unknown nodes it identified at start. // Currently we count from the graph snapshot: every unknown whose // status is "resolved" (or whose ID appears in resolvedNodeIds). let questionsAnswered = 0; let questionsRemaining = 0; if (graph?.nodes) { const allUnknowns = graph.nodes.filter((n) => n.kind === "unknown"); const resolvedCount = allUnknowns.filter( (n) => n.status === "resolved" || (graph.resolvedNodeIds && graph.resolvedNodeIds.includes(n.id)) ).length; questionsAnswered = resolvedCount; // TODO: this is a rough heuristic — the reasoning engine should // explicitly track which unknowns were proposed for questioning. questionsRemaining = allUnknowns.length - resolvedCount; } // ── Timestamps ─────────────────────────────────────────── // TODO: reasoning should provide investigationStartedAt and // lastUpdatedAt as part of the start/update contract. // Currently we use the session updatedAt timestamp (persisted by // the UI layer) as a best-effort approximation. const investigationStartTime = result?.updatedAt || null; const lastUpdatedAt = result?.updatedAt || null; // Derive elapsed time since last update let elapsedSeconds = 0; if (lastUpdatedAt) { elapsedSeconds = Math.floor((Date.now() - new Date(lastUpdatedAt).getTime()) / 1000); } return (
{/* Status */}
{currentStatus.label}
{/* Current understanding */} {currentUnderstanding && (

What we understand so far

{currentUnderstanding}

)} {/* Questions — hidden when no meaningful value to show */} {isInvestigating && questionsRemaining > 0 && (
Questions answered {questionsAnswered}
Still working on {questionsRemaining + " items"}
)}
); } export default InvestigationSummaryPanel;