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

Current investigation

{q}

{whyMattersText && (

Why this matters

{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.

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

Current understanding

{summary}

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

Current understanding

{summary}

); } // ── Investigation history card (readable notebook style) ────── function InvestigationHistoryCard({ turn }) { const isCollapsed = turn._collapsed; const isAnswered = Boolean(turn.answer?.trim()); const displayedQuestion = isCollapsed && turn.question.length > 60 ? `${turn.question.slice(0, 60)}…` : turn.question; return (
{isAnswered && } {displayedQuestion}

Your answer

{turn.answer}

{turn.acknowledgement && ( <>

What changed

{turn.acknowledgement}

)}
); } // ── Investigation history section ───────────────────────────── function InvestigationHistory({ turns }) { if (!turns || turns.length === 0) return null; const latestId = turns[turns.length - 1].id; return (

Investigation history

{turns.map((turn) => ( ))}
); } // ── Original situation (always-visible reference card) ──────── function OriginalSituation({ scenario, centralStatement }) { const text = scenario || centralStatement; if (!text) return null; return (

Original 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; } export default function ReasoningWorkspace({ scenario, status, updateStatus, currentUnderstanding: propUnderstanding, result, answer, setAnswer, onAnswerSubmit, lastSubmittedAnswer, onRestart, }) { const [investigationHistory, setInvestigationHistory] = useState([]); const turnCounter = useRef(0); const pendingTurnRef = useRef(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(), }); } }, [updateStatus, result]); // 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 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"; const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus( INITIAL_MESSAGES, status === "loading" ); const { elapsed: updateElapsed, currentMessage: updateMsg } = useLoadingStatus( UPDATE_MESSAGES, updateStatus === "loading" ); const isUpdating = updateStatus === "loading"; const hasSelectedQuestion = Boolean(result?.selectedQuestion); 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; return (
{/* ── Loading overlays ─────────────────────────────── */} {status === "loading" && ( )} {updateStatus === "loading" && ( )} {/* ── Provider unavailable recovery ──────────────── */} {isProviderUnavailable && ( )} {/* ── Malformed response recovery ────────────────── */} {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."}
) : ( <> {/* ── Investigation summary card ─────────────── */} {/* ── Active investigation: question + form (top priority) ─ */} {canAnswer && ( <> {/* Post-update acknowledgement */} {updateStatus === "success" && } {/* Response form */}