Files
confidence-engine/components/investigation-summary-panel.jsx
T
robbond 46f2d12726 exp(06): focused investigation — visual hierarchy without layout changes
Emphasise the active investigation card through stronger elevation,
clearer borders, and improved spacing. Quiet supporting panels by
reducing border opacity, softening heading weight, and lowering
text contrast — making them available without competing for attention.

Facilitator card receives a warm surface tint to read as a briefing
card rather than a generic panel.

Documentation: close experiment 05 with findings, add experiment 06
to the design evolution log, add Attention Hierarchy to UX guidelines,
defer dark mode to a future Investigation Mode experiment.

Presentation changes only — no reasoning, prompts, graph, API, or
backend modifications.
2026-08-05 12:41:13 +01:00

153 lines
6.7 KiB
React

/**
* 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 ──────────────────────────────────────────────── */
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 (
<div className={`rounded-lg border ${colors.border} ${colors.bg} p-5 space-y-4`}>
{/* Status */}
<div className="flex items-center gap-2">
<span className={`inline-block h-2.5 w-2.5 rounded-full bg-current ${colors.text}`} />
<span className={`text-sm font-medium ${colors.text}`}>{currentStatus.label}</span>
</div>
{/* Current understanding */}
{currentUnderstanding && (
<div>
<h3 className="mb-1 text-[11px] font-medium tracking-widest uppercase text-gray-400/70">
What we understand so far
</h3>
<p className="text-sm leading-relaxed text-gray-600">{currentUnderstanding}</p>
</div>
)}
{/* Questions — hidden when no meaningful value to show */}
{isInvestigating && questionsRemaining > 0 && (
<div className="grid grid-cols-2 gap-4">
<div>
<span className="block text-xs text-gray-400">Questions answered</span>
<span className={`text-lg font-semibold ${colors.text}`}>{questionsAnswered}</span>
</div>
<div>
<span className="block text-xs text-gray-400">Still working on</span>
<span className={`text-lg font-semibold ${colors.text}`}>{questionsRemaining + " items"}</span>
</div>
</div>
)}
</div>
);
}
export default InvestigationSummaryPanel;