Files
confidence-engine/components/investigation-map.jsx
T

100 lines
3.4 KiB
React

/**
* 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>
);
}