"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 && ( )}
); } 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 && ( )}
); } 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 && ( )} {onRestart && ( )}
); } function ContinueLaterBanner({ onRestart }) { return (

Your previous investigation state is still saved. You can continue where you left off or start fresh.

{onRestart && ( )}
); } // ── 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 }; } // ── 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 && (

{summary}

)}
); } // ── Evidence-limit card (terminal state: no next question) ─────── function EvidenceLimitCard({ summary }) { return (

Current evidence limit reached

{summary && (

{summary}

)}

Further progress requires additional evidence.

); } // ── Per-thread contribution badge (compact indicator) ────────────── 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 (

New update

{update.text}

); } // ── 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) => (

{c.text}

))}
)} {/* 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 && (
)} {/* 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 ( ); } return (

{question.label}

We have not explored this yet. Do you want to work through it?

); } // ── Current understanding card ──────────────────────────────── function CurrentUnderstandingCard({ currentSummary, plainLanguage }) { if (plainLanguage) return ; const summary = resolveCurrentSummary(currentSummary); if (!summary) return null; return (

Understanding

{summary}

); } // ── Plain-language understanding card (from pipeline summary) ── function PlainLanguageCard({ summary }) { if (!summary) return null; return (

Understanding

{summary}

); } // ── 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 (

Situation

{text}

); } // ── 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 (
{summary}
); } // ── 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; } // ── 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, }) { const openNodes = (graph?.nodes || []).filter( (n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id), ); if (openNodes.length <= 1) return null; return (

Open questions

{openNodes.map((node) => { const isSelected = selectedPresentationItemId === node.id; const isFocused = focusedPresentationItemId === node.id; return (
{ if (!focused?.question?.trim() || (focused && !hasFocusedContent())) { setSelectedPresentationItemId( selectedPresentationItemId === node.id ? null : node.id, ); } }} 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?

)} {isFocused && (() => { const focusedNode = graph?.nodes.find((n) => n.id === node.id); return (
{hasFocusedContent() && (
{focused?.question?.trim() ? (

Question

{focused.question}

) : formulationStep === "active" ? (

{formulateMsg}

) : null} {focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (