fix: localise update reasoning state

This commit is contained in:
2026-08-05 08:36:18 +01:00
parent 6d11c1d503
commit 61210c1200
4 changed files with 84 additions and 78 deletions
+28 -38
View File
@@ -197,18 +197,35 @@ The workspace is interactive.
---
### Reasoning Mode
### Reasoning Mode (initial analysis)
The engine is analysing the available evidence.
The engine is constructing the first investigation from nothing.
A full primary loading state appears:
- prominent overlay with spinner, rotating status messages, elapsed timer;
- the entire workspace is replaced until reasoning completes;
- no partial or changing content is visible during processing.
---
### Reasoning Mode (subsequent answers — localised)
The investigation already exists.
Only the active response panel is replaced by the loading card:
- Current investigation question remains visible for context;
- Current understanding, Original situation, and Investigation history persist;
- Terminal state cards are suppressed during loading;
- The workspace layout remains stable and recognisable;
- Recovery states appear in place of the loading card if reasoning fails.
The interface should:
- clearly indicate that reasoning is in progress
- temporarily suspend the workspace
- reassure the user that their input has been accepted
- avoid displaying partial or changing reasoning
The workspace is paused until reasoning completes.
- clearly indicate that reasoning is in progress via the response-panel overlay;
- reassure the user that their answer has been accepted;
- avoid displaying partial or changing reasoning outside the response panel.
---
@@ -218,37 +235,10 @@ Every submission follows the same lifecycle:
User submits information
Workspace pauses
Loading card appears (full-page for initial analysis, localised for updates)
Reasoning mode
Updated workspace appears
Updated workspace returns
The interaction should be identical whether the submission is:
- the initial situation
- an investigation answer
- a future uploaded document
- any other evidence
The interaction is consistent in intent — both modes confirm input acceptance and pause the active response area — but the page-level behaviour differs because one constructs from nothing while the other refines existing context.
Users should never wonder whether their input has been accepted or whether the engine is still reasoning.
---
## Consistent Submission Lifecycle
Every submission follows the same lifecycle regardless of context.
1. User submits → workspace pauses immediately, stale content disappears
2. Reasoning mode appears with loading overlay (spinner, rotating status messages, elapsed timer)
3. Updated workspace returns or error state appears
The loading card must appear before any async request begins and remain visible until the result arrives.
During reasoning mode:
- The workspace is fully hidden (not just disabled)
- No stale question, response form, or active indicators are visible
- The Investigation Summary Panel shows "Reasoning" rather than "Investigation in progress"
- Recovery states render on top of the loading overlay when applicable
This applies equally to initial analysis and all update submissions.
+26 -22
View File
@@ -636,20 +636,13 @@ export default function ReasoningWorkspace({
variant="initial"
/>
)}
{updateStatus === "loading" && (
<LoadingOverlay
elapsed={updateElapsed}
currentMessage={updateMsg}
variant="update"
/>
)}
{/* ── Provider unavailable recovery ──────────────── */}
{/* ── Provider unavailable recovery (always visible) ───────────── */}
{isProviderUnavailable && (
<ProviderUnavailableCard onRestart={onRestart} />
)}
{/* ── Malformed response recovery ────────────────── */}
{/* ── Malformed response recovery (always visible) ──────────────── */}
{isMalformedResponse && (
<MalformedResponseCard onRestart={onRestart} />
)}
@@ -675,15 +668,19 @@ export default function ReasoningWorkspace({
{/* ── Investigation summary card ─────────────── */}
<InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
{/* ── Active investigation: question + form (top priority) ─ */}
{canAnswer && (
{/* ── Active investigation context (always visible while question exists) ─ */}
{hasSelectedQuestion && (
<>
<CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />
</>
)}
{/* Post-update acknowledgement */}
{updateStatus === "success" && <UpdateAcknowledgement updateResult={result} />}
{/* Post-update acknowledgement — only after success, never during loading */}
{updateStatus === "success" && !isUpdating && <UpdateAcknowledgement updateResult={result} />}
{/* Response form */}
{/* ── Response area (replaced by loading / error during reasoning) ─ */}
{!isUpdating ? (
canAnswer && (
<form onSubmit={handleUpdateCaptureAndSubmit} className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
<div>
<label htmlFor="rw-answer" className="mb-2 block text-sm font-medium text-gray-700">
@@ -701,25 +698,32 @@ export default function ReasoningWorkspace({
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400">
{updateStatus === "loading" ? "Updating..." : "One update turn only in this prototype."}
One update turn only in this prototype.
</p>
<button
type="submit"
disabled={updateStatus === "loading" || !answer.trim()}
disabled={!answer.trim()}
className="rounded-lg bg-blue-700 px-5 py-2 text-sm font-medium text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40"
>
{updateStatus === "loading" ? "Updating..." : "Update situation"}
Update situation
</button>
</div>
</form>
</>
)
) : (
/* ── Local update loading card (replaces response panel) ─ */}
<LoadingOverlay
elapsed={updateElapsed}
currentMessage={updateMsg}
variant="update"
/>
)}
{/* ── Terminal state: outcome card (no active question) ─ */}
{status === "success" && !hasSelectedQuestion && graph && genuineCompletion && (
{/* ── Terminal state: outcome card — hidden during update loading ─ */}
{!isUpdating && status === "success" && !hasSelectedQuestion && graph && genuineCompletion && (
<CompletionCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
)}
{status === "success" && !hasSelectedQuestion && graph && !genuineCompletion && (
{!isUpdating && status === "success" && !hasSelectedQuestion && graph && !genuineCompletion && (
<EvidenceLimitCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
)}
@@ -753,7 +757,7 @@ export default function ReasoningWorkspace({
Error: {result.error}
</div>
)}
{updateStatus === "error" && result?.updateError && (
{updateStatus === "error" && !isProviderUnavailable && !isMalformedResponse && result?.updateError && (
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
Update error: {result.updateError.error || JSON.stringify(result.updateError)}
</div>
+1 -11
View File
@@ -443,7 +443,7 @@ export default function ScenarioForm() {
)}
{/* ── Main result workspace ─────────────────────── */}
{(status === "success" || status === "error") && updateStatus !== "loading" && (
{(status === "success" || status === "error") && (
<ReasoningWorkspace
scenario={scenario}
status={status}
@@ -475,16 +475,6 @@ export default function ScenarioForm() {
/>
)}
{/* ── Update loading (reasoning mode) ─────────── */}
{updateStatus === "loading" && status === "success" && (
<LoadingOverlay
isLoading={true}
elapsed={updateElapsed}
currentMessage={updateMsg}
variant="update"
/>
)}
{/* ── Continue later banner when session was restored ── */}
{status === "success" && result?.updatedAt && (
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
+29 -7
View File
@@ -308,7 +308,7 @@ test("loading overlay appears on submit and disappears after response", async ({
/* ═══════ Test: update loading hides workspace and shows overlay ═ */
test("update submission hides workspace, shows reasoning overlay, then restores workspace", async ({ page }) => {
test("update submission replaces response panel with loading card, preserves context", async ({ page }) => {
// Use normal (700ms) delay so the loading overlay is observable during updates
await page.goto("/");
@@ -326,23 +326,45 @@ test("update submission hides workspace, shows reasoning overlay, then restores
await waitForWorkspaceReady(page);
// Workspace visible after initial analysis
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
// Submit an answer to trigger update loading
const answerTextarea = page.locator('textarea[placeholder*=Answer]');
if (await answerTextarea.isVisible({ timeout: 3000 })) {
// Current investigation visible before click
await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible();
// Loading overlay absent before click
await expect(page.locator('[data-testid="loading-overlay"][role="status"]')).not.toBeVisible();
await answerTextarea.fill("The figures are comparable.");
await page.getByRole("button", { name: "Update situation" }).click();
// Workspace should disappear during update loading (reasoning mode)
await expect(page.locator('[data-testid="reasoning-workspace"]')).not.toBeVisible({ timeout: 5_000 });
// Current investigation remains visible during update loading
await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible({ timeout: 5_000 });
// Loading overlay should be visible (with role="status" for accessibility)
// Response textarea hidden — replaced by loading overlay
const rwTextarea = page.locator('#rw-answer');
await expect(rwTextarea).not.toBeVisible({ timeout: 3_000 });
// Loading overlay visible in response panel position
const overlay = page.locator('[data-testid="loading-overlay"][role="status"]');
await expect(overlay).toBeVisible({ timeout: 5_000 });
// Status text should indicate reasoning mode (update-specific copy)
const statusText = overlay.getByRole("status");
await expect(statusText).toHaveText(/Working through your situation/);
await expect(overlay.getByText(/Working through your situation/)).toHaveCount(1);
// Current understanding card remains visible (supporting context preserved)
const cuHeading = page.getByRole("heading", { name: "Current understanding" });
if (await cuHeading.isVisible({ timeout: 2_000 })) {
await expect(cuHeading).toBeVisible();
}
// After loading completes, workspace returns with updated question
await waitForWorkspaceReady(page);
await expect(workspace).toBeVisible();
await expect(rwTextarea).toBeVisible();
await expect(overlay).not.toBeVisible({ timeout: 3_000 });
}
});