From ce673b8eb4ee267825cc465c3d117729b1633e6e Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 5 Aug 2026 10:10:16 +0100 Subject: [PATCH] feat: add investigation map workspace view --- components/investigation-map.jsx | 100 +++++++++++++++++++++++++++ components/reasoning-workspace.jsx | 6 ++ docs/reasoning-contract-backlog.md | 7 ++ lib/map/investigation-map-adapter.js | 63 +++++++++++++++++ tests/e2e/happy-path.spec.js | 71 +++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 components/investigation-map.jsx create mode 100644 lib/map/investigation-map-adapter.js diff --git a/components/investigation-map.jsx b/components/investigation-map.jsx new file mode 100644 index 0000000..1360013 --- /dev/null +++ b/components/investigation-map.jsx @@ -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 ( +
+ + {title} +
+ ); +} + +/* ── 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 ( +
+

+ Investigation Map +

+

+ We are building an understanding of your situation one question at a time. +

+ +
+ {topics.map((topic, i) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 3bde7fc..112823d 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -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({ )} + {/* ── Investigation Map (visible during active investigation) ─ */} + {hasSelectedQuestion && ( + + )} + {investigationHistory.length > 0 && } diff --git a/docs/reasoning-contract-backlog.md b/docs/reasoning-contract-backlog.md index d4d0593..dad316c 100644 --- a/docs/reasoning-contract-backlog.md +++ b/docs/reasoning-contract-backlog.md @@ -97,6 +97,13 @@ temporary workaround and the desired eventual contract. | OriginalSituation | Central statement display | `scenario` prop (user input) or `graph.centralStatement` | Engine-derived central statement from user input | Start case | UI already handles both; engine should normalise | | DeveloperDetails | Node descriptions | Mock nodes have `label === description` | Distinct, detailed description per node | Graph construction | Current mock uses label as description; separate fields needed | +## Investigation Map (Workspace UX) + +| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes | +| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- | +| InvestigationMap | Visible investigation progress | Mock topic set with manual turn-based status progression | Engine emits `investigationTopics: [{ title, status, ordering?, evidenceCount? }]` | Each turn — start and update response | UI displays topics in engine-determined order; statuses: "established" / "current" / "unknown" | +| InvestigationMap | Topic status evolution across turns | Hardcoded PROGRESSION array indexed by `investigationHistory.length` | Engine determines which topics are established, active, or unknown at each turn | Question selection phase | Topics should not expose graph internals; plain-language labels only | + ## Open Questions / Future Work 1. **Structured confidence scores**: The UI currently mocks ordinal confidence (low/medium/high). The reasoning engine should eventually emit numeric confidence values per node and a computed conclusion confidence, enabling richer visual indicators. diff --git a/lib/map/investigation-map-adapter.js b/lib/map/investigation-map-adapter.js new file mode 100644 index 0000000..89171fe --- /dev/null +++ b/lib/map/investigation-map-adapter.js @@ -0,0 +1,63 @@ +/** + * Investigation Map Mock Adapter + * + * Provides the Investigation Map with a set of investigation topics and their + * current status (established / current / unknown). + * + * TODO: Replace this mock adapter when the reasoning engine emits real + * investigation data. The eventual contract should provide: + * - `investigationTopics`: [{ title, status, evidenceCount? }] + * - `topicStatus` values: "established" | "current" | "unknown" + * - `topicOrdering`: the reasoning-engine-determined sequence + * - `evidenceCount`: optional count of supporting evidence per topic + * + * Until then, this adapter drives a realistic mock progression across turns. + */ + +/** @type {Array<{ title: string }>} */ +const TOPICS = [ + { title: "Central situation" }, + { title: "Complaint trend direction" }, + { title: "Measurement basis" }, + { title: "Production volume context" }, + { title: "QA process changes" }, + { title: "Product change log" }, + { title: "Support response patterns" }, + { title: "Prior similar cases" }, +]; + +/** + * Status progression per turn index. + * The reasoning engine will eventually determine these values. + */ +const PROGRESSION = [ + // Turn 0 — initial analysis just started + ["established", "unknown", "unknown", "unknown", "unknown", "unknown", "unknown", "unknown"], + // Turn 1 — first question answered + ["established", "established", "current", "unknown", "unknown", "unknown", "unknown", "unknown"], + // Turn 2 — second question answered + ["established", "established", "established", "current", "unknown", "unknown", "unknown", "unknown"], + // Turn 3 — third question answered + ["established", "established", "established", "established", "current", "unknown", "unknown", "unknown"], + // Turn 4 — fourth question answered + ["established", "established", "established", "established", "established", "current", "unknown", "unknown"], + // Turn 5 — fifth question answered + ["established", "established", "established", "established", "established", "established", "current", "unknown"], + // Turn 6+ — final turn + ["established", "established", "established", "established", "established", "established", "established", "current"], +]; + +/** + * Get investigation map topics for a given turn index. + * @param {number} turnIndex - Zero-based turn number (0 = initial analysis). + * @returns {{ title: string, status: 'established' | 'current' | 'unknown' }[]} + */ +export function getInvestigationMapTopics(turnIndex) { + const idx = Math.min(turnIndex, PROGRESSION.length - 1); + return TOPICS.map((topic, i) => ({ + title: topic.title, + status: PROGRESSION[idx][i], + })); +} + +export default getInvestigationMapTopics; \ No newline at end of file diff --git a/tests/e2e/happy-path.spec.js b/tests/e2e/happy-path.spec.js index a49f457..fb0cbd7 100644 --- a/tests/e2e/happy-path.spec.js +++ b/tests/e2e/happy-path.spec.js @@ -367,4 +367,75 @@ test("update submission replaces response panel with loading card, preserves con await expect(rwTextarea).toBeVisible(); await expect(overlay).not.toBeVisible({ timeout: 3_000 }); } +}); + +/* ═══════ Test: investigation map appears and evolves across turns ═ */ + +test("investigation map: appears after start, topics evolve across mocked turns", async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + window.__MOCK_ENABLED = true; + window.__MOCK_DELAY = "instant"; + window.__MOCK_SCENARIO = "complete"; + }); + + const textarea = page.locator("textarea[placeholder*=Describe]"); + await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement); + await page.getByRole("button", { name: "Analyse" }).click(); + + await waitForLoading(page); + await waitForWorkspaceReady(page); + + // Verify the investigation map card is present after start + const mapCard = page.locator('[aria-label="Investigation map"]'); + await expect(mapCard).toBeVisible(); + + // Map heading visible + await expect(mapCard.getByRole("heading", { name: "Investigation Map" })).toBeVisible(); + + // Helper text visible + await expect(mapCard.getByText(/building an understanding/i)).toBeVisible(); + + // At turn 0 (initial analysis): one topic established, one current, rest unknown + const establishedTopics = page.locator('[data-testid="map-topic-established"]'); + const currentTopics = page.locator('[data-testid="map-topic-current"]'); + const unknownTopics = page.locator('[data-testid="map-topic-unknown"]'); + + await expect(establishedTopics).toHaveCount(1); + await expect(currentTopics).toHaveCount(1); + await expect(unknownTopics).toHaveCount(6); + + // Submit first answer → turn 1: one more established, next topic becomes current + const answerTextarea = page.locator('textarea[placeholder*=Answer]'); + if (await answerTextarea.isVisible({ timeout: 3000 })) { + await answerTextarea.fill("The figures are comparable."); + await page.getByRole("button", { name: "Update situation" }).click(); + + await waitForLoading(page); + await waitForWorkspaceReady(page); + + // Current investigation remains visible (context preserved) + await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible({ timeout: 5_000 }); + + // Map should update: 2 established, 1 current, 5 unknown + await expect(establishedTopics).toHaveCount(2); + await expect(currentTopics).toHaveCount(1); + await expect(unknownTopics).toHaveCount(5); + + // Submit second answer → turn 2 + const answerTextarea2 = page.locator('textarea[placeholder*=Answer]'); + if (await answerTextarea2.isVisible({ timeout: 3000 })) { + await answerTextarea2.fill("Complaint rate fell from 2.0 to 1.9 per 100 units."); + await page.getByRole("button", { name: "Update situation" }).click(); + + await waitForLoading(page); + await waitForWorkspaceReady(page); + + // After second update: 3 established, current shifts again + await expect(establishedTopics).toHaveCount(3); + await expect(currentTopics).toHaveCount(1); + await expect(unknownTopics).toHaveCount(4); + } + } }); \ No newline at end of file