fix: localise update reasoning state
This commit is contained in:
+28
-38
@@ -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:
|
The interface should:
|
||||||
|
|
||||||
- clearly indicate that reasoning is in progress
|
- clearly indicate that reasoning is in progress via the response-panel overlay;
|
||||||
- temporarily suspend the workspace
|
- reassure the user that their answer has been accepted;
|
||||||
- reassure the user that their input has been accepted
|
- avoid displaying partial or changing reasoning outside the response panel.
|
||||||
- avoid displaying partial or changing reasoning
|
|
||||||
|
|
||||||
The workspace is paused until reasoning completes.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -218,37 +235,10 @@ Every submission follows the same lifecycle:
|
|||||||
|
|
||||||
User submits information
|
User submits information
|
||||||
↓
|
↓
|
||||||
Workspace pauses
|
Loading card appears (full-page for initial analysis, localised for updates)
|
||||||
↓
|
↓
|
||||||
Reasoning mode
|
Updated workspace returns
|
||||||
↓
|
|
||||||
Updated workspace appears
|
|
||||||
|
|
||||||
The interaction should be identical whether the submission is:
|
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.
|
||||||
|
|
||||||
- the initial situation
|
|
||||||
- an investigation answer
|
|
||||||
- a future uploaded document
|
|
||||||
- any other evidence
|
|
||||||
|
|
||||||
Users should never wonder whether their input has been accepted or whether the engine is still reasoning.
|
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.
|
|
||||||
|
|||||||
@@ -636,20 +636,13 @@ export default function ReasoningWorkspace({
|
|||||||
variant="initial"
|
variant="initial"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{updateStatus === "loading" && (
|
|
||||||
<LoadingOverlay
|
|
||||||
elapsed={updateElapsed}
|
|
||||||
currentMessage={updateMsg}
|
|
||||||
variant="update"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Provider unavailable recovery ──────────────── */}
|
{/* ── Provider unavailable recovery (always visible) ───────────── */}
|
||||||
{isProviderUnavailable && (
|
{isProviderUnavailable && (
|
||||||
<ProviderUnavailableCard onRestart={onRestart} />
|
<ProviderUnavailableCard onRestart={onRestart} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Malformed response recovery ────────────────── */}
|
{/* ── Malformed response recovery (always visible) ──────────────── */}
|
||||||
{isMalformedResponse && (
|
{isMalformedResponse && (
|
||||||
<MalformedResponseCard onRestart={onRestart} />
|
<MalformedResponseCard onRestart={onRestart} />
|
||||||
)}
|
)}
|
||||||
@@ -675,15 +668,19 @@ export default function ReasoningWorkspace({
|
|||||||
{/* ── Investigation summary card ─────────────── */}
|
{/* ── Investigation summary card ─────────────── */}
|
||||||
<InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
<InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
||||||
|
|
||||||
{/* ── Active investigation: question + form (top priority) ─ */}
|
{/* ── Active investigation context (always visible while question exists) ─ */}
|
||||||
{canAnswer && (
|
{hasSelectedQuestion && (
|
||||||
<>
|
<>
|
||||||
<CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />
|
<CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Post-update acknowledgement */}
|
{/* Post-update acknowledgement — only after success, never during loading */}
|
||||||
{updateStatus === "success" && <UpdateAcknowledgement updateResult={result} />}
|
{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">
|
<form onSubmit={handleUpdateCaptureAndSubmit} className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="rw-answer" className="mb-2 block text-sm font-medium text-gray-700">
|
<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>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs text-gray-400">
|
<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>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
)
|
||||||
|
) : (
|
||||||
|
/* ── Local update loading card (replaces response panel) ─ */}
|
||||||
|
<LoadingOverlay
|
||||||
|
elapsed={updateElapsed}
|
||||||
|
currentMessage={updateMsg}
|
||||||
|
variant="update"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Terminal state: outcome card (no active question) ─ */}
|
{/* ── Terminal state: outcome card — hidden during update loading ─ */}
|
||||||
{status === "success" && !hasSelectedQuestion && graph && genuineCompletion && (
|
{!isUpdating && status === "success" && !hasSelectedQuestion && graph && genuineCompletion && (
|
||||||
<CompletionCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
<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)} />
|
<EvidenceLimitCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -753,7 +757,7 @@ export default function ReasoningWorkspace({
|
|||||||
Error: {result.error}
|
Error: {result.error}
|
||||||
</div>
|
</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">
|
<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)}
|
Update error: {result.updateError.error || JSON.stringify(result.updateError)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export default function ScenarioForm() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Main result workspace ─────────────────────── */}
|
{/* ── Main result workspace ─────────────────────── */}
|
||||||
{(status === "success" || status === "error") && updateStatus !== "loading" && (
|
{(status === "success" || status === "error") && (
|
||||||
<ReasoningWorkspace
|
<ReasoningWorkspace
|
||||||
scenario={scenario}
|
scenario={scenario}
|
||||||
status={status}
|
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 ── */}
|
{/* ── Continue later banner when session was restored ── */}
|
||||||
{status === "success" && result?.updatedAt && (
|
{status === "success" && result?.updatedAt && (
|
||||||
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
|
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
|
||||||
|
|||||||
@@ -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 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
|
// Use normal (700ms) delay so the loading overlay is observable during updates
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
@@ -326,23 +326,45 @@ test("update submission hides workspace, shows reasoning overlay, then restores
|
|||||||
await waitForWorkspaceReady(page);
|
await waitForWorkspaceReady(page);
|
||||||
|
|
||||||
// Workspace visible after initial analysis
|
// 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
|
// Submit an answer to trigger update loading
|
||||||
const answerTextarea = page.locator('textarea[placeholder*=Answer]');
|
const answerTextarea = page.locator('textarea[placeholder*=Answer]');
|
||||||
if (await answerTextarea.isVisible({ timeout: 3000 })) {
|
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 answerTextarea.fill("The figures are comparable.");
|
||||||
await page.getByRole("button", { name: "Update situation" }).click();
|
await page.getByRole("button", { name: "Update situation" }).click();
|
||||||
|
|
||||||
// Workspace should disappear during update loading (reasoning mode)
|
// Current investigation remains visible during update loading
|
||||||
await expect(page.locator('[data-testid="reasoning-workspace"]')).not.toBeVisible({ timeout: 5_000 });
|
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"]');
|
const overlay = page.locator('[data-testid="loading-overlay"][role="status"]');
|
||||||
await expect(overlay).toBeVisible({ timeout: 5_000 });
|
await expect(overlay).toBeVisible({ timeout: 5_000 });
|
||||||
|
|
||||||
// Status text should indicate reasoning mode (update-specific copy)
|
// Status text should indicate reasoning mode (update-specific copy)
|
||||||
const statusText = overlay.getByRole("status");
|
await expect(overlay.getByText(/Working through your situation/)).toHaveCount(1);
|
||||||
await expect(statusText).toHaveText(/Working through your situation/);
|
|
||||||
|
// 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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user