diff --git a/components/diagnostics-view.jsx b/components/diagnostics-view.jsx
index 60a5d05..07433ea 100644
--- a/components/diagnostics-view.jsx
+++ b/components/diagnostics-view.jsx
@@ -1,3 +1,5 @@
+import React from "react";
+
const ValidationIndicator = ({ status }) => {
const styles = {
valid: "text-green-600",
@@ -27,23 +29,53 @@ const validationIcons = {
export default function DiagnosticsView({ result }) {
if (!result) return null;
+ const diagnostics = result.diagnostics || result;
+
const metrics = [
- { label: "Model", value: result.modelName || "?" },
+ { label: "Model", value: diagnostics.modelName || result.modelName || "?" },
{ label: "Provider", value: "Ollama" },
- { label: "Prompt version", value: result.promptVersion || "?" },
+ {
+ label: "Prompt version",
+ value: diagnostics.promptVersion || result.promptVersion || "?",
+ },
{
label: "Duration",
value:
- result.responseDurationMs != null
- ? `${result.responseDurationMs}ms`
+ diagnostics.responseDurationMs != null
+ ? `${diagnostics.responseDurationMs}ms`
: "?",
},
{
label: "Validation",
value: (
-
+
),
},
+ {
+ label: "Node count",
+ value: diagnostics.nodeCount != null ? diagnostics.nodeCount : "?",
+ },
+ {
+ label: "Edge count",
+ value: diagnostics.edgeCount != null ? diagnostics.edgeCount : "?",
+ },
+ {
+ label: "Graph references",
+ value:
+ diagnostics.graphReferenceValidation == null
+ ? "?"
+ : diagnostics.graphReferenceValidation.valid
+ ? `${validationIcons.valid} valid`
+ : `${validationIcons.invalid} invalid`,
+ },
+ ];
+
+ const errors = [
+ ...(result.errors || []),
+ ...(result.validationErrors || []),
+ ...(result.analysisErrors || []),
];
return (
@@ -72,14 +104,14 @@ export default function DiagnosticsView({ result }) {
)}
{/* Errors if present */}
- {result.errors && result.errors.length > 0 && (
+ {errors.length > 0 && (
- Validation errors ({result.errors.length})
+ Validation errors ({errors.length})
- {result.errors.map((err, i) => (
- - {err}
+ {errors.map((err, i) => (
+ - {typeof err === "string" ? err : err?.message || JSON.stringify(err)}
))}
diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx
index fb2e7d8..e71bc28 100644
--- a/components/scenario-form.jsx
+++ b/components/scenario-form.jsx
@@ -1,14 +1,59 @@
"use client";
+import React from "react";
import { useState, useRef } from "react";
-import ReconstructionView from "@/components/reconstruction-view";
import DiagnosticsView from "@/components/diagnostics-view";
+import SituationGraphView from "@/components/situation-graph-view";
const MAX_LENGTH = 10000;
+export async function submitScenarioForStartCase(fetchImpl, scenario) {
+ return fetchImpl("/api/cases/start", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ scenario }),
+ });
+}
+
+export function ScenarioResultPanels({ status, result }) {
+ if (!result) return null;
+
+ const hasGraph = Boolean(result.situationGraph);
+ const hasQuestion = Boolean(result.selectedQuestion?.question);
+ const hasDiagnostics = Boolean(result.diagnostics);
+
+ return (
+ <>
+ {status === "error" && (
+
+ {result.error && (
+
+ Error: {result.error}
+
+ )}
+ {!hasGraph && !hasQuestion && (
+
+ Validation failed — no structured graph output was produced.
+
+ )}
+
+ )}
+
+ {(status === "success" || hasGraph || hasQuestion) && (
+
+ )}
+
+ {hasDiagnostics && }
+ >
+ );
+}
+
export default function ScenarioForm() {
const [scenario, setScenario] = useState("");
- const [status, setStatus] = useState("idle"); // idle | loading | error | success | partial
+ const [status, setStatus] = useState("idle"); // idle | loading | error | success
const [result, setResult] = useState(null);
const textareaRef = useRef(null);
@@ -18,19 +63,11 @@ export default function ScenarioForm() {
setResult(null);
try {
- const res = await fetch("/api/analyse", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ scenario }),
- });
+ const res = await submitScenarioForStartCase(fetch, scenario);
const data = await res.json();
- if (res.ok && data.validationStatus === "valid") {
- setStatus("success");
- setResult(data);
- } else if (data.success) {
- // Success in analysis but validation may be partial
+ if (res.ok && data.success) {
setStatus("success");
setResult(data);
} else {
@@ -43,14 +80,6 @@ export default function ScenarioForm() {
}
};
- // Determine if we have meaningful content to display
- const hasClassification = result?.inputClassification;
- const hasReconstruction = result?.reconstruction;
- const hasNextQuestion = result?.nextQuestion;
- const hasEvidence = result?.evidence && result.evidence.length > 0;
- const hasMeaningfulContent =
- hasClassification || hasReconstruction || hasNextQuestion || hasEvidence;
-
return (
- {/* Error state */}
- {status === "error" && (
-
- {result?.error && (
-
- Error: {result.error}
-
- )}
- {/* Show partial content even on validation failure */}
- {(hasClassification || hasReconstruction) && (
-
- ⚠ Partial result — some fields failed validation. Showing what was
- accepted.
-
- )}
- {hasReconstruction && (
-
- )}
-
- )}
-
- {/* Success state */}
- {status === "success" && hasMeaningfulContent && (
-
-
-
- )}
-
- {/* Always show diagnostics when we have any result */}
- {(hasClassification || hasReconstruction || hasNextQuestion) && (
-
- )}
+
{status === "loading" && (
@@ -123,13 +121,6 @@ export default function ScenarioForm() {
)}
-
- {/* Invalid result with no partial data */}
- {status === "error" && !result?.error && !hasMeaningfulContent && (
-
- Validation failed — no structured output was produced.
-
- )}
);
}
diff --git a/components/situation-graph-view.jsx b/components/situation-graph-view.jsx
new file mode 100644
index 0000000..81bd33a
--- /dev/null
+++ b/components/situation-graph-view.jsx
@@ -0,0 +1,121 @@
+"use client";
+
+import React from "react";
+
+function NodeBadge({ children, tone = "gray" }) {
+ const tones = {
+ gray: "border-gray-200 bg-gray-50 text-gray-700",
+ blue: "border-blue-200 bg-blue-50 text-blue-700",
+ green: "border-green-200 bg-green-50 text-green-700",
+ yellow: "border-yellow-200 bg-yellow-50 text-yellow-700",
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+function NodeGroup({ title, nodes }) {
+ if (!nodes?.length) return null;
+
+ return (
+
+
+ {title} ({nodes.length})
+
+
+
+ );
+}
+
+export default function SituationGraphView({
+ situationGraph,
+ selectedQuestion,
+}) {
+ if (!situationGraph) return null;
+
+ const activeUnknown = situationGraph.activeUnknownNodeId
+ ? situationGraph.nodes.find((node) => node.id === situationGraph.activeUnknownNodeId)
+ : null;
+
+ const nodesByKind = situationGraph.nodes.reduce((acc, node) => {
+ if (!acc[node.kind]) acc[node.kind] = [];
+ acc[node.kind].push(node);
+ return acc;
+ }, {});
+
+ return (
+
+ {selectedQuestion?.question && (
+
+ Selected Question
+ {selectedQuestion.question}
+
+ )}
+
+
+ Situation Graph
+
+
+
- Central statement
+ - {situationGraph.centralStatement}
+
+ {situationGraph.currentSummary && (
+
+
- Current summary
+ - {situationGraph.currentSummary}
+
+ )}
+ {activeUnknown && (
+
+
- Active unknown
+ - {activeUnknown.label}
+
+ )}
+
+
- Edge count
+ - {situationGraph.edges.length}
+
+
+
+
+ {Object.entries(nodesByKind).map(([kind, nodes]) => (
+
+ ))}
+
+
+
+ Raw graph JSON
+
+
+ {JSON.stringify(situationGraph, null, 2)}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/docs/v0.4-route-status.md b/docs/v0.4-route-status.md
index ff52c37..f423afa 100644
--- a/docs/v0.4-route-status.md
+++ b/docs/v0.4-route-status.md
@@ -5,10 +5,9 @@
- Covered by `tests/app/api/cases-start-route.test.js`.
- `app/api/start-case/route.js`
- - Untracked earlier experiment / duplicate start route.
- - Not referenced by the current UI.
- - Still mentioned in untracked handoff docs.
- - Leave untracked for now; recommend deletion once route migration is explicitly confirmed.
+ - Earlier experiment / duplicate start route.
+ - No repository UI/test references were found.
+ - Deleted from the working tree during UI connection cleanup.
- `app/api/update-case/route.js`
- Untracked future `updateCase` work.
@@ -16,5 +15,6 @@
- Leave untracked for the current milestone.
- Current UI status
- - `components/scenario-form.jsx` still calls `/api/analyse`.
- - No active UI path currently calls `/api/cases/start`, `/api/start-case`, or `/api/update-case`.
+ - `components/scenario-form.jsx` now calls `/api/cases/start` for the main experimental flow.
+ - `/api/analyse` remains available for compatibility.
+ - No active UI path currently calls `/api/update-case`.
diff --git a/tests/smoke.test.js b/tests/smoke.test.js
index f5cef5e..a15f642 100644
--- a/tests/smoke.test.js
+++ b/tests/smoke.test.js
@@ -1,55 +1,70 @@
import { test, expect } from "@playwright/test";
-test("v0.3 UI smoke test with live model response", async ({ page }) => {
- await page.goto("http://localhost:3000");
+const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000";
+
+test.setTimeout(300000);
+
+test("graph-backed start flow smoke test", async ({ page }) => {
+ await page.goto(BASE_URL);
// Page should load without error
await expect(page.getByText(/Confidence Engine/i)).toBeVisible();
// Type the scenario
const textarea = page.locator("textarea[placeholder*='Describe']");
- await textarea.fill("Complaints increased by 35% while production increased by 40%.");
+ await textarea.fill(
+ "Complaints increased by 35% while production increased by 40%.",
+ );
+ await expect(textarea).toHaveValue(
+ "Complaints increased by 35% while production increased by 40%.",
+ );
// Button should be enabled
await expect(page.getByRole("button", { name: /Analyse/i })).toBeEnabled();
- // Click Analyse and wait for diagnostics panel
+ // Click Analyse and wait for graph-backed result
await page.getByRole("button", { name: /Analyse/i }).click();
-
- // Wait for result section (ReconstructionView rendered)
- await expect(page.getByRole("heading", { name: /Next Question/i })).toBeVisible({ timeout: 180000 });
- // Take screenshot of result page
- await page.screenshot({ path: "tests-results/smoke-v0.3.png", fullPage: true });
+ await expect(
+ page.getByRole("heading", { name: /Selected Question/i }),
+ ).toBeVisible({ timeout: 180000 });
+ await expect(
+ page.getByRole("heading", { name: /Situation Graph/i }),
+ ).toBeVisible({ timeout: 180000 });
+ await expect(page.getByText(/Central statement/i)).toBeVisible();
+ await expect(page.getByText(/Active unknown/i)).toBeVisible();
+ await expect(page.getByText(/Error:/i)).toHaveCount(0);
- // Verify diagnostics panel exists and contains relevant info
- const diagPanel = page.locator('details summary').first();
- if (await diagPanel.isVisible()) {
- console.log("Raw response viewer:", await diagPanel.innerText().catch(() => "not visible"));
- }
+ const rawJsonToggle = page.getByText(/Raw graph JSON/i);
+ await expect(rawJsonToggle).toBeVisible();
+ await rawJsonToggle.click();
+ await expect(page.getByText(/centralStatement/i)).toBeVisible();
// Get full body text for verification
const bodyText = await page.locator("body").innerText();
-
+
console.log("\n=== UI Smoke Test Results ===");
console.log("Page title:", await page.title());
console.log("Body content length:", bodyText.length);
-
+
// Check key content indicators
- const hasNextQ = bodyText.includes("Next Question");
- const hasComplaints = bodyText.includes("Complaint") || bodyText.includes("complaint");
- const hasProduction = bodyText.includes("production") || bodyText.includes("Production");
- const hasRateContext = bodyText.toLowerCase().includes("rate") ||
- bodyText.toLowerCase().includes("unit") ||
- bodyText.toLowerCase().includes("denominator") ||
- bodyText.toLowerCase().includes("per-unit");
-
- console.log("Has Next Question heading:", hasNextQ);
+ const hasSelectedQuestion = bodyText.includes("Selected Question");
+ const hasComplaints =
+ bodyText.includes("Complaint") || bodyText.includes("complaint");
+ const hasProduction =
+ bodyText.includes("production") || bodyText.includes("Production");
+ const hasRateContext =
+ bodyText.toLowerCase().includes("rate") ||
+ bodyText.toLowerCase().includes("unit") ||
+ bodyText.toLowerCase().includes("denominator") ||
+ bodyText.toLowerCase().includes("per-unit");
+
+ console.log("Has selected question heading:", hasSelectedQuestion);
console.log("Has complaints reference:", hasComplaints);
console.log("Has production reference:", hasProduction);
console.log("Has rate context (rate/unit/denominator):", hasRateContext);
// Basic structural checks
expect(bodyText.length).toBeGreaterThan(200);
- expect(hasNextQ).toBe(true);
-}, { timeout: 300000 });
+ expect(hasSelectedQuestion).toBe(true);
+});
diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx
new file mode 100644
index 0000000..e08dcbc
--- /dev/null
+++ b/tests/ui/scenario-form.test.jsx
@@ -0,0 +1,190 @@
+import React from "react";
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import DiagnosticsView from "@/components/diagnostics-view.jsx";
+import SituationGraphView from "@/components/situation-graph-view.jsx";
+import {
+ ScenarioResultPanels,
+ submitScenarioForStartCase,
+} from "@/components/scenario-form.jsx";
+
+function makeGraphResult(overrides = {}) {
+ return {
+ success: true,
+ situationGraph: {
+ centralStatement: "Complaints increased while production increased.",
+ currentSummary:
+ "Nodes: 2 observation, 1 unknown | Edges: 2 total | Unknowns: 1 unresolved",
+ activeUnknownNodeId: "n-unknown",
+ resolvedNodeIds: [],
+ nodes: [
+ {
+ id: "n-1",
+ label: "Complaints up 35%",
+ description: "Complaints increased by 35%",
+ kind: "observation",
+ status: "supported",
+ confidence: "high",
+ value: 35,
+ unit: "%",
+ },
+ {
+ id: "n-2",
+ label: "Production up 40%",
+ description: "Production increased by 40%",
+ kind: "observation",
+ status: "supported",
+ confidence: "high",
+ value: 40,
+ unit: "%",
+ },
+ {
+ id: "n-unknown",
+ label: "Complaint rate denominator",
+ description: "Need the denominator for complaint rate",
+ kind: "unknown",
+ status: "unknown",
+ confidence: "medium",
+ value: null,
+ unit: null,
+ },
+ ],
+ edges: [
+ { id: "e1", fromNodeId: "n-1", toNodeId: "n-unknown" },
+ { id: "e2", fromNodeId: "n-2", toNodeId: "n-unknown" },
+ ],
+ },
+ selectedQuestion: {
+ question: "What denominator is being used for the complaint rate?",
+ },
+ diagnostics: {
+ modelName: "test",
+ responseDurationMs: 1234,
+ validationStatus: "valid",
+ promptVersion: "test-prompt",
+ nodeCount: 3,
+ edgeCount: 2,
+ graphReferenceValidation: { valid: true, errors: [] },
+ },
+ ...overrides,
+ };
+}
+
+describe("scenario-form UI helpers", () => {
+ it("submits to /api/cases/start", async () => {
+ const fetchImpl = vi.fn().mockResolvedValue({ ok: true });
+
+ await submitScenarioForStartCase(fetchImpl, "Scenario text");
+
+ expect(fetchImpl).toHaveBeenCalledWith(
+ "/api/cases/start",
+ expect.objectContaining({
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ }),
+ );
+ });
+});
+
+describe("graph-backed UI rendering", () => {
+ it("renders central statement from successful graph response", () => {
+ const data = makeGraphResult();
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Central statement");
+ expect(html).toContain("Complaints increased while production increased.");
+ });
+
+ it("renders active unknown", () => {
+ const data = makeGraphResult();
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Active unknown");
+ expect(html).toContain("Complaint rate denominator");
+ });
+
+ it("renders selected question exactly once", () => {
+ const data = makeGraphResult();
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(
+ html.match(/What denominator is being used for the complaint rate\?/g) ||
+ [],
+ ).toHaveLength(1);
+ });
+
+ it("renders diagnostics", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Diagnostics");
+ expect(html).toContain("test");
+ expect(html).toContain("1234ms");
+ expect(html).toContain("Node count");
+ expect(html).toContain("Edge count");
+ expect(html).toContain("Graph references");
+ });
+
+ it("hides empty sections", () => {
+ const base = makeGraphResult();
+ const result = makeGraphResult({
+ selectedQuestion: null,
+ situationGraph: {
+ ...base.situationGraph,
+ activeUnknownNodeId: null,
+ nodes: [base.situationGraph.nodes[0]],
+ edges: [],
+ },
+ });
+
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).not.toContain("Selected Question");
+ expect(html).not.toContain("Active unknown");
+ });
+
+ it("displays API error clearly", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Error: Invalid start-case request");
+ });
+
+ it("renders expandable raw graph JSON", () => {
+ const data = makeGraphResult();
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Raw graph JSON");
+ expect(html).toContain(""centralStatement"");
+ });
+});
\ No newline at end of file