feat: Phase 2-5 UX enhancements — recovery cards, session persistence, summary panel, contract backlog
Phase 2: Recovery state components (ProviderUnavailableCard, MalformedResponseCard, UnexpectedStateCard, ContinueLaterBanner) with automatic error detection for provider/network/malformed/unexpected states. Phase 3: Session persistence via sessionStorage — save after each successful turn, restore on mount, clear on restart/reset. Continuelater banner shown when session is restored. Phase 4: InvestigationSummaryPanel component displaying current status, understanding summary, questions answered/remaining, investigation timestamps. Phase 5: docs/reasoning-contract-backlog.md documenting all mocked fields (60+ rows across 7 categories) with feature/UI need/mock/desired output/stage/notes columns. Also: wired onRestart through ReasoningWorkspace → ScenarioForm, fixed getErrorType scope issues, removed broken window.__restartInvestigation.
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useState, useRef, useMemo } from "react";
|
||||
import DiagnosticsView from "@/components/diagnostics-view";
|
||||
import ReasoningWorkspace, { LoadingOverlay } from "@/components/reasoning-workspace";
|
||||
import { mockFetch } from "@/lib/mocks/confidence-engine/mock-client";
|
||||
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
|
||||
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
|
||||
|
||||
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
|
||||
const MOCK_ENABLED = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS === "true";
|
||||
@@ -186,6 +186,27 @@ export function UpdateErrorPanel({ updateError }) {
|
||||
|
||||
export { INITIAL_MESSAGES, UPDATE_MESSAGES, useLoadingStatus };
|
||||
|
||||
// ── Session key ────────────────────────────────────────────────
|
||||
const SESSION_KEY = "confidence-engine-session";
|
||||
|
||||
function getSession() {
|
||||
if (typeof sessionStorage === "undefined") return null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem(SESSION_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
function saveSession(state) {
|
||||
if (typeof sessionStorage === "undefined") return;
|
||||
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(state)); } catch (_) {}
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
if (typeof sessionStorage === "undefined") return;
|
||||
try { sessionStorage.removeItem(SESSION_KEY); } catch (_) {}
|
||||
}
|
||||
|
||||
export default function ScenarioForm() {
|
||||
const [scenario, setScenario] = useState("");
|
||||
const [status, setStatus] = useState("idle"); // idle | loading | error | success
|
||||
@@ -196,11 +217,52 @@ export default function ScenarioForm() {
|
||||
const [updateResult, setUpdateResult] = useState(null);
|
||||
const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState("");
|
||||
const [currentUnderstanding, setCurrentUnderstanding] = useState(null);
|
||||
const [mockScenario, setMockScenario] = useState("");
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
/* Restore persisted session on mount (Phase 3) ─────────── */
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = getSession();
|
||||
if (!saved) return;
|
||||
setScenario(saved.scenario || "");
|
||||
setResult(saved.situationGraph ? { ...saved, situationGraph: saved.situationGraph } : null);
|
||||
setCurrentUnderstanding(saved.summary || null);
|
||||
setStatus("success");
|
||||
}, []);
|
||||
|
||||
/* Inject mock globals so the interceptor can read them at runtime */
|
||||
useMockGlobals();
|
||||
|
||||
function handleScenarioSelect(key) {
|
||||
setMockScenario(key);
|
||||
if (typeof window !== "undefined") {
|
||||
window.__MOCK_SCENARIO = key;
|
||||
}
|
||||
// Auto-fill central statement for quick start
|
||||
var found = AVAILABLE_SCENARIOS.find(function(s) { return s.key === key; });
|
||||
if (found && found.centralStatement) {
|
||||
setScenario(found.centralStatement);
|
||||
}
|
||||
}
|
||||
|
||||
function handleScenarioFill(key) {
|
||||
handleScenarioSelect(key);
|
||||
setStatus("loading");
|
||||
setResult(null);
|
||||
setAnswer("");
|
||||
setUpdateStatus("idle");
|
||||
setUpdateResult(null);
|
||||
setLastSubmittedAnswer("");
|
||||
setCurrentUnderstanding(null);
|
||||
setUpdateError(null);
|
||||
// Simulate a click on the analyse button after auto-filling
|
||||
setTimeout(function() {
|
||||
var btn = document.querySelector('button[type="submit"]');
|
||||
if (btn && !btn.disabled) btn.click();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
|
||||
INITIAL_MESSAGES,
|
||||
status === "loading"
|
||||
@@ -230,7 +292,9 @@ export default function ScenarioForm() {
|
||||
if (res.ok && data.success) {
|
||||
setStatus("success");
|
||||
setCurrentUnderstanding(data.summary ?? null);
|
||||
setResult(normaliseStartResult(data));
|
||||
const normalised = normaliseStartResult(data);
|
||||
setResult(normalised);
|
||||
saveSession({ scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString() });
|
||||
} else {
|
||||
setStatus("error");
|
||||
setCurrentUnderstanding(data.summary ?? null);
|
||||
@@ -292,6 +356,8 @@ export default function ScenarioForm() {
|
||||
diagnostics: outcome.diagnostics,
|
||||
}));
|
||||
setAnswer("");
|
||||
// Persist after successful update turn
|
||||
saveSession({ scenario, situationGraph: outcome.updatedSituationGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: outcome.summary ?? currentUnderstanding, updatedAt: new Date().toISOString() });
|
||||
} else {
|
||||
setUpdateStatus("error");
|
||||
setUpdateError(outcome);
|
||||
@@ -329,6 +395,43 @@ export default function ScenarioForm() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{status === "idle" && MOCK_ENABLED && (
|
||||
<details className="rounded-lg border border-gray-200 bg-gray-50">
|
||||
<summary className="cursor-pointer px-4 py-2 text-sm font-medium text-gray-600">Developer details</summary>
|
||||
<div className="space-y-3 px-4 pb-4">
|
||||
<div>
|
||||
<label htmlFor="mock-scenario" className="block text-xs font-medium text-gray-500 mb-1">Mock scenario</label>
|
||||
<select
|
||||
id="mock-scenario"
|
||||
value={mockScenario}
|
||||
onChange={(e) => handleScenarioSelect(e.target.value)}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
>
|
||||
<option value="">— default (env var) —</option>
|
||||
{AVAILABLE_SCENARIOS.map(function(s) {
|
||||
return <option key={s.key} value={s.key}>{s.label}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{AVAILABLE_SCENARIOS.map(function(s) {
|
||||
return (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
onClick={() => handleScenarioFill(s.key)}
|
||||
disabled={!scenario.trim() && scenario !== s.centralStatement}
|
||||
className="rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 hover:bg-gray-100 disabled:opacity-30"
|
||||
>
|
||||
{s.label} ({s.turnCount} turns)
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* ── Initial analysis loading card ─────────────── */}
|
||||
{status === "loading" && (
|
||||
<LoadingOverlay
|
||||
@@ -358,14 +461,31 @@ export default function ScenarioForm() {
|
||||
setAnswer={setAnswer}
|
||||
onAnswerSubmit={handleUpdate}
|
||||
lastSubmittedAnswer={lastSubmittedAnswer}
|
||||
onRestart={() => {
|
||||
clearSession();
|
||||
setStatus("idle");
|
||||
setResult(null);
|
||||
setAnswer("");
|
||||
setUpdateStatus("idle");
|
||||
setUpdateResult(null);
|
||||
setLastSubmittedAnswer("");
|
||||
setCurrentUnderstanding(null);
|
||||
setUpdateError(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Continue later banner when session was restored ── */}
|
||||
{status === "success" && result?.updatedAt && (
|
||||
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
|
||||
)}
|
||||
|
||||
{/* Reset button after successful analysis */}
|
||||
{status === "success" && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearSession();
|
||||
setScenario("");
|
||||
setStatus("idle");
|
||||
setResult(null);
|
||||
|
||||
Reference in New Issue
Block a user