+ );
+}
+
+// ── No question state ─────────────────────────────────────────
+function NoQuestionMessage({ noQuestionReason }) {
+ let message = "There is no next question at the moment.";
+ if (noQuestionReason) {
+ const reason = String(noQuestionReason);
+ if (reason.toLowerCase().includes("satisfied") || reason.toLowerCase().includes("complete")) {
+ message += " The situation has been fully investigated.";
+ } else if (reason.toLowerCase().includes("insufficient")) {
+ message += " We need more information to determine the next step.";
+ } else {
+ message += " " + reason;
+ }
+ }
+ return (
+
+
{message}
+
+ );
+}
+
+// ── Loading overlay (for both start and update) ───────────────
+function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
+ if (!isLoading) return null;
+
+ const messages = variant === "update" ? UPDATE_MESSAGES : INITIAL_MESSAGES;
+ let statusText = messages[0].text;
+ for (const m of messages) {
+ if (elapsed >= m.min) statusText = m.text;
+ }
+
+ return (
+
+
+
+ Working through your situation
+
+
{statusText}
+
+ This has been running for {elapsed}s.
+ {variant === "initial" && elapsed > 30 && (
+ This can take around a minute with the current local model.
+ )}
+
+ {noQuestionReason
+ ? "Validation failed — no structured graph output was produced."
+ : "The analysis completed but did not produce a structured result."}
+
+ {/* ── Main result workspace ─────────────────────── */}
+ {(status === "success" || status === "error" || updateStatus === "success") && (
+
)}
{/* Empty state */}
diff --git a/docs/v0.7-user-workspace-ux-first-pass.md b/docs/v0.7-user-workspace-ux-first-pass.md
new file mode 100644
index 0000000..9dfc94a
--- /dev/null
+++ b/docs/v0.7-user-workspace-ux-first-pass.md
@@ -0,0 +1,129 @@
+# v0.7 UX First Pass — User-Focused Reasoning Workspace
+
+## UX Problem
+
+The current interface exposes the reasoning engine's graph structure directly to users. It presents:
+
+- Raw node-grouped tables with status/confidence badges
+- Diagnostic metadata (model name, prompt version, validation status)
+- Graph update change details (resolved nodes, affected nodes, proposal JSON)
+- A bare "Waiting for model response..." placeholder with no elapsed time or rotating status
+
+This is useful as a developer/debug view but difficult to understand for non-technical users. The next question is visually buried under the graph tables, and there is no clear feedback during slow LLM analysis or update operations.
+
+## Design Goals
+
+- **Calmer default view**: Present scenario, understanding, focus, next question, and progress as a sequence of clean cards
+- **Preserve full debug access**: All existing graph, diagnostics, and update history components remain available behind a collapsed disclosure
+- **Clear slow-operation feedback**: Animated spinner, elapsed time, rotating plain-language status messages during analysis and update operations
+- **Professional visual tone**: Neutral colours, generous whitespace, restrained borders, no gradients or glassmorphism
+
+## Main Workspace Structure
+
+The `ReasoningWorkspace` component (`components/reasoning-workspace.jsx`) renders the result area. When a successful start analysis completes, it shows:
+
+1. **Your situation** — Central statement from `situationGraph.centralStatement`, displayed in a white card
+2. **Current understanding** — The API's `currentSummary` text in a second white card
+3. **What we are working out** — The active unknown label, its description ("Why it matters"), and a plain-language status badge (e.g., "Under investigation")
+4. **Next question** — The largest visual element: green-bordered card with bold heading and prominent question text in `text-xl` font-weight-semibold
+5. **Progress** — A single inline bar showing resolved count + remaining unknown count (no percentage)
+6. **Answer form** — Visible only when a selected question exists; textarea + "Update situation" button, disabled during update loading
+7. **Developer details** — Collapsible `` element with full SituationGraphView, GraphUpdateView, and DiagnosticsView inside; closed by default
+
+When analysis completes without producing a graph:
+- A yellow warning card states the outcome plainly
+- Error messages remain in red cards above all content
+
+When there is no next question:
+- A calm gray card says "There is no next question at the moment." with a contextual elaboration derived from `noQuestionReason` when available
+- No broken-looking empty areas appear
+
+## Loading-State Behaviour
+
+### Initial analysis (start request)
+
+A blue-bordered card appears with:
+- **Spinner** — CSS-only spinning ring (`@keyframes spin`)
+- **Heading**: "Working through your situation"
+- **Rotating status text** (based on elapsed seconds):
+ - 0–10s: "Reading your situation"
+ - 10–25s: "Building a structured understanding"
+ - 25–45s: "Identifying what is known and still unclear"
+ - 45+s: "Selecting the next useful question"
+- **Elapsed time**: "This has been running for Xs."
+- **Reassuring copy** (shown after 30s): "This can take around a minute with the current local model."
+
+### Answer update (update request)
+
+Same card format, different status text pool:
+- 0–10s: "Considering your answer"
+- 10–25s: "Updating the situation"
+- 25–45s: "Checking what changed"
+- 45+s: "Choosing the next question"
+
+### Duplicate submit prevention
+
+Both "Analyse" and "Update situation" buttons are `disabled` while their respective `status` / `updateStatus` is `"loading"`. The answer textarea also disables during update loading.
+
+## Debug View Preservation
+
+All existing components are preserved inside the collapsed "Developer details" `` element:
+
+- **SituationGraphView** — Full node-grouped graph with badges, active unknown highlighting, newly surfaced markers, and raw JSON toggle
+- **GraphUpdateView** — Update history (resolved unknowns, newly surfaced unknowns, affected nodes, proposal details)
+- **DiagnosticsView** — Model name, provider, prompt version, duration, validation status, node/edge counts
+
+These are only accessible by expanding the disclosure. Raw node IDs do not appear in any user-facing card text.
+
+## Deliberate Exclusions (for this pass)
+
+- Spider/dag graph rendering
+- Persistence or session handling
+- Navigation or routing changes
+- Accounts or authentication
+- Export functionality
+- Dark mode
+- Radical input page redesign
+- Backend code changes (APIs, routes, logic, prompts, schemas)
+- Reasoning test modifications
+- New component library additions
+
+## Remaining UX Limitations
+
+1. **Multi-turn not implemented** — The workspace currently reflects the one-update prototype limitation. A multi-turn version would need persistent state management between turns.
+2. **Timer is client-side only** — Elapsed time starts when loading begins but no backend stage telemetry is exposed yet, so rotating messages are honest approximations only.
+3. **No skeleton/loading shimmer** — The spinner card replaces content entirely during loading rather than showing a layout-aware skeleton. A skeleton approach would be a future enhancement.
+4. **Loading overlay does not persist across route changes** — No persistence layer means refresh loses state. This is intentional for the prototype scope.
+5. **No visual distinction between "idle" and "success" empty states** — Both render similarly when no answer is typed. A small hint like "Type an answer to continue" could be added later.
+6. **Progress count uses resolved/remaining labels only** — No percentage or bar despite having the data, per constraint. This is intentional; we avoid false precision in a prototype context.
+
+## Files Changed
+
+| File | Change |
+|------|--------|
+| `components/reasoning-workspace.jsx` | New — main workspace component with cards, loading feedback, developer details disclosure |
+| `components/scenario-form.jsx` | Refactored to use ReasoningWorkspace for result rendering; removed inline answer form/debug panels from render |
+| `app/globals.css` | Added `@keyframes spin` animation definition |
+| `tests/ui/scenario-form.test.jsx` | 20 new tests for ReasoningWorkspace rendering, loading states, error states, no-question states, debug view preservation |
+
+## Test Results
+
+- All 48 UI tests pass (28 existing + 20 new)
+- ESLint: no warnings or errors
+- Next.js build: clean, no new route entries or compilation issues
+
+## Manual UI Notes
+
+A single manual check was not performed in this pass. The next step for verification is:
+
+1. Run `npm run dev`
+2. Submit a scenario to an available local LLM endpoint
+3. Confirm the initial loading card shows rotating status messages
+4. Confirm the result renders as a clean sequence of cards with "Next question" as the strongest visual element
+5. Expand "Developer details" and confirm graph/diagnostics/updates are preserved
+6. Submit an answer and confirm update loading feedback appears
+7. Confirm no raw node IDs appear outside the developer section
+
+---
+
+*This is a first-pass UX improvement only. Reasoning logic, API contracts, schemas, and tests remain unchanged.*
diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx
index e3d13b5..097fd91 100644
--- a/tests/ui/scenario-form.test.jsx
+++ b/tests/ui/scenario-form.test.jsx
@@ -4,6 +4,11 @@ import { renderToStaticMarkup } from "react-dom/server";
import DiagnosticsView from "@/components/diagnostics-view.jsx";
import GraphUpdateView from "@/components/graph-update-view.jsx";
import SituationGraphView from "@/components/situation-graph-view.jsx";
+import ReasoningWorkspace, {
+ useLoadingStatus,
+ INITIAL_MESSAGES,
+ UPDATE_MESSAGES,
+} from "@/components/reasoning-workspace.jsx";
import {
ScenarioResultPanels,
UpdateErrorPanel,
@@ -838,4 +843,309 @@ describe("graph-backed UI rendering", () => {
expect(html).toContain("New active unknown");
expect(html).not.toContain("No next question selected yet.");
});
+});
+
+// ── ReasoningWorkspace tests ────────────────────────────────
+describe("ReasoningWorkspace UI", () => {
+ function makeWorkspaceResult(overrides = {}) {
+ return {
+ success: true,
+ situationGraph: makeGraphResult().situationGraph,
+ selectedQuestion: { question: "What denominator is being used for the complaint rate?" },
+ newlySurfacedNodeIds: [],
+ diagnostics: {
+ modelName: "test",
+ responseDurationMs: 1234,
+ validationStatus: "valid",
+ nodeCount: 3,
+ edgeCount: 2,
+ graphReferenceValidation: { valid: true, errors: [] },
+ },
+ ...overrides,
+ };
+ }
+
+ function makeWorkspaceProps(overrides = {}) {
+ return {
+ status: "success",
+ updateStatus: "idle",
+ result: makeWorkspaceResult(),
+ answer: "",
+ setAnswer: vi.fn(),
+ onAnswerSubmit: vi.fn(),
+ ...overrides,
+ };
+ }
+
+ it("renders by default with a successful start result", () => {
+ const html = renderToStaticMarkup();
+
+ expect(html).toContain("Your situation");
+ expect(html).toContain("Complaints increased while production increased.");
+ });
+
+ it("shows current understanding section", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Current understanding");
+ expect(html).toContain("Nodes: 2 observation, 1 unknown");
+ });
+
+ it("shows current focus section with the active unknown", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("What we are working out");
+ expect(html).toContain("Complaint rate denominator");
+ });
+
+ it("prominently displays the next question", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Next question");
+ expect(html).toContain("What denominator is being used for the complaint rate?");
+ });
+
+ it("shows progress summary with resolved and remaining counts", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("resolved");
+ expect(html).toContain("remaining");
+ });
+
+ it("renders the answer form when a question is available", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Your answer");
+ expect(html).toContain("Update situation");
+ });
+
+ it("renders developer details section (collapsed)", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Developer details");
+ });
+
+ it("preserves the full situation graph in developer details", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Situation Graph");
+ expect(html).toContain("Central statement");
+ });
+
+ it("preserves diagnostics in developer details", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Diagnostics");
+ expect(html).toContain("test");
+ expect(html).toContain("1234ms");
+ });
+
+ it("user-facing sections show labels, not raw node IDs", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Labels and human-readable text should be present
+ expect(html).toContain("Your situation");
+ expect(html).toContain("Current understanding");
+ expect(html).toContain("What we are working out");
+ expect(html).toContain("Next question");
+ expect(html).toContain("Complaint rate denominator");
+
+ // Raw node IDs only appear in developer details (collapsed), not in user-facing sections
+ expect(html).toContain("Developer details");
+ });
+
+ it("error state remains visible", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Error: Invalid start-case request");
+ });
+
+ it("no-question state is handled clearly", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("There is no next question at the moment.");
+ });
+
+ it("initial loading state appears with spinner heading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Working through your situation");
+ expect(html).toContain("Reading your situation");
+ });
+
+ it("update loading state appears with spinner heading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Working through your situation");
+ });
+
+ it("elapsed time text appears during loading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("This has been running for");
+ });
+
+ it("empty state shows guidance when idle", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Should not show any result panels or workspace
+ expect(html).not.toContain("Your situation");
+ expect(html).not.toContain("Next question");
+ });
+
+ it("update error is visible", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Update error: Invalid graph update proposal");
+ });
+
+ it("message pools are exported for external use", () => {
+ expect(INITIAL_MESSAGES.length).toBeGreaterThan(0);
+ expect(UPDATE_MESSAGES.length).toBeGreaterThan(0);
+ expect(INITIAL_MESSAGES[0].min).toBe(0);
+ expect(UPDATE_MESSAGES[0].min).toBe(0);
+ });
+
+ it("no answer form shown when there is no question", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).not.toContain("Your answer");
+ expect(html).not.toContain("Update situation");
+ });
+
+ it("update result merges into workspace correctly", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Updated summary");
+ expect(html).toContain(
+ "What evidence would clarify how the two observations were measured?",
+ );
+ });
});
\ No newline at end of file