feat: add investigation map workspace view

This commit is contained in:
2026-08-05 10:10:16 +01:00
parent a4dc165385
commit ce673b8eb4
5 changed files with 247 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* Investigation Map — user-facing workspace card.
*
* Shows the progress of reasoning as a set of investigation topics with
* simple status indicators. Does NOT expose graph internals.
*
* Design principles:
* - Calm, spacious, accessible
* - No percentages, no progress bars, no confidence scores
* - Topics evolve naturally across turns
*/
import getInvestigationMapTopics from "@/lib/map/investigation-map-adapter";
/* ── Status icons (unicode — no icon library dependency) ─── */
const STATUS_ICONS = {
established: "✓",
current: "●",
unknown: "○",
};
function topicRowColor(status) {
switch (status) {
case "established":
return "text-gray-900";
case "current":
return "text-blue-800";
default:
return "text-gray-400";
}
}
function topicIconColor(status) {
switch (status) {
case "established":
return "text-green-600";
case "current":
return "text-blue-500";
default:
return "text-gray-300";
}
}
/* ── Single topic row ───────────────────────────────────── */
function TopicRow({ title, status }) {
const icon = STATUS_ICONS[status];
const colorClass = topicRowColor(status);
const iconColor = topicIconColor(status);
const ariaLabel = `${status === "established" ? "Established" : status === "current" ? "Currently investigating" : "Still to explore"}: ${title}`;
return (
<div
className={`flex items-center gap-3 py-2.5 text-sm transition-opacity duration-300 ease-in-out ${colorClass}`}
aria-label={ariaLabel}
role="listitem"
data-testid={`map-topic-${status === "established" ? "established" : status === "current" ? "current" : "unknown"}`}
>
<span className={`flex-none text-base ${iconColor} leading-none`} aria-hidden="true">
{icon}
</span>
<span className="flex-1">{title}</span>
</div>
);
}
/* ── Card wrapper ────────────────────────────────────────── */
export default function InvestigationMap({ turnCount = 0 }) {
const topics = getInvestigationMapTopics(turnCount);
// Group topics by status for cleaner rendering
const groups = {
established: topics.filter((t) => t.status === "established"),
current: topics.filter((t) => t.status === "current"),
unknown: topics.filter((t) => t.status === "unknown"),
};
// Only render the card if there are non-established topics (during active investigation)
const hasActiveTopics = groups.current.length > 0 || groups.unknown.length > 0;
if (!hasActiveTopics && groups.established.length === 0) return null;
return (
<div className="rounded-lg border border-gray-200 bg-white p-5" role="region" aria-label="Investigation map">
<h2 className="mb-1 text-sm font-semibold uppercase tracking-wide text-gray-500">
Investigation Map
</h2>
<p className="mb-3 text-xs text-gray-400">
We are building an understanding of your situation one question at a time.
</p>
<div className="space-y-px border-t border-gray-100 pt-3" role="list" aria-label="Investigation topics">
{topics.map((topic, i) => (
<TopicRow key={`${topic.title}-${i}`} title={topic.title} status={topic.status} />
))}
</div>
</div>
);
}
+6
View File
@@ -5,6 +5,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
import GraphUpdateView from "@/components/graph-update-view";
import SituationGraphView from "@/components/situation-graph-view";
import InvestigationSummaryPanel from "@/components/investigation-summary-panel";
import InvestigationMap from "@/components/investigation-map";
// ── Technical summary detector (main view filters these) ───
const TECHNICAL_PATTERNS = [
@@ -733,6 +734,11 @@ export default function ReasoningWorkspace({
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding || null} />
)}
{/* ── Investigation Map (visible during active investigation) ─ */}
{hasSelectedQuestion && (
<InvestigationMap turnCount={investigationHistory.length} />
)}
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />
{investigationHistory.length > 0 && <InvestigationHistory turns={investigationHistory} />}