diff --git a/.claude/ux-guidelines.md b/.claude/ux-guidelines.md index 72644af..c1dc2cf 100644 --- a/.claude/ux-guidelines.md +++ b/.claude/ux-guidelines.md @@ -422,3 +422,15 @@ The active conversation has a stable spatial home. Question, Response, and Histo Supporting artefacts should remain spatially stable while the conversation grows. Desktop width should be used to preserve context, not merely enlarge cards. Text should not be truncated when sufficient readable space exists. Mobile remains a natural stacked flow with no horizontal split. + +## Facilitator Translation Layer (Experiment 11 — Emerging) + +The reasoning engine produces a rich graph with structured concepts (observations, unknowns, assumptions, relationships, metrics, states). The UI should increasingly become a translation layer over this graph rather than maintaining separate duplicated summaries. + +For end users, present the same data as: + +- **Known** — resolved nodes and established observations +- **Still investigating** — unresolved unknowns and assumptions to validate +- **Quiet reasoning summary** — raw counts (nodes, edges, etc.) visually secondary + +Internal graph concepts should remain available for developers (Developer Details) but should not dominate the primary view. The panel should feel like a facilitator's notebook: someone looking at it should immediately understand where the investigation stands, what has been learned, and what remains uncertain — without needing to understand graph theory. diff --git a/components/investigation-summary-panel-v2.jsx b/components/investigation-summary-panel-v2.jsx new file mode 100644 index 0000000..1a87d5f --- /dev/null +++ b/components/investigation-summary-panel-v2.jsx @@ -0,0 +1,257 @@ +/** + * 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. + */ + +/* ── 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
+