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

Investigation complete

The current investigation has reached a justified conclusion.

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

Current evidence limit reached

Further progress requires additional evidence.

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

Current understanding

{summary}

); } // ── Investigation history card (readable notebook style) ────── function InvestigationHistoryCard({ turn }) { const isCollapsed = turn._collapsed; return (
{isCollapsed ? "Q: " : "✓ "} {turn.question.length > 60 && !isCollapsed ? turn.question.slice(0, 60) + "…" : turn.question} {!isCollapsed && ( — answered )}

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 }) { if (!scenario) return null; return (

Original situation

{centralStatement && (

{centralStatement}

)}

{scenario}

); } // ── Update acknowledgement ──────────────────────────────────── function UpdateAcknowledgement({ updateResult }) { if (!updateResult) return null; const summary = updateResult.summary; const hasResolvedNodes = updateResult.resolvedUnknownNodeIds?.length > 0; const hasAffectedNodes = updateResult.affectedNodeIds?.length > 0; const graph = updateResult.updatedSituationGraph; function getNodeText(nodeId) { if (!graph?.nodes) return String(nodeId); const node = graph.nodes.find((n) => n.id === nodeId); if (node) { const parts = [node.label]; if (node.status !== "resolved") { parts.push(node.status); } return parts.join(" "); } return String(nodeId); } let changedText; if (hasResolvedNodes) { const items = []; for (const id of updateResult.resolvedUnknownNodeIds.slice(0, 3)) { items.push(getNodeText(id)); } if (updateResult.resolvedUnknownNodeIds.length > 3) { items.push(`and ${updateResult.resolvedUnknownNodeIds.length - 3} more resolved`); } changedText = items.join(". ") + "."; } else if (hasAffectedNodes) { const items = []; for (const id of updateResult.affectedNodeIds.slice(0, 3)) { items.push(getNodeText(id)); } if (updateResult.affectedNodeIds.length > 3) { items.push(`and ${updateResult.affectedNodeIds.length - 3} more affected`); } changedText = items.join(". ") + "."; } else if (updateResult.changesApplied) { const ca = updateResult.changesApplied; const parts = []; if (ca.addedNodeCount) parts.push(`${ca.addedNodeCount} node(s) added`); if (ca.updatedNodeCount) parts.push(`${ca.updatedNodeCount} node(s) updated`); if (ca.addedEdgeCount) parts.push(`${ca.addedEdgeCount} edge(s) added`); if (ca.removedEdgeCount) parts.push(`${ca.removedEdgeCount} edge(s) removed`); changedText = parts.length > 0 ? parts.join(", ") : null; } const displayMessage = summary || changedText || "Your answer has been added to the investigation."; return (
{displayMessage}
); } // ── 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 ────────────────────────────────── export default function ReasoningWorkspace({ scenario, status, updateStatus, result, answer, setAnswer, onAnswerSubmit, lastSubmittedAnswer, }) { const [investigationHistory, setInvestigationHistory] = useState([]); const turnCounter = useRef(0); const pendingTurnRef = useRef(null); // 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 { 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; const graph = result?.situationGraph ?? null; const diagnostics = result?.diagnostics ?? null; const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || []; const genuineCompletion = hasGenuineCompletion(graph); return (
{/* ── Loading overlays ─────────────────────────────── */} {status === "loading" && ( )} {updateStatus === "loading" && ( )} {/* ── 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."}
) : ( <> {/* ── 1. Terminal state: outcome card (no active question) ─ */} {status === "success" && !canAnswer && graph && !isUpdating && genuineCompletion && } {status === "success" && !canAnswer && graph && !isUpdating && !genuineCompletion && } {/* ── 2. Current understanding (when meaningful) ─ */} {(graph?.currentSummary || (status === "success" && !canAnswer)) && } {/* ── 3. Original situation (always-visible reference) ─ */} {/* ── 4. Investigation history ───────────────────── */} {investigationHistory.length > 0 && } {/* ── Active investigation: question + form + acknowledgement ─ */} {canAnswer && ( <> {/* Post-update acknowledgement */} {updateStatus === "success" && } {/* Response form */}