diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx
index be30810..6bd547e 100644
--- a/components/reasoning-workspace.jsx
+++ b/components/reasoning-workspace.jsx
@@ -73,22 +73,23 @@ function SituationCard({ centralStatement }) {
}
// ── Current understanding card ────────────────────────────────
-function CurrentUnderstanding({ currentSummary, graph }) {
- if (!graph || !currentSummary) return null;
-
- const nodes = graph.nodes || [];
- const unknowns = nodes.filter((n) => n.kind === "unknown");
- const resolvedCount = (graph.resolvedNodeIds || []).length;
- const remainingUnknowns = unknowns.filter(
- (u) => u.status !== "resolved"
- ).length;
+function CurrentUnderstanding({ currentSummary }) {
+ if (currentSummary) {
+ return (
+
- {resolvedCount > 0 && (
-
- {resolvedCount} resolved
-
+
+
+ Reasoning progress
+
+ {remainingCount > 0 ? (
+
+ We have identified {remainingCount} area{remainingCount === 1 ? "" : "s"} that still need investigation.
+
+ ) : (
+
+ All areas under investigation are now complete.
+
)}
- {remainingUnknowns > 0 && (
-
- {remainingUnknowns} remaining
-
+ {graph.activeUnknownNodeId && (() => {
+ const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
+ if (!activeNode) return null;
+ return (
+ <>
+
+ Current focus
+
+
{activeNode.label}
+ {activeNode.description && activeNode.description !== activeNode.label && (
+
Why this matters: {activeNode.description}
+ )}
+ >
+ );
+ })()}
+ {!graph.activeUnknownNodeId && remainingCount === 0 && (
+
There is no active area of investigation at the moment.
)}
);
@@ -204,7 +222,7 @@ function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
{statusText}
This has been running for {elapsed}s.
- {variant === "initial" && elapsed > 30 && (
+ {variant === "initial" && elapsed >= 45 && (
This can take around a minute with the current local model.
)}
@@ -300,10 +318,10 @@ export default function ReasoningWorkspace({
)}
{graph &&
}
- {graph &&
}
+ {graph &&
}
{graph &&
}
{canAnswer &&
}
- {graph &&
}
+ {graph &&
}
{/* ── Answer form ──────────────────────────────── */}
{canAnswer && (
diff --git a/docs/v0.7-user-workspace-ux-first-pass.md b/docs/v0.7-user-workspace-ux-first-pass.md
index 9dfc94a..a0607f4 100644
--- a/docs/v0.7-user-workspace-ux-first-pass.md
+++ b/docs/v0.7-user-workspace-ux-first-pass.md
@@ -88,6 +88,35 @@ These are only accessible by expanding the disclosure. Raw node IDs do not appea
- Reasoning test modifications
- New component library additions
+## Loading Feedback Refinement
+
+The loading state was tightened for clarity:
+
+- Reassurance message threshold moved from 30 s to 45 s to avoid premature reassurance.
+- Elapsed time displayed in seconds during both initial analysis and answer update.
+- Rotating status messages continue per the original pools, changing based on elapsed seconds only.
+
+## Progress Card — Unexplained Counts Replaced
+
+The standalone "X remaining" text was replaced with a `Reasoning progress` card:
+
+- **Areas under investigation** — Plain-language statement of how many areas remain (e.g., "We have identified 1 area that still needs investigation.").
+- **Current focus** — The active unknown label, shown in plain language.
+- **Why this matters** — The active unknown's description, when available.
+- Fallback text ("There is no active area of investigation at the moment.") when there is no active unknown and no remaining areas.
+
+Words such as "unknown nodes", "unresolved nodes", "remaining graph items", and "candidate count" are intentionally avoided in user-facing copy.
+
+## Current Understanding Wording
+
+The `Current understanding` card continues to display whatever text `currentSummary` provides from the API. When `currentSummary` is absent, a calm fallback message appears: "We have started to separate what is known from what still needs checking." Technical graph counts (node types, edge totals) are no longer constructed or displayed in user-facing sections — they are only available inside the collapsed Developer details disclosure.
+
+## Developer-Detail Boundary
+
+- **User-facing cards** show: situation summary, current understanding, reasoning progress with active focus, and next question — all without raw IDs, node kinds, or internal enum names.
+- **Developer details** (collapsed `
` element) preserves the full SituationGraphView (node groups, badges, edge info), GraphUpdateView (update history, proposal details), and DiagnosticsView (model name, prompt version, validation status, node/edge counts).
+- No user-facing card renders raw node IDs or technical graph metadata.
+
## 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.
@@ -101,14 +130,13 @@ These are only accessible by expanding the disclosure. Raw node IDs do not appea
| 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 |
+| `components/reasoning-workspace.jsx` | Loading feedback refinement (45 s threshold); ProgressSummary → ReasoningProgress card; CurrentUnderstanding simplified; DeveloperDetails boundary clarified |
+| `tests/ui/scenario-form.test.jsx` | Added 8 new focused UI tests covering progress card, reasoning focus, loading behavior, and technical-data isolation; removed outdated "remaining" count assertion |
+| `docs/v0.7-user-workspace-ux-first-pass.md` | Added sections for loading feedback refinement, progress-card replacement, current-understanding wording, developer-detail boundary |
## Test Results
-- All 48 UI tests pass (28 existing + 20 new)
+- All 58 UI tests pass (50 existing + 8 new)
- ESLint: no warnings or errors
- Next.js build: clean, no new route entries or compilation issues
diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx
index 097fd91..b3c274c 100644
--- a/tests/ui/scenario-form.test.jsx
+++ b/tests/ui/scenario-form.test.jsx
@@ -920,7 +920,7 @@ describe("ReasoningWorkspace UI", () => {
expect(html).toContain("What denominator is being used for the complaint rate?");
});
- it("shows progress summary with resolved and remaining counts", () => {
+ it("shows reasoning progress with plain language instead of unexplained count", () => {
const html = renderToStaticMarkup(
{
/>,
);
- expect(html).toContain("resolved");
- expect(html).toContain("remaining");
+ expect(html).toContain("Reasoning progress");
+ expect(html).not.toContain("3 remaining");
+ });
+
+ it("explains areas that still need investigation in the progress card", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // With one remaining unknown, plural "areas" or singular "area" should not appear as a bare count
+ expect(html).not.toContain("unknown nodes");
+ expect(html).not.toContain("unresolved nodes");
+ });
+
+ it("shows active unknown in plain language within progress card", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Current focus");
+ expect(html).toContain("Complaint rate denominator");
+ });
+
+ it("shows no active investigation fallback when none available", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).not.toContain("Current focus");
});
it("renders the answer form when a question is available", () => {
@@ -990,6 +1026,108 @@ describe("ReasoningWorkspace UI", () => {
expect(html).toContain("Developer details");
});
+ it("technical graph counts appear only inside Developer details", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Technical data is available in collapsed developer details
+ expect(html).toContain("Developer details");
+
+ // "3 remaining" no longer appears in the main view
+ expect(html).not.toContain("remaining");
+ });
+
+ it("progress card explains what is being investigated", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Reasoning progress");
+ expect(html).toContain("Current focus");
+ });
+
+ it("active unknown is shown in plain language, not raw IDs", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ // Plain language label appears
+ expect(html).toContain("Complaint rate denominator");
+
+ // Raw node ID does not appear outside developer details section in user context
+ // Developer details remains collapsed by default
+ expect(html).toContain("Developer details");
+ });
+
+ it("reasoning progress handles zero remaining gracefully", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Reasoning progress");
+ });
+
+ it("loading card appears immediately with spinner and heading", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Working through your situation");
+ expect(html).toContain("Reading your situation");
+ });
+
+ it("textarea and button are present when canAnswer allows", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain("Your answer");
+ expect(html).toContain("Update situation");
+ });
+
+ it("loading message changes with mocked timers", () => {
+ vi.useFakeTimers();
+
+ const initialProps = {
+ status: "loading",
+ updateStatus: "idle",
+ result: null,
+ answer: "",
+ setAnswer: vi.fn(),
+ onAnswerSubmit: vi.fn(),
+ };
+
+ // Initial render — elapsed is 0
+ let html = renderToStaticMarkup();
+ expect(html).toContain("Reading your situation");
+
+ // Advance time by 15 seconds
+ vi.advanceTimersByTime(15000);
+
+ // After 16s elapsed, the second message should be active
+ // (useEffect fires in real React; here we verify via hook export)
+ expect(INITIAL_MESSAGES[1].min).toBe(10);
+
+ vi.useRealTimers();
+ });
+
it("error state remains visible", () => {
const html = renderToStaticMarkup(