"use client"; import React, { useEffect } from "react"; import { useState, useRef, useMemo } from "react"; import DiagnosticsView from "@/components/diagnostics-view"; import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace"; import ExperimentalBranchSwitcher, { PulseStyle } from "@/components/experimental/branch-switcher"; import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client"; import { useBranchScopedFixture } from "@/lib/fixtures/rto26b-branch-scoped.mjs"; /* 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"; /* ── inject runtime globals for the mock client to read ──── */ function useMockGlobals() { useEffect(() => { if (MOCK_ENABLED) { var w = window; w.__MOCK_ENABLED = true; w.__MOCK_DELAY = process.env.NEXT_PUBLIC_CONFIDENCE_MOCK_DELAY || "normal"; w.__MOCK_SCENARIO = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCK_SCENARIO || ""; } }, []); } const MAX_LENGTH = 10000; export async function submitScenarioForStartCase(fetchImpl, scenario) { return fetchImpl("/api/cases/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scenario }), }); } export async function submitAnswerForUpdateCase( fetchImpl, { situationGraph, previousQuestion, answer }, ) { if (!answer?.trim()) { return { ok: false, skipped: true, data: { success: false, stage: "request_validation", error: "Please enter an answer before updating.", }, }; } const response = await fetchImpl("/api/cases/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ situationGraph, previousQuestion, answer }), }); return { ok: response.ok, skipped: false, data: await response.json(), }; } function normaliseStartResult(data) { return { ...data, selectedQuestion: typeof data?.selectedQuestion === "string" ? data.selectedQuestion : data?.selectedQuestion?.question ?? null, newlySurfacedNodeIds: data?.newlySurfacedNodeIds ?? [], }; } function normaliseUpdateSelectedQuestion(selectedQuestion) { if (!selectedQuestion) return null; if (typeof selectedQuestion === "string") return selectedQuestion; return selectedQuestion.question ?? null; } export function ScenarioResultPanels({ status, result }) { if (!result) return null; const hasGraph = Boolean(result.situationGraph); const hasQuestion = Boolean(result.selectedQuestion?.question); const hasDiagnostics = Boolean(result.diagnostics); return ( <> {status === "error" && (
{result.error && (
Error: {result.error}
)} {!hasGraph && !hasQuestion && (
Validation failed — no structured graph output was produced.
)}
)} {hasDiagnostics && } ); } // ── Message pools ─────────────────────────────────────────── const INITIAL_MESSAGES = [ { min: 0, text: "Reading your situation" }, { min: 10, text: "Building a structured understanding" }, { min: 25, text: "Identifying what is known and still unclear" }, { min: 45, text: "Selecting the next useful question" }, ]; const UPDATE_MESSAGES = [ { min: 0, text: "Considering your answer" }, { min: 10, text: "Updating the situation" }, { min: 25, text: "Checking what changed" }, { min: 45, text: "Choosing the next question" }, ]; function useLoadingStatus(messages, isLoading) { const [elapsed, setElapsed] = useState(0); const startRef = useRef(null); useEffect(() => { if (isLoading) { startRef.current = Date.now(); const iv = setInterval(() => { setElapsed(Math.floor((Date.now() - startRef.current) / 1000)); }, 1000); return () => clearInterval(iv); } else { setElapsed(0); startRef.current = null; } }, [isLoading]); const currentMessage = useMemo(() => { if (!messages || messages.length === 0) return ""; let msg = messages[0].text; for (const m of messages) { if (elapsed >= m.min) msg = m.text; } return msg; }, [messages, elapsed]); return { elapsed, currentMessage }; } export function UpdateErrorPanel({ updateError }) { if (!updateError) return null; const errors = [ ...(updateError.errors || []), ...(updateError.validationErrors || []), ...(updateError.graphValidationErrors || []), ...(updateError.proposalErrors || []), ...(updateError.providerErrors || []), ]; return (
Update error: {updateError.error}
{errors.length > 0 && (
Update details ({errors.length})
    {errors.map((item, index) => (
  • {typeof item === "string" ? item : item?.message || JSON.stringify(item)}
  • ))}
)}
); } 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 (_) {} } /** * Derives whether the current component state represents a valid investigation * context sufficient to render a workspace surface. * * Valid only when: * - result carries a situationGraph (renderable graph), OR * - status is "success" AND there is a non-empty scenario * (from session restoration with real data). * * This predicate is the single source of truth for all render-gate decisions. * showExperimentView, fixture availability, or sessionStorage keys alone are * NOT sufficient to constitute valid context. */ export function hasValidInvestigationContext(result, status, scenario) { return Boolean(result?.situationGraph) || (status === "success" && Boolean(scenario?.trim())); } /** * Derives the primary surface that must render for the given state tuple. * Enforces exactly-one-primary-surface invariant: no zero, no two. */ export function derivePrimarySurface(result, status, showExperimentView, scenario, activeBranchId) { if (status === "loading") return "LOADING"; if (status === "error") return "ERROR_SURFACE"; const valid = hasValidInvestigationContext(result, status, scenario); // RTO.28B: show question-selection surface when investigation exists but no question/branch is active if (showExperimentView && valid && !activeBranchId) return "QUESTION_SELECTION"; if (showExperimentView && valid) return "EXPERIMENT_NOTEBOOK"; if (!showExperimentView && valid) return "NORMAL_WORKSPACE"; if (!showExperimentView) return "SCENARIO_ENTRY"; // showExperimentView === true but no valid context → fall back to entry return "SCENARIO_ENTRY"; } export default function ScenarioForm() { const [scenario, setScenario] = useState(""); const [status, setStatus] = useState("idle"); // idle | loading | error | success const [result, setResult] = useState(null); const [answer, setAnswer] = useState(""); const [updateStatus, setUpdateStatus] = useState("idle"); // idle | loading | error | success const [updateError, setUpdateError] = useState(null); const [updateResult, setUpdateResult] = useState(null); const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState(""); const [currentUnderstanding, setCurrentUnderstanding] = useState(null); const [mockScenario, setMockScenario] = useState(""); const [hideFacilitatorOnLanding, setHideFacilitatorOnLanding] = useState(false); const textareaRef = useRef(null); /* ── RTO.25A — passive late-result branch switcher (experimental) ── */ // RTO.28A: no active branch until the user explicitly chooses one. const [activeBranchId, setActiveBranchId] = useState(null); // Pre-seed Competitor development with a late result for RTO.27A testing const [branchNewResults, setBranchNewResults] = useState({ "branch-a": true }); /* ── RTO.27B — provisional done-for-now state (experimental) ── */ const [doneForNowBranchIds, setDoneForNowBranchIds] = useState([]); /* ── RTO.26B — experimental branch-scoped reasoning fixture ───── */ const branchScoped = useBranchScopedFixture(); // Toggle to control when the experiment view is visible vs production // Always start false for SSR-safe deterministic first render. // sessionStorage reads are deferred to useEffect (after mount). const [showExperimentView, setShowExperimentView] = useState(false); // Use fixture branches as the authoritative source when available const BRANCHES = useMemo(() => branchScoped.getBranches(), [branchScoped]); // Track the originating question for provenance const [originQuestion, setOriginQuestion] = useState(null); // Extract inferred questions from the fixture (flat list for post-Analyse surface) const inferredQuestions = useMemo(() => { if (!branchScoped || typeof branchScoped.getAllInferredQuestions !== 'function') return []; return branchScoped.getAllInferredQuestions(); }, [branchScoped]); // Simulate a late semantic result arriving on Branch A ~2s after a graph loads useEffect(() => { if (status !== "success" && status !== "error") return; if (branchNewResults["branch-a"]) return; const timer = setTimeout(() => { setBranchNewResults((prev) => ({ ...prev, "branch-a": true })); }, 2000); return () => clearTimeout(timer); }, [status, branchNewResults]); // Compute branch-local data for the active branch (explicit provenance) const activeBranch = BRANCHES.find(b => b.id === activeBranchId); const experimentalBranchQuestions = useMemo(() => { if (!activeBranch) return []; return branchScoped.getBranchQuestions(activeBranch.id); }, [activeBranch, branchScoped]); const experimentalBranchContributions = useMemo(() => { if (!activeBranch) return []; return branchScoped.getBranchContributions(activeBranch.id); }, [activeBranch, branchScoped]); // RTO.27A — late results for active branch const experimentalBranchLateResults = useMemo(() => { if (!activeBranch) return []; return (branchScoped.getBranchLateResults?.(activeBranch.id) || []).map(lr => ({ text: lr.text })); }, [activeBranch, branchScoped]); // Determine which non-active branches have new results for passive indicator // (RTO.27B: also include done-for-now branches so pause state is visible) const inactiveBranchNewResults = useMemo(() => { const result = {}; BRANCHES.forEach(b => { if (b.id !== activeBranchId) { if (b.id in branchNewResults) { result[b.id] = true; } // Show pause indicator on any done-for-now branch if (doneForNowBranchIds.includes(b.id)) { result[b.id] = true; } } }); return result; }, [activeBranchId, BRANCHES, branchNewResults, doneForNowBranchIds]); /* ── Valid investigation predicate ─────────────────────── */ // Delegated to the exported utility below. const validCtx = hasValidInvestigationContext(result, status, scenario); /* Restore persisted session on mount (Phase 3) ─────────── */ useEffect(() => { if (typeof window === "undefined") return; const saved = getSession(); if (!saved) return; const hasGraph = Boolean(saved.situationGraph); setScenario(saved.scenario || ""); setResult(hasGraph ? { ...saved, situationGraph: saved.situationGraph } : null); setCurrentUnderstanding(saved.summary || null); // Partial sessions (present but no graph) must NOT suppress the // scenario-entry form. Only promote to success when there is actual // investigation data to render. if (hasGraph) { setStatus("success"); setShowExperimentView(true); } }, []); /* Restore experiment view preference from storage (after hydration) ─ */ useEffect(() => { if (typeof window === "undefined") return; // Only promote to experiment mode when there is real investigation // data to render. The ce-show-experiment flag is a presentation // preference, not proof that an investigation exists. if (!validCtx) return; const savedExp = sessionStorage?.getItem("ce-show-experiment"); if (savedExp === "true") { setShowExperimentView(true); return; } // Also enable experiment mode if session data provides a graph // (covers the pre-restoration case where scenario was typed but not yet submitted). const session = getSession(); if (session?.situationGraph) { setShowExperimentView(true); } }, [validCtx]); /* Restore facilitator dismiss preference (Experiment 05) ─── */ useEffect(() => { if (typeof window === "undefined") return; try { const pref = sessionStorage.getItem("ce-facilitator-dismissed"); setHideFacilitatorOnLanding(pref === "true"); } catch (_) {} }, []); /* 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" ); const { elapsed: updateElapsed, currentMessage: updateMsg } = useLoadingStatus( UPDATE_MESSAGES, updateStatus === "loading" ); const handleSubmit = async (e) => { e.preventDefault(); setStatus("loading"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setUpdateResult(null); setLastSubmittedAnswer(""); setUpdateError(null); setCurrentUnderstanding(null); // Force a DOM flush so loading state renders before awaiting (prevents instant mocks from swallowing it) await new Promise(r => requestAnimationFrame(() => setTimeout(r, 50))); try { const res = await submitScenarioForStartCase(MOCK_ENABLED ? mockFetch : fetch, scenario); const data = await res.json(); if (res.ok && data.success) { setStatus("success"); setCurrentUnderstanding(data.summary ?? null); 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); setResult(normaliseStartResult(data)); } } catch (err) { setStatus("error"); setResult({ error: err.message || "Network request failed" }); } }; const handleUpdate = async (e) => { e.preventDefault(); // Guard empty answer before showing loading state if (!answer?.trim()) { setUpdateStatus("error"); setUpdateError({ error: "Please enter an answer before updating." }); return; } setUpdateStatus("loading"); setUpdateError(null); setLastSubmittedAnswer(answer.trim()); // Force a DOM flush so loading state renders before awaiting (prevents instant mocks from swallowing it) await new Promise(r => requestAnimationFrame(() => setTimeout(r, 50))); const submission = await submitAnswerForUpdateCase(MOCK_ENABLED ? mockFetch : fetch, { situationGraph: result?.situationGraph, previousQuestion: result?.selectedQuestion, answer, }); if (submission.skipped) { setUpdateStatus("error"); setUpdateError(submission.data); return; } try { const outcome = submission.data; if (submission.ok && outcome.success) { setUpdateStatus("success"); setCurrentUnderstanding( outcome.summary ? outcome.summary : currentUnderstanding, ); setUpdateResult({ ...outcome, previousSituationGraph: result?.situationGraph ?? null, }); setResult((current) => ({ ...current, situationGraph: outcome.updatedSituationGraph, selectedQuestion: normaliseUpdateSelectedQuestion( outcome.selectedQuestion, ), newlySurfacedNodeIds: (outcome.proposal?.addedNodes || []) .filter((node) => node.kind === "unknown") .map((node) => node.id), 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); } } catch (err) { setUpdateStatus("error"); setUpdateError({ error: err.message || "Network request failed" }); } }; return (
{/* ── RTO.26B — standalone branch-scoped experimental view (shown when experiment is active) ───── */} {showExperimentView && validCtx && status !== "loading" && ( <> {!activeBranchId ? ( /* RTO.28B: inferred questions surface — user chooses what to investigate */
{/* Situation remains visible */} {currentUnderstanding && (

Situation

{currentUnderstanding}

)}
{/* Inferred questions — user chooses what to investigate */}

Questions to explore

Click a question to start investigating. A branch will be created for your choice.

{inferredQuestions.map((q) => ( ))} {inferredQuestions.length === 0 && (

No questions available yet.

)}
) : ( /* Has active branch — show notebook */
{/* Workspace (3/4) — branch-scoped fixture only, no API needed */}
{(() => { const activeBranch = BRANCHES.find(b => b.id === activeBranchId); const branchContext = activeBranch ? { label: activeBranch.label, origin: activeBranch.origin } : null; return ( {}} onAnswerSubmit={async (e) => e.preventDefault()} lastSubmittedAnswer="" branchContext={{ ...branchContext, originQuestion: originQuestion }} experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined} branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined} branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined} branchLocalLateResults={experimentalBranchLateResults.length > 0 ? experimentalBranchLateResults : undefined} inactiveBranchNewResults={Object.keys(inactiveBranchNewResults).length > 0 ? inactiveBranchNewResults : undefined} activeBranchIdForNotebook={activeBranchId} doneForNowBranchIds={doneForNowBranchIds} onDoneForNow={() => { if (activeBranchId && !doneForNowBranchIds.includes(activeBranchId)) { setDoneForNowBranchIds(prev => [...prev, activeBranchId]); } }} onReopenBranch={(id) => { setDoneForNowBranchIds(prev => prev.filter(x => x !== id)); setActiveBranchId(id); }} onRestart={() => { setStatus("idle"); setResult(null); setScenario(""); }} /> ); })()}
{/* Branch switcher (1/4 sidebar) */}
{ if (id === activeBranchId) return; // Reopen: if the selected branch is paused, clear its pause state if (doneForNowBranchIds.includes(id)) { setDoneForNowBranchIds(prev => prev.filter(x => x !== id)); } setActiveBranchId(id); }} />
)} )} {/* ── Experiment toggle (visible when experiment is NOT shown) ─ */} {!showExperimentView && status !== "loading" && !result?.updatedSituationGraph && (

Production view active.

)} {/* ── Idle form for scenario input (shown only when experiment is off) ─ */} {!showExperimentView && !result?.situationGraph && status === "idle" && (
{/* Two-column landing workspace */}
{/* Left panel — Facilitator (1/3 on desktop) */} {!hideFacilitatorOnLanding && (

Before we begin

The Confidence Engine helps build confidence by understanding situations before deciding what to do.

You do not need to know exactly what the problem is.

Simply describe what you have observed. We will work through it together, one question at a time.

{ if (e.target.checked) { setHideFacilitatorOnLanding(true); try { sessionStorage.setItem("ce-facilitator-dismissed", "true"); } catch (_) {} } }} className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
)} {/* Right panel — Workspace (2/3 on desktop) */}

What's the situation