"use client";
import React, { useState, useRef, useEffect, useMemo } from "react";
import DiagnosticsView from "@/components/diagnostics-view";
import GraphUpdateView from "@/components/graph-update-view";
import SituationGraphView from "@/components/situation-graph-view";
import InvestigationSummaryPanel from "@/components/investigation-summary-panel";
import InvestigationSummaryPanelV2 from "@/components/investigation-summary-panel-v2";
import InvestigationSummaryPanelV3 from "@/components/investigation-summary-panel-v3";
import InvestigationMap from "@/components/investigation-map";
// ── Technical summary detector (main view filters these) ───
const TECHNICAL_PATTERNS = [
/nodes?\s*[:\d]/i,
/edges?\s*[:\d]/i,
/\b(?:unknown|observation|conclusion)\b\s/i,
/\bsorted\b/i,
/by_kind/i,
/\b(?:node|edge|unknown|state)\s+count/i,
];
function isTechnicalSummary(summary) {
if (!summary || typeof summary !== "string") return false;
const trimmed = summary.trim();
if (!trimmed) return false;
for (const p of TECHNICAL_PATTERNS) {
if (p.test(trimmed)) return true;
}
return false;
}
// ── Recovery state components (Phase 2) ───────────────────────
function ProviderUnavailableCard({ onRestart }) {
return (
Provider unavailable
The reasoning service could not be reached. This is usually temporary — check that the local model is running and try again.
{onRestart && (
Restart investigation
)}
);
}
function MalformedResponseCard({ onRestart }) {
return (
Unexpected response
The reasoning service returned a response we could not interpret. This may indicate a temporary issue with the model output format.
{onRestart && (
Try again
)}
);
}
function UnexpectedStateCard({ stateName, onRetry, onRestart }) {
return (
Unexpected state
{stateName ? `The system is in an unexpected state (${stateName}).` : "An unexpected internal error occurred."}
Please restart the investigation to continue.
{onRetry && (
Retry update
)}
{onRestart && (
Restart investigation
)}
);
}
function ContinueLaterBanner({ onRestart }) {
return (
Your previous investigation state is still saved. You can continue where you left off or start fresh.
{onRestart && (
Restart investigation
)}
);
}
// ── Session persistence hook (Phase 3) ────────────────────────
function useSessionPersistence() {
const [sessionReady, setSessionReady] = useState(false);
const sessionKey = "confidence-engine-session";
function saveSession(state) {
if (typeof sessionStorage === "undefined") return;
try {
sessionStorage.setItem(sessionKey, JSON.stringify(state));
} catch (_) { /* quota or disabled — ignore silently */ }
}
function loadSession() {
if (typeof sessionStorage === "undefined") return null;
try {
const raw = sessionStorage.getItem(sessionKey);
return raw ? JSON.parse(raw) : null;
} catch (_) {
return null;
}
}
function clearSession() {
if (typeof sessionStorage === "undefined") return;
try { sessionStorage.removeItem(sessionKey); } catch (_) {}
}
return { saveSession, loadSession, clearSession, sessionReady: true };
}
// ── Shared focused-question body (extracted from OpenQuestionsPanel) ──
function FocusedQuestionBody({
nodeId,
isFocused,
hasCompletedInvestigation: hasCompleted,
focused,
formulationStep,
formulateMsg,
processingStep,
deconstructMsg,
focusedAnswer,
handleDeconstructSubmit,
retryFormulation,
setFocusedAnswer,
setSelectedPresentationItemId,
setFocusedPresentationItemId,
setDoneForNowIds,
setFollowUpQuestion,
focusedContributions,
currentFindings,
onUpdateFindingDisposition,
onUpdateFindingProposition,
}) {
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
const hasResult = Boolean(focused?.result);
// ── Local correction state (FQB-owned, not propagated upward) ─
const [editingFindingId, setEditingFindingId] = useState(null);
const [draft, setDraft] = useState("");
function startEditing(id, proposition) {
setEditingFindingId(id);
setDraft(proposition ?? "");
}
function cancelEditing() {
setEditingFindingId(null);
setDraft("");
}
function saveEditing() {
const trimmed = (draft ?? "").trim();
if (!trimmed || !editingFindingId) {
cancelEditing();
return;
}
onUpdateFindingProposition?.(editingFindingId, trimmed);
cancelEditing();
}
return (
<>
{isFocused && hasContent && (
{focused?.question?.trim() ? (
Question
{focused.question}
) : formulationStep === "active" ? (
{formulateMsg}
) : null}
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (
Your response
)}
{processingStep === "active" &&
{deconstructMsg}
}
{focused?.result && (
<>
{/* Prior accumulated learning (prior turns, current turn excluded — shown above) */}
What this tells us {(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
const isFinding = typeof item === "object" && item !== null && "id" in item;
const disposition = isFinding ? item.userDisposition : null;
const isEditing = isFinding && editingFindingId === item.id;
if (!isFinding) {
return (
{item}
);
}
if (isEditing) {
return (
);
}
return (
{item.proposition}
{onUpdateFindingProposition && (
{ e.stopPropagation(); startEditing(item.id, item.proposition); }} data-testid={`not-quite-${item.id}`} className="mt-[2px] text-[10px] font-medium text-amber-500 underline shrink-0 hover:text-amber-600">Not quite
)}
{isFinding && onUpdateFindingDisposition && (
disposition === "not_relevant" ? (
{ e.stopPropagation(); onUpdateFindingDisposition(item.id, null); }} data-testid={`restore-${item.id}`} className="mt-[2px] text-[10px] font-medium text-teal-600 underline shrink-0 hover:text-teal-700" title="Restore to understanding">restore
) : (
{ e.stopPropagation(); onUpdateFindingDisposition(item.id, "not_relevant"); }} data-testid={`not-relevant-${item.id}`} className="mt-[2px] text-[10px] font-medium text-gray-400 underline shrink-0 hover:text-red-500" title="Remove from understanding">not relevant
)
)}
);
})}
Still unclear {(focused.result.uncertainties || []).map((u, i) => ({u} ))}
Questions this raises
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
{focused.result.possibleFollowUpQuestions.map((q, i) => {
const isCurrentQuestion = q === focused?.question;
return (
{ if (!isCurrentQuestion) { e.stopPropagation(); setFollowUpQuestion(q); } }}
style={{ cursor: isCurrentQuestion ? "default" : "pointer" }}
className={`w-full text-left rounded-lg border px-3 py-2.5 text-sm leading-relaxed transition ${
isCurrentQuestion
? "border-gray-200 bg-gray-100/60 text-gray-400 cursor-default"
: "border-blue-200/60 bg-blue-50/40 text-gray-800 hover:border-blue-300 hover:bg-blue-100/60"
}`}
data-testid="follow-up-question"
>
{q}
{isCurrentQuestion ? " (current question)" : " → pick this question"}
);
})}
) : (
None yet
)}
Assumptions {(focused.result.assumptions || []).map((a, i) => ({a} ))}
Connections {(focused.result.relationships || []).map((r, i) => ({r.from} → {r.to} ({r.type}) ))}
>
)}
{focused?.error && processingStep !== "active" && (
We were unable to process your request right now. Please try again later. { e.stopPropagation(); retryFormulation(); }} className="ml-2 font-medium underline">Retry
)}
)}
>
);
}
// ── Persistent navigation controls (overlay-level, outside content grid) ──
function FocusedWorkspaceNavigation({ nodeId, doneForNow, onBackToOpenQuestions, isDoneForNowActive }) {
const canDoneForNow = Boolean(isDoneForNowActive);
return (
{ e.stopPropagation(); onBackToOpenQuestions?.(); }}
style={{ cursor: "pointer" }}
className="text-sm text-gray-400 underline hover:text-gray-600 transition whitespace-nowrap"
>
Back to open questions
{ e.stopPropagation(); doneForNow?.(); }}
style={{ cursor: canDoneForNow ? "pointer" : "not-allowed" }}
className={`rounded-lg border px-4 py-2 text-sm font-medium transition whitespace-nowrap ${canDoneForNow ? 'border-gray-300 bg-white text-gray-600 hover:bg-gray-50' : 'border-gray-100 bg-gray-50 text-gray-300'}`}
>
Done for now
);
}
// ── Current understanding card ────────────────────────────────
// Evidence-limit text that must not appear inside Current understanding
// when the terminal outcome already communicates that state.
const EVIDENCE_LIMIT_PHRASES = [
"The available evidence has reached its current limit",
"evidence has reached its current limit",
"evidence limit reached",
"has reached its current limit",
];
function resolveCurrentSummary(currentSummary) {
if (!currentSummary || typeof currentSummary !== "string") return null;
const trimmed = currentSummary.trim();
if (!trimmed) return null;
// Filter out technical graph summaries
for (const p of TECHNICAL_PATTERNS) {
if (p.test(trimmed)) return null;
}
// Don't show evidence-limit text in Current understanding when
// the terminal outcome card already communicates that state.
const lower = trimmed.toLowerCase();
for (const phrase of EVIDENCE_LIMIT_PHRASES) {
if (lower.includes(phrase)) return null;
}
return trimmed;
}
// ── Status message pools for loading feedback ────────────────
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 };
}
// ── Spinner component ───────────────────────────────────────
function ActivitySpinner() {
return (
);
}
// ── Current investigation card (prominent hero section) ──────
function CurrentInvestigationCard({ selectedQuestion, graph }) {
if (!selectedQuestion) return null;
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
if (!q) return null;
// Derive meaningful context from the active node only when it adds value
let whyMattersText = null;
if (graph?.activeUnknownNodeId && graph.nodes) {
const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
if (activeNode?.description && activeNode.description !== activeNode.label) {
whyMattersText = activeNode.description;
}
}
return (
Investigation
{q}
{whyMattersText && (
{whyMattersText}
)}
);
}
// ── Outcome helpers ───────────────────────────────────────────
function hasGenuineCompletion(graph) {
if (!graph || !graph.nodes?.length) return false;
const resolvedIds = new Set(graph.resolvedNodeIds || []);
const unresolvedCount = graph.nodes.filter(
(n) => n.kind === "unknown" && n.status !== "resolved" && !resolvedIds.has(n.id),
).length;
if (unresolvedCount > 0) return false;
if (graph.activeUnknownNodeId) {
const active = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
if (active && active.status !== "resolved" && !resolvedIds.has(active.id)) return false;
}
return true;
}
// ── Completion card (terminal state when all unknowns resolved) ─
function CompletionCard({ summary }) {
return (
Investigation complete
The available evidence supports the following understanding.
{summary && (
)}
);
}
// ── Evidence-limit card (terminal state: no next question) ───────
function EvidenceLimitCard({ summary }) {
return (
Current evidence limit reached
{summary && (
)}
Further progress requires additional evidence.
);
}
// ── Prior contribution summary (embedded within FocusedQuestionBody) ───
function PriorContributionsSummary({ nodeId, contributions }) {
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
if (!threadContribs.length) return null;
// Exclude the most recent contribution — it is already shown as the current result above.
const priorContribs = threadContribs.slice(0, -1);
if (!priorContribs.length) return null;
return (
Previous learning
{priorContribs.map((c, idx) => (
Turn {c.sequence || idx + 1} — contribution ({c.observations?.length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
{c.observations?.length ? (
What this tells us
{c.observations.map((o, i) => (
{o}
))}
) : null}
{c.uncertainties?.length ? (
Still unclear
{c.uncertainties.map((u, i) => (
{u}
))}
) : null}
))}
);
}
// ── Standalone previous learning block (for two-column secondary placement) ───
function SecondaryPreviousLearning({ nodeId, contributions }) {
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
if (!threadContribs.length) return null;
// Exclude the most recent contribution — it is already shown as the current result above.
const priorContribs = threadContribs.slice(0, -1);
if (!priorContribs.length) return null;
return (
Previous learning
{priorContribs.map((c, idx) => (
Turn {c.sequence || idx + 1} — contribution ({c.observations?.length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
{c.observations?.length ? (
What this tells us
{c.observations.map((o, i) => (
{o}
))}
) : null}
{c.uncertainties?.length ? (
Still unclear
{c.uncertainties.map((u, i) => (
{u}
))}
) : null}
))}
);
}
// ── Thread contributions badge (standalone — used outside focused body) ───
function ThreadContributionsBadge({ nodeId, contributions }) {
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
if (!threadContribs.length) return null;
// Show most recent contribution summary inline
const latest = threadContribs[threadContribs.length - 1];
const nonEmptyGroups = [];
for (const key of ["observations", "uncertainties", "assumptions", "relationships"]) {
const arr = latest[key];
if (Array.isArray(arr) && arr.length > 0) nonEmptyGroups.push(key);
}
return (
{/* Thread learning indicator — collapsed by default; user can expand to inspect history */}
📝 {threadContribs.length} learned contribution{threadContribs.length !== 1 ? "s" : ""}
{/* All contributions listed in order */}
{threadContribs.map((c, idx) => (
{idx > 0 &&
Contribution #{c.sequence || idx + 1}
}
{/* What this tells us */}
{c.observations?.length ? (
What this tells us
{c.observations.map((o, i) => (
{o}
))}
) : null}
{/* Still unclear */}
{c.uncertainties?.length ? (
Still unclear
{c.uncertainties.map((u, i) => (
{u}
))}
) : null}
{/* Assumptions */}
{c.assumptions?.length ? (
Assumptions
{c.assumptions.map((a, i) => (
{a}
))}
) : null}
{/* Connections */}
{c.relationships?.length ? (
Connections
{c.relationships.map((r, i) => (
{r.from} → {r.to} ({r.type})
))}
) : null}
))}
);
}
// ── Quiet facilitator state (experiment mode) ─────────────────────
function QuietStateCard() {
return (
Nothing more to add here right now
You can continue with another question, switch branches, or return when
you have more information.
);
}
// ── New connection / late-result indicator ────────────────────────
function UpdatedConnectionCard({ update }) {
return (
);
}
// ── Branch notebook content (RTO.27A) ────────────────────────────
// Smallest useful branch-scoped composition from fixture data.
// Only renders sections grounded in existing records.
function BranchNotebookContent({
branchContext,
contributions,
openQuestions,
lateResults,
inactiveBranchNewResults,
doneForNowBranchIds,
onDoneForNow,
branchId,
}) {
const hasOpenItems = openQuestions && openQuestions.length > 0;
const hasContributions = contributions && contributions.length > 0;
const isPaused = doneForNowBranchIds?.includes(branchId) || false;
const isBranchActive = !isPaused;
return (
{/* Why this branch exists — origin question is primary */}
{branchContext && (
Investigating
{branchContext.originQuestion ? (
<>
{branchContext.originQuestion.text}
{branchContext.label && branchContext.originQuestion.text !== branchContext.label && (
Branch: {branchContext.label}
)}
>
) : branchContext.origin ? (
<>
{branchContext.label}
Trying to understand: {branchContext.origin}
>
) : (
{branchContext.label}
)}
)}
{/* What we know here */}
{hasContributions && (
What we know here
{contributions.map((c, i) => (
))}
)}
{/* Still uncertain here */}
{hasOpenItems && (
Still uncertain / open questions
{openQuestions.map((q, i) => (
))}
)}
{/* New connection / updates (active branch) */}
{lateResults && lateResults.length > 0 && (
)}
{/* Done for now control — only when branch is not already paused */}
{isBranchActive && onDoneForNow && contributions.length > 0 && (
{ e.stopPropagation(); onDoneForNow(); }}
style={{ cursor: "pointer" }}
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 transition whitespace-nowrap"
>
Done for now
)}
{/* Quiet state */}
{!hasOpenItems && !lateResults?.length && (
{isPaused ? (
<>
Nothing more to add here right now
This branch is paused. Reasoning preserved — return when you have more information.
>
) : (
)}
)}
);
}
// ── Question row (renders clickable card with optional focused content) ─
function QuestionRow({ question }) {
const [isFocused, setIsFocused] = useState(false);
if (!isFocused) {
return (
setIsFocused(true)}
style={{ cursor: "pointer" }}
className="w-full text-left rounded-lg border border-gray-200 px-4 py-3 transition hover:border-gray-300 hover:bg-white"
>
{question.label}
);
}
return (
{question.label}
We have not explored this yet. Do you want to work through it?
{
e.stopPropagation();
// Placeholder — production would call API for focused investigation
}}
style={{ cursor: "pointer" }}
className="rounded-lg border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50 transition"
>
Work through this
setIsFocused(false)}
style={{ cursor: "pointer" }}
className="text-xs text-gray-400 underline hover:text-gray-600 transition"
>
Back to open questions
);
}
// ── Current understanding card ────────────────────────────────
function CurrentUnderstandingCard({ currentSummary, plainLanguage }) {
if (plainLanguage) return ;
const summary = resolveCurrentSummary(currentSummary);
if (!summary) return null;
return (
);
}
// ── Plain-language understanding card (from pipeline summary) ──
function PlainLanguageCard({ summary }) {
if (!summary) return null;
return (
);
}
// ── Investigation history card (readable notebook style) ──────
function InvestigationHistoryCard({ turn }) {
const isCollapsed = turn._collapsed;
const isAnswered = Boolean(turn.answer?.trim());
const displayedQuestion = turn.question;
return (
{isAnswered && ✓ }
{displayedQuestion}
{turn.answer}
{turn.acknowledgement && (
{turn.acknowledgement}
)}
);
}
// ── Investigation history section ─────────────────────────────
function InvestigationHistory({ turns }) {
if (!turns || turns.length === 0) return null;
const latestId = turns[turns.length - 1].id;
return (
History
{turns.map((turn) => (
))}
);
}
// ── Original situation (always-visible reference card) ────────
function OriginalSituation({ scenario, centralStatement }) {
const text = scenario || centralStatement;
if (!text) return null;
return (
);
}
// ── Transient acknowledgement (auto-dismisses after 3s) ─────────
function useAutoDismiss(duration = 3000) {
const [visible, setVisible] = useState(true);
useEffect(() => {
if (!visible) return;
const timer = setTimeout(() => setVisible(false), duration);
return () => clearTimeout(timer);
}, [visible, duration]);
return visible;
}
function UpdateAcknowledgement({ updateResult }) {
const visible = useAutoDismiss(3000);
if (!updateResult || !visible) return null;
const summary = updateResult.summary;
return (
);
}
// ── Developer details disclosure ──────────────────────────────
function DeveloperDetails({ graph, selectedQuestion, diagnostics, newlySurfacedNodeIds, updateResult }) {
return (
Developer details
{graph && (
)}
{updateResult && (
)}
{diagnostics && }
);
}
// ── Loading overlay (for both start and update) ───────────────
function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
if (!isLoading) return null;
const messages = variant === "update" ? UPDATE_MESSAGES : INITIAL_MESSAGES;
let statusText = messages[0].text;
for (const m of messages) {
if (elapsed >= m.min) statusText = m.text;
}
return (
Working through your situation
{statusText}
This has been running for {elapsed}s.
{variant === "initial" && elapsed >= 45 && (
This can take around a minute with the current local model.
)}
);
}
// ── Main workspace component ──────────────────────────────────
function getErrorType(errorStr, stage, hasGraph) {
if (!errorStr && !stage) return null;
const lower = (errorStr || "").toLowerCase();
if (/provider|unavailable|network|timeout/.test(lower)) return "provider-unavailable";
if (/malformed|invalid.*format|parse|structured/.test(lower)) return "malformed-response";
if (stage === "provider") return "provider-error";
if (stage === "unexpected") return "unexpected-state";
if (/validation/.test(lower) && !hasGraph) return "no-graph";
return null;
}
// ── Focused investigation overlay workspace container ──────────
function FocusedInvestigationWorkspace({
nodeId,
focused,
formulationStep,
formulateMsg,
processingStep,
deconstructMsg,
focusedAnswer,
handleDeconstructSubmit,
retryFormulation,
setFocusedAnswer,
setSelectedPresentationItemId,
setFocusedPresentationItemId,
setDoneForNowIds,
setFollowUpQuestion,
setIsFocusedWorkspaceOpen,
hasCompletedInvestigation,
focusedContributions,
currentFindings,
onUpdateFindingDisposition,
onUpdateFindingProposition,
}) {
const hasResult = Boolean(focused?.result);
return (
{/* Two-column responsive grid: primary active work + secondary context */}
{/* ── Primary column: active investigation — min-w-0 prevents flex growth in single-column mode ── */}
{/* ── Secondary context: Previous Learning — visible on all breakpoints, placed in grid column on wide / flows below primary on narrow ── */}
{hasResult && (
)}
);
}
// ── Open questions panel (production, non-experiment mode) ────────
function OpenQuestionsPanel({
graph, selectedPresentationItemId, focusedPresentationItemId, hasFocusedContent,
focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds,
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
setFollowUpQuestion, focusedContributions, focusedInvestigations, setIsFocusedWorkspaceOpen,
onUpdateFindingDisposition,
onUpdateFindingProposition,
}) {
const openNodes = (graph?.nodes || []).filter(
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
);
if (openNodes.length <= 0) return null;
// Check whether a node has a completed focused result stored locally.
const hasCompletedInvestigation = (nid) => {
const inv = focusedInvestigations?.[nid];
return Boolean(inv && inv.status === "formulated" && inv.result && typeof inv.question === "string" && inv.question.trim());
};
function handleNodeClick(node) {
if (hasCompletedInvestigation(node.id) && selectedPresentationItemId === node.id) {
setFocusedPresentationItemId(node.id);
return;
}
setSelectedPresentationItemId(
selectedPresentationItemId === node.id ? null : node.id,
);
}
return (
Open questions
{openNodes.map((node) => {
const isSelected = selectedPresentationItemId === node.id;
const isFocused = focusedPresentationItemId === node.id;
return (
handleNodeClick(node)}
style={{ cursor: "pointer" }}
className="w-full text-left rounded-lg border border-gray-200 px-4 py-3 transition hover:border-gray-300 hover:bg-white"
>
{node.label}
{isSelected && !hasFocusedContent() && (
We have not explored this yet. Do you want to work through it?
{ e.stopPropagation(); startFocused(node.id); }}
style={{ cursor: "pointer" }}
className="rounded-lg border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50 transition"
>
Work through this
)}
hasCompletedInvestigation(node.id)}
focused={focused}
formulationStep={formulationStep}
formulateMsg={formulateMsg}
processingStep={processingStep}
deconstructMsg={deconstructMsg}
focusedAnswer={focusedAnswer}
handleDeconstructSubmit={handleDeconstructSubmit}
retryFormulation={retryFormulation}
setFocusedAnswer={setFocusedAnswer}
setSelectedPresentationItemId={setSelectedPresentationItemId}
setFocusedPresentationItemId={setFocusedPresentationItemId}
setDoneForNowIds={setDoneForNowIds}
setFollowUpQuestion={setFollowUpQuestion}
focusedContributions={focusedContributions}
onUpdateFindingDisposition={onUpdateFindingDisposition}
onUpdateFindingProposition={onUpdateFindingProposition}
/>
{/* Thread contributions for this node */}
);
})}
{doneForNowIds.length > 0 && (
Done for now
{graph.nodes.filter((n) => doneForNowIds.includes(n.id)).map((node) => (
{node.label}
setDoneForNowIds(doneForNowIds.filter(id => id !== node.id))} style={{ cursor: "pointer" }} className="mt-1 rounded border border-gray-300 px-3 py-1 text-xs font-medium text-gray-500 hover:bg-white transition">Reopen
))}
)}
);
}
export default function ReasoningWorkspace({
scenario,
status,
updateStatus,
currentUnderstanding: propUnderstanding,
result,
answer,
setAnswer,
onAnswerSubmit,
lastSubmittedAnswer,
onRestart,
focusedContributions,
onFocusedContribution,
findings,
onUpdateFindingDisposition,
onUpdateFindingProposition,
}) {
const [investigationHistory, setInvestigationHistory] = useState([]);
const turnCounter = useRef(0);
const pendingTurnRef = useRef(null);
// ── Experiment 12: toggle between progress panel versions (temporary experimental UI) ──
const [panelVariant, setPanelVariant] = useState("c");
// ── RTO.01 — presentation-only selection for open investigation items ──
const [selectedPresentationItemId, setSelectedPresentationItemId] = useState(null);
// ── RTO.02 — case / question workspace lifecycle (presentation only) ──
const [focusedPresentationItemId, setFocusedPresentationItemId] = useState(null);
const [doneForNowIds, setDoneForNowIds] = useState([]);
// ── Focused investigation overlay workspace ───────────────
const [isFocusedWorkspaceOpen, setIsFocusedWorkspaceOpen] = useState(false);
// ── RTO.29D — post-Analyse initial reflection surface ─────────
const [initialReflectionActive, setInitialReflectionActive] = useState(false);
const [postAnalyseStatus, setPostAnalyseStatus] = useState(null);
const { saveSession, loadSession } = useSessionPersistence();
// Persist workspace state on every successful update (Phase 3)
useEffect(() => {
if (updateStatus === "success" && result?.situationGraph) {
saveSession({
scenario,
situationGraph: result.situationGraph,
selectedQuestion: result.selectedQuestion,
summary: result.summary || propUnderstanding,
updatedAt: new Date().toISOString(),
focusedContributions,
});
}
}, [updateStatus, result, focusedContributions]);
// Capture the current selected question at submit time (not from a stale ref)
const capturePendingTurn = (selectedQuestion, answerText) => {
if (!selectedQuestion || !answerText?.trim()) return null;
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
if (!q) return null;
turnCounter.current += 1;
return {
id: `turn-${turnCounter.current}`,
question: q,
answer: answerText.trim(),
acknowledgement: null,
};
};
// Append the captured pending turn to history after a successful update only
useEffect(() => {
const pending = pendingTurnRef.current;
if (!pending || updateStatus !== "success") return;
setInvestigationHistory((prev) => [
...prev,
{ ...pending, acknowledgement: result?.summary || null },
]);
pendingTurnRef.current = null;
}, [updateStatus, result]);
const handleUpdateCaptureAndSubmit = async (e) => {
e.preventDefault();
if (!answer?.trim() || !result?.selectedQuestion) return;
pendingTurnRef.current = capturePendingTurn(result.selectedQuestion, answer);
await onAnswerSubmit(e);
};
const graph = result?.situationGraph ?? null;
const hasGraph = Boolean(graph);
const diagnostics = result?.diagnostics ?? null;
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
const genuineCompletion = hasGenuineCompletion(graph);
const errorType = getErrorType(
result?.error || (result?.updateError ? result.updateError.error : null),
result?.stage,
Boolean(graph)
);
const isProviderUnavailable =
errorType === "provider-unavailable" || errorType === "provider-error";
const isMalformedResponse = errorType === "malformed-response";
// ── RTO.13B — focused investigation localized state ───────────
const [formulationStep, setFormulationStep] = useState("idle");
const [processingStep, setProcessingStep] = useState("idle");
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
INITIAL_MESSAGES,
status === "loading"
);
const { elapsed: updateElapsed, currentMessage: updateMsg } = useLoadingStatus(
UPDATE_MESSAGES,
updateStatus === "loading"
);
// ── RTO.13B — focused investigation loading hooks ─────────────
const FORMULATING_MESSAGES = [
{ min: 0, text: "Working out a question…" },
{ min: 15, text: "Still working on that question" },
{ min: 30, text: "Formulating the right question for this" },
{ min: 45, text: "A moment longer — this can take around a minute" },
];
const DECONSTRUCT_MESSAGES = [
{ min: 0, text: "Working through your response…" },
{ min: 15, text: "Still working on that response" },
{ min: 30, text: "A moment longer — this can take around a minute" },
];
const { elapsed: formulateElapsed, currentMessage: formulateMsg } = useLoadingStatus(
FORMULATING_MESSAGES,
formulationStep === "active",
);
const { elapsed: deconstructElapsed, currentMessage: deconstructMsg } = useLoadingStatus(
DECONSTRUCT_MESSAGES,
processingStep === "active",
);
const isUpdating = updateStatus === "loading";
const hasSelectedQuestion = Boolean(result?.selectedQuestion);
// ── RTO.13B — focused investigation localized state (keyed by node ID) ──
const [focusedInvestigations, setFocusedInvestigations] = useState({});
const [focusedAnswer, setFocusedAnswer] = useState("");
// ── Persist focused contributions immediately after deconstruct success ────
const lastPersistedContribCount = useRef(0);
useEffect(() => {
if (!graph) return;
if (focusedContributions.length === lastPersistedContribCount.current) return;
lastPersistedContribCount.current = focusedContributions.length;
saveSession({
scenario,
situationGraph: graph,
selectedQuestion: result?.selectedQuestion,
summary: result?.summary || propUnderstanding,
updatedAt: new Date().toISOString(),
focusedContributions,
});
}, [focusedContributions]);
function getFocusedInvestigation() {
if (!focusedPresentationItemId) return null;
return focusedInvestigations[focusedPresentationItemId] || null;
}
function focusItem(nodeId) {
setSelectedPresentationItemId(nodeId);
setFocusedPresentationItemId(nodeId);
}
const focused = getFocusedInvestigation();
// ── Derive presentation data: exact existing Findings for the current focused Contribution ──
let currentFindings = [];
if (focused?.result?.correlationId && findings) {
const correlationId = focused.result.correlationId;
const matchedContribution = (focusedContributions || []).find(
(c) => c.correlationId === correlationId,
);
if (matchedContribution) {
currentFindings = findings.filter(
(f) => f.contributionId === matchedContribution.id,
);
}
}
// Gate evidence-limit card: do NOT show when active investigation paths remain.
const showEvidenceLimit = !(
processingStep === "active" ||
(focused?.question?.trim() && !processingStep) ||
(hasGraph && !genuineCompletion)
);
// ── RTO.13B — workflow handlers ──────────────────────────────
function startFocused(nodeId) {
const target = nodeId || focusedPresentationItemId;
if (!target) return;
const priorContribs = (focusedContributions || []).filter(
(c) => c.targetNodeId === target,
);
setFocusedPresentationItemId(target);
setFocusedAnswer("");
// Always open the focused workspace overlay
setIsFocusedWorkspaceOpen(true);
if (priorContribs.length > 0) {
// Reopen path: resume from accumulated contribution history.
// Do NOT call doFormulate — the user's prior investigation direction
// is preserved; only follow-up questions surface for explicit selection.
const latest = priorContribs[priorContribs.length - 1];
setFocusedInvestigations((prev) => ({
...prev,
[target]: {
status: "formulated",
question: latest.question || "",
answer: latest.answer ?? null,
result: latest.possibleFollowUpQuestions
? {
observations: latest.observations || [],
uncertainties: latest.uncertainties || [],
assumptions: latest.assumptions || [],
relationships: latest.relationships || [],
possibleFollowUpQuestions: latest.possibleFollowUpQuestions,
}
: null,
error: null,
},
}));
setFormulationStep("idle");
return;
}
// Fresh thread path — unchanged original behaviour.
setFormulationStep("active");
setFocusedInvestigations((prev) => ({
...prev,
[target]: { status: "formulating", question: "", answer: null, result: null, error: null },
}));
doFormulate(target);
}
async function doFormulate(targetNodeId) {
const nodeId = targetNodeId || focusedPresentationItemId;
if (!nodeId || !hasGraph) return;
try {
const url = "/api/focused-investigation/formulate";
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph: graph, targetNodeId: nodeId }),
});
const data = await res.json();
if (!data.success) throw new Error(data.error || "Formulation failed");
// Deterministic presentation anchor: explicitly set the focused item to the
// target node so formulation success always renders correctly.
setFocusedPresentationItemId(nodeId);
setFocusedInvestigations((prev) => ({
...prev,
[nodeId]: { ...prev[nodeId], question: data.question, status: "formulated", error: null },
}));
setFormulationStep("idle");
} catch (err) {
setFocusedInvestigations((prev) => ({
...prev,
[focusedPresentationItemId]: { ...prev[focusedPresentationItemId], question: "", error: err.message || "Formulation failed" },
}));
}
}
async function handleDeconstructSubmit(targetNodeId, answerText) {
if (!hasGraph) return;
const targetNode = graph?.nodes?.find((n) => n.id === targetNodeId);
const centralStmt = graph?.centralStatement || scenario || "";
setProcessingStep("active");
const correlationId = crypto.randomUUID();
try {
const url = "/api/focused-investigation/deconstruct";
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
targetNodeId: targetNodeId,
targetLabel: targetNode?.label || "",
targetDescription: targetNode?.description || "",
centralStatement: centralStmt,
question: focused.question,
answer: answerText,
}),
});
const data = await res.json();
if (!data.success) throw new Error(data.error || "Deconstruction failed");
// Persist contribution to case-level owner (ScenarioForm)
onFocusedContribution?.({
targetNodeId,
targetLabel: targetNode?.label || "",
targetDescription: targetNode?.description || "",
question: focused.question,
answer: answerText,
observations: data.observations,
uncertainties: data.uncertainties,
assumptions: data.assumptions,
relationships: data.relationships,
possibleFollowUpQuestions: data.possibleFollowUpQuestions,
correlationId,
});
setProcessingStep("idle");
// Deterministic presentation anchor: explicitly set the focused item to the
// target node so the post-API success path always renders the correct state
// regardless of render timing or concurrent parent updates.
setFocusedPresentationItemId(targetNodeId);
setFocusedInvestigations((prev) => ({
...prev,
[targetNodeId]: { ...prev[targetNodeId], result: { ...data, correlationId }, answer: answerText, error: null },
}));
} catch (err) {
setProcessingStep("idle");
setFocusedPresentationItemId(targetNodeId);
setFocusedInvestigations((prev) => ({
...prev,
[targetNodeId]: { ...prev[targetNodeId], result: null, error: err.message || "Deconstruction failed" },
}));
}
}
async function submitFocusedAnswer() {
if (!focused?.question?.trim() || !hasGraph) return;
const targetNodeId = focusedPresentationItemId;
await handleDeconstructSubmit(targetNodeId, focusedAnswer);
}
function retryFormulation() {
const target = focusedPresentationItemId;
if (!target) return;
setFocusedInvestigations((prev) => ({
...prev,
[target]: { ...prev[target], question: "", error: null },
}));
doFormulate(target);
}
function setFollowUpQuestion(followUpText) {
const target = focusedPresentationItemId;
if (!target || !followUpText?.trim()) return;
// Deterministic presentation anchor: explicitly set after follow-up selection.
setFocusedPresentationItemId(target);
setFocusedInvestigations((prev) => ({
...prev,
[target]: { ...prev[target], question: followUpText.trim(), answer: null },
}));
setFocusedAnswer("");
}
function hasFocusedContent() {
if (!focused) return false;
const q = focused.question;
return Boolean(q?.trim()) || formulationStep === "active" || processingStep === "active" || focused.error;
}
// ── End RTO.13B ──────────────────────────────────────────────
// ── Body scroll lock while overlay is open ────────────────
useEffect(() => {
if (isFocusedWorkspaceOpen) {
document.body.style.overflow = "hidden";
return () => { document.body.style.overflow = ""; };
}
}, [isFocusedWorkspaceOpen]);
const canAnswer =
status === "success" &&
!isUpdating &&
Boolean(result?.situationGraph) &&
hasSelectedQuestion;
const selectedQ = result?.selectedQuestion ?? null;
// Determine whether the Current Understanding card should render:
// — when there is a durable plain-language understanding, or
// — when there is an actual summary from any graph snapshot, or
// — when the investigation has reached a terminal state with no active question.
const hasCurrentSummaryCondition =
Boolean(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary) || !hasSelectedQuestion;
// ── RTO.29D — manage initial reflection lifecycle ────────────
useEffect(() => {
// Entry is now based on successful initial reconstruction state ONLY.
// selectedQuestion is internal reasoning data — it must NOT gate the
// post-Analyse presentation path. Both null and populated
// selectedQuestion enter Run A identically.
const entering = status === "success" && hasGraph && propUnderstanding;
if (entering) {
setPostAnalyseStatus("success");
setInitialReflectionActive(true);
} else if (!entering && initialReflectionActive) {
// Leave initial reflection when something changes (user interacts, status shifts)
setInitialReflectionActive(false);
setPostAnalyseStatus(null);
}
}, [status, result, hasGraph]);
// Deactivate initial reflection when user submits a turn (leaves post-analyse).
// DO NOT deactivate on formulationStep or processingStep transitions — those
// are part of the focused workflow and must not break Run A's lifecycle.
useEffect(() => {
if (!initialReflectionActive) return;
// If we've already transitioned out of post-analyse mode, bail early.
// This prevents the effect from re-triggering on every state change after
// an explicit deactivation and avoids spurious double-fires when multiple
// dependencies change in the same commit.
if (postAnalyseStatus !== "success") return;
// Genuine exit: a turn has been submitted → history grows past zero.
// No other lifecycle state (formulation, processing, focused IDs, etc.)
// deactivates Run A during a focused investigation cycle.
if (investigationHistory.length > 0) {
setInitialReflectionActive(false);
setPostAnalyseStatus(null);
}
// All other lifecycle events within the focused workflow are intentionally
// ignored here — Run A stays active until an explicit turn is submitted.
}, [initialReflectionActive, investigationHistory, postAnalyseStatus]);
return (
{/* ── Loading overlays ─────────────────────────────── */}
{status === "loading" && (
)}
{/* ── Provider unavailable recovery (always visible) ───────────── */}
{isProviderUnavailable && (
)}
{/* ── Malformed response recovery (always visible) ──────────────── */}
{isMalformedResponse && (
)}
{/* ── Unexpected state recovery ──────────────────── */}
{(errorType === "unexpected-state") && result && (
)}
{/* ── No graph produced after initial analysis ───────── */}
{(status === "success" || status === "error") && !graph ? (
{diagnostics?.noQuestionReason
? "Validation failed — no structured graph output was produced."
: "The analysis completed but did not produce a structured result."}
) : (
<>
{/* ── RTO.29D — initial post-Analyse reflection ──────── */}
{postAnalyseStatus === "success" && (
{/* Current Understanding + Situation — independent vertical flow */}
{/* Current Understanding — prominent orienting surface */}
Current Understanding
{propUnderstanding}
{/* Situation panel during initial reflection */}
{hasGraph && (
)}
{!hasGraph && scenario && (
)}
{/* Initial proposed findings — unknowns + plausible interpretations from reconstruction */}
{(() => {
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
// Open Questions: unresolved unknown nodes only (investigable)
const openUnknowns = (graph?.nodes || []).filter(
(n) =>
n.kind === "unknown" &&
n.status !== "resolved" &&
!resolvedIds.has(n.id),
);
// Possible Interpretations: unresolved assumption nodes only (informational, not investigable)
const possibleInterpretations = (graph?.nodes || []).filter(
(n) =>
n.kind === "assumption" &&
n.status !== "resolved" &&
!resolvedIds.has(n.id),
);
return (
<>
{/* OPEN QUESTIONS — unknown nodes (clickable → focused investigation) */}
{openUnknowns.length > 0 && (
Open Questions
{openUnknowns.map((node) => {
const isFocused = focusedPresentationItemId === node.id;
const hasCompleted = Boolean(
focusedInvestigations[node.id] &&
focusedInvestigations[node.id].question
);
function handleNodeClick(n) {
if (hasCompleted && selectedPresentationItemId === n.id) {
setFocusedPresentationItemId(n.id);
setIsFocusedWorkspaceOpen(true);
return;
}
startFocused(n.id);
}
return (
handleNodeClick(node)}
style={{ cursor: "pointer" }}
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
>
{node.label}
{node.description && node.description !== node.label && (
{node.description}
)}
{!isFocused && Unclear }
);
})}
{/* Focused content — rendered inline in the initial reflection surface (suppressed when overlay open) */}
{!isFocusedWorkspaceOpen && (() => {
const activeForFocus = openUnknowns.find((n) => focusedPresentationItemId === n.id);
if (!activeForFocus) return null;
const node = activeForFocus;
const hasCompleted = Boolean(
focusedInvestigations[node.id] &&
focusedInvestigations[node.id].question
);
const isFocused = focusedPresentationItemId === node.id;
return (
hasCompleted}
focused={getFocusedInvestigation()}
formulationStep={formulationStep}
formulateMsg={formulateMsg}
processingStep={processingStep}
deconstructMsg={deconstructMsg}
focusedAnswer={focusedAnswer}
handleDeconstructSubmit={handleDeconstructSubmit}
retryFormulation={retryFormulation}
setFocusedAnswer={setFocusedAnswer}
setSelectedPresentationItemId={setSelectedPresentationItemId}
setFocusedPresentationItemId={setFocusedPresentationItemId}
setDoneForNowIds={setDoneForNowIds}
setFollowUpQuestion={setFollowUpQuestion}
focusedContributions={focusedContributions}
currentFindings={currentFindings || []}
onUpdateFindingDisposition={onUpdateFindingDisposition}
onUpdateFindingProposition={onUpdateFindingProposition}
/>
);
})()}
)}
{/* POSSIBLE INTERPRETATIONS — assumption nodes (informational, not investigable) */}
{possibleInterpretations.length > 0 && (
Possible Interpretations
{possibleInterpretations.map((node) => (
{node.label}
{node.description && node.description !== node.label && (
{node.description}
)}
Plausible interpretation
))}
)}
>
);
})()}
)}
{/* ── Workspace grid: persistent whenever a graph exists ─── */}
{hasGraph && (
{/* Current Understanding — independent row, full-width of left area (cols 1-2) */}
{propUnderstanding && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
)}
{/* Left column: Investigation + Open Questions (rows 2-3, columns 1-2) */}
{postAnalyseStatus !== "success" && (
)}
{postAnalyseStatus !== "success" && (
)}
{/* Terminal state — suppressed during initial reflection */}
{postAnalyseStatus !== "success" && status === "success" && !hasSelectedQuestion && showEvidenceLimit && (
<>
{genuineCompletion && (
)}
{!genuineCompletion ? (
) : null}
>
)}
{investigationHistory.length > 0 && (
)}
{/* ── Experiment 12: progress panel A / B / C toggle (temporary experimental UI) — de-emphasised by branch-as-context experiment RTO.25D */}
{hasGraph && (
setPanelVariant("a")}
onKeyDown={(e) => {
if (e.key === "ArrowRight") setPanelVariant("b");
if (e.key === "ArrowLeft") setPanelVariant("c");
}}
className={`text-xs transition ${panelVariant === "a" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
>
A
/
setPanelVariant("b")}
onKeyDown={(e) => {
if (e.key === "ArrowRight") setPanelVariant("c");
if (e.key === "ArrowLeft") setPanelVariant("a");
}}
className={`text-xs transition ${panelVariant === "b" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
>
B
/
setPanelVariant("c")}
onKeyDown={(e) => {
if (e.key === "ArrowRight") setPanelVariant("a");
if (e.key === "ArrowLeft") setPanelVariant("b");
}}
className={`text-xs transition ${panelVariant === "c" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
>
C (exp)
{panelVariant === "a"
?
: panelVariant === "b"
?
:
}
)}
{/* Right lane: stable supporting reference (independent column) */}
{(scenario || graph?.centralStatement) && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
{/* Situation — always here when condition met, independent of left column height */}
{/* RTO.25B — temporarily hidden to reduce competing navigation while branch-experiment is active */}
)}
{/* Possible Interpretations — persistent provisional hypothesis cards (spans full workspace width, below Investigation) */}
{(() => {
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
const interpretationNodes = (graph?.nodes || []).filter(
(n) =>
n.kind === "assumption" &&
n.status !== "resolved" &&
!resolvedIds.has(n.id),
);
return postAnalyseStatus !== "success" && interpretationNodes.length > 0 ? (
Possible Interpretations
{interpretationNodes.map((node) => (
{node.label}
{node.description && node.description !== node.label && (
{node.description}
)}
Plausible interpretation
))}
) : null;
})()}
)}
{/* ── Focused investigation overlay workspace ─────────── */}
{isFocusedWorkspaceOpen && (
{ if (e.target === e.currentTarget) setIsFocusedWorkspaceOpen(false); }}
>
{/* Dimmed background — prevents interaction with page behind overlay */}
{/* Workspace container: constrained to viewport height, not pushed by content */}
e.stopPropagation()}
role="dialog"
aria-label="Focused investigation workspace"
>
{/* Floating close control — persistent, top-right inside panel */}
{ e.stopPropagation(); setFocusedAnswer(""); setFocusedPresentationItemId(null); setIsFocusedWorkspaceOpen(false); }}
style={{ cursor: "pointer" }}
aria-label="Close investigation"
title="Close investigation"
className="absolute right-4 top-3 z-20 flex items-center gap-2 rounded-lg border border-gray-300 bg-white/90 px-4 py-2 text-sm font-medium text-gray-600 shadow-sm transition hover:bg-gray-50"
>
Close investigation
{/* Scrollable workspace body */}
{hasFocusedContent() || formulationStep === "active" ? (
<>
{
if (!focused) return false;
const q = focused.question;
return Boolean(q?.trim());
}}
focusedContributions={focusedContributions}
currentFindings={currentFindings || []}
onUpdateFindingDisposition={onUpdateFindingDisposition}
onUpdateFindingProposition={onUpdateFindingProposition}
/>
{/* Workspace navigation — hidden during formulation/loading states */}
{formulationStep !== "active" && (
{
setDoneForNowIds((prev) => [...prev, focusedPresentationItemId]);
setFocusedAnswer("");
setFocusedPresentationItemId(null);
}}
isDoneForNowActive={Boolean(getFocusedInvestigation()?.question?.trim())}
onBackToOpenQuestions={() => {
setFocusedAnswer("");
setFocusedPresentationItemId(null);
}}
/>
)}
>
) : (
Formulating your question…
{formulateMsg}
)}
)}
{/* Developer details — full-width beneath workspace */}
{(status === "success" || status === "error") && graph && (
)}
>
)}
{/* ── Errors (always visible above debug) ─────────── */}
{(status === "error" || updateStatus === "error") && (
{status === "error" && result?.error && (
Error: {result.error}
)}
{updateStatus === "error" && !isProviderUnavailable && !isMalformedResponse && result?.updateError && (
Update error: {result.updateError.error || JSON.stringify(result.updateError)}
)}
)}
);
}
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary, ContinueLaterBanner };