/** * InvestigationSummaryPanelV2 — Phase 4, Experiment 11 * A facilitator-style progress panel that translates the reasoning graph * into a human-friendly "what is known / what remains" view. * * Design principle: * The UI should progressively become a translation layer over the * reasoning graph rather than maintaining separate duplicated summaries. * Internal graph concepts remain available for developers, while end * users see a facilitator-style explanation of what is currently understood * and what remains uncertain. * * This component uses exactly the same graph data as InvestigationSummaryPanel * (Version A). No new backend fields or API contracts are required. */ import React from "react"; /* ── Helpers ──────────────────────────────────────────────── */ 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`; } /* ── Data extraction helpers ─────────────────────────────── */ /** * Classify nodes into "known" (resolved / observations with values) and * "still investigating" (unresolved unknowns and assumptions needing validation). */ function classifyNodes(graph, resolvedIds) { if (!graph?.nodes) return { known: [], stillInvestigating: [] }; const resolved = new Set(resolvedIds || []); const known = []; const stillInvestigating = []; for (const node of graph.nodes) { const isResolved = resolved.has(node.id) || node.status === "resolved"; // Resolved nodes become known facts if (isResolved) { known.push({ label: node.label, description: node.description, kind: node.kind, confidence: node.confidence, }); } else { // Unresolved unknowns and assumptions go into "still investigating" stillInvestigating.push({ label: node.label, description: node.description, kind: node.kind, confidence: node.confidence, }); } } return { known, stillInvestigating }; } /** * Map graph node kinds to end-user-friendly group labels. */ function groupLabelForKind(kind) { const map = { unknown: "Still investigating", assumption: "Assumptions to validate", observation: "Observations", state: "Current states", metric: "Metrics", conclusion: "Conclusions", }; return map[kind] || kind.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } /* ── Rendering helpers ───────────────────────────────────── */ /** * Render a single item from the known or still-investigating lists. * Show only meaningful content — hide labels that duplicate description. */ function renderListItem(item) { // Prefer description if it adds something beyond the label const text = (item.description && item.description !== item.label) ? item.description : item.label; return text; } /* ── Component ────────────────────────────────────────────── */ function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateStatus }) { // ── Status (same derivation logic as Version A) ────────── 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 { 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 (same source as Version A) ──── const currentUnderstanding = result?.summary || result?.updatedSituationGraph?.currentSummary || graph?.currentSummary || null; // ── Classify graph data ───────────────────────────────── const resolvedIds = new Set(graph?.resolvedNodeIds || []); const { known, stillInvestigating } = classifyNodes(graph, resolvedIds); // Group still-investigating items by kind for a cleaner view const investigatingByGroup = {}; for (const item of stillInvestigating) { const key = groupLabelForKind(item.kind); if (!investigatingByGroup[key]) investigatingByGroup[key] = []; investigatingByGroup[key].push(item); } // ── Reasoning summary counts (quiet, at bottom) ───────── const reasonCounts = { observations: graph?.nodes?.filter((n) => n.kind === "observation").length || 0, unknowns: stillInvestigating.filter((n) => n.kind === "unknown").length || 0, assumptions: graph?.nodes?.filter((n) => n.kind === "assumption" && !resolvedIds.has(n.id)).length || 0, relationships: graph?.edges?.length || 0, metrics: graph?.nodes?.filter((n) => n.kind === "metric").length || 0, states: graph?.nodes?.filter((n) => n.kind === "state").length || 0, conclusions: graph?.nodes?.filter((n) => n.kind === "conclusion").length || 0, }; // Only show non-zero counts in the reasoning summary const reasonEntries = Object.entries(reasonCounts).filter(([_, v]) => v > 0); return (
{currentUnderstanding}
{renderListItem(stillInvestigating[0])}
Reasoning