"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; } function resolveCurrentSummary(currentSummary) { if (isTechnicalSummary(currentSummary)) { return null; } return currentSummary || null; } // ── 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 ( ); } // ── Situation card ──────────────────────────────────────────── function SituationCard({ centralStatement }) { if (!centralStatement) return null; return (

Your situation

{centralStatement}

); } // ── Current understanding card ──────────────────────────────── function CurrentUnderstanding({ currentSummary }) { const summary = resolveCurrentSummary(currentSummary); return (

What we've established

{summary ? (

{summary}

) : (

We have separated what is known from what still needs checking.

)}
); } // ── Current investigation card (prominent) ───────────────────── function CurrentInvestigationCard({ selectedQuestion }) { if (!selectedQuestion) return null; const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question; if (!q) return null; return (

Current investigation

{q}

); } // ── 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; } // ── Investigation progress card ────────────────────────────── function InvestigationProgress({ graph, noQuestionReason: rwNoQuestionReason }) { if (!graph?.nodes?.length) return null; const resolvedIds = new Set(graph.resolvedNodeIds || []); const unknowns = graph.nodes.filter((n) => n.kind === "unknown"); const remainingCount = unknowns.filter( (u) => u.status !== "resolved" && !resolvedIds.has(u.id), ).length; const activeNode = graph.activeUnknownNodeId ? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId) : null; const isComplete = hasGenuineCompletion(graph); return (
{remainingCount > 0 && !isComplete ? (

We are still building confidence about your situation.{" "} {remainingCount === 1 ? "One area remains." : `${remainingCount} areas remain.`}

) : (

All areas under investigation are now complete.

)} {activeNode && ( <>

Current focus

{activeNode.label}

{activeNode.description && activeNode.description !== activeNode.label && (

Why it matters: {activeNode.description}

)} )} {!activeNode && remainingCount === 0 && (

There is nothing further to investigate at this time.

)}
); } // ── Investigation complete state ─────────────────────────────── function InvestigationCompleteMessage({ noQuestionReason }) { let message = "We have established enough for now."; if (noQuestionReason) { const reason = String(noQuestionReason); if ( reason.toLowerCase().includes("resolved") || reason.toLowerCase().includes("satisfied") || reason.toLowerCase().includes("complete") ) { message = "You have provided enough information. The situation has been fully investigated."; } else if (reason.toLowerCase().includes("insufficient")) { message = "There is not yet enough evidence to guide the next step. Your original situation will remain our focus when new information becomes available."; } else { message = reason; } } return (

{message}

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

); } // ── Update acknowledgement ──────────────────────────────────── function UpdateAcknowledgement({ answer, updateResult }) { if (!updateResult || !answer?.trim()) return null; const hasResolvedNodes = updateResult.resolvedUnknownNodeIds && updateResult.resolvedUnknownNodeIds.length > 0; const hasAffectedNodes = updateResult.affectedNodeIds && 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 || hasAffectedNodes) { const items = []; if (hasResolvedNodes) { for (const id of updateResult.resolvedUnknownNodeIds.slice(0, 5)) { items.push(getNodeText(id)); } if (updateResult.resolvedUnknownNodeIds.length > 5) { items.push(`and ${updateResult.resolvedUnknownNodeIds.length - 5} more resolved`); } } if (hasAffectedNodes && !hasResolvedNodes) { for (const id of updateResult.affectedNodeIds.slice(0, 5)) { items.push(getNodeText(id)); } if (updateResult.affectedNodeIds.length > 5) { items.push(`and ${updateResult.affectedNodeIds.length - 5} more affected`); } } if (items.length === 0 && updateResult.changesApplied) { const parts = []; const ca = updateResult.changesApplied; if (ca.addedNodeCount) parts.push(`${ca.addedNodeCount} node(s) added`); if (ca.updatedNodeCount) parts.push(`${ca.updatedNodeCount} node(s) updated`); if (ca.resolvedUnknownCount) parts.push(`${ca.resolvedUnknownCount} unknown(s) resolved`); changedText = parts.join(", "); } else { 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 summary = updateResult.summary || null; const displayChanged = summary || changedText || "Your answer has been added to the investigation."; return (

You told us

{answer}

What changed

{displayChanged}

); } // ── Developer details disclosure ────────────────────────────── function DeveloperDetails({ graph, selectedQuestion, diagnostics, newlySurfacedNodeIds, updateResult }) { return (
Developer details
{graph && ( )} {updateResult && ( )} {diagnostics && }
); } // ── Main workspace component ────────────────────────────────── export default function ReasoningWorkspace({ status, updateStatus, result, answer, setAnswer, onAnswerSubmit, lastSubmittedAnswer, }) { 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 noQuestionReason = diagnostics?.noQuestionReason ?? null; const remainingUnknowns = graph?.nodes?.filter( (n) => n.kind === "unknown" && n.status !== "resolved" && !(graph.resolvedNodeIds || []).includes(n.id), ); const genuineCompletion = hasGenuineCompletion(graph); const unresolvedRemaining = !genuineCompletion && remainingUnknowns ? remainingUnknowns.length > 0 : false; return (
{/* ── Loading overlays ─────────────────────────────── */} {updateStatus === "loading" && (
)} {/* ── User-facing workspace ────────────────────────── */} {(status === "success" || status === "error") && !graph ? (
{noQuestionReason ? "Validation failed — no structured graph output was produced." : "The analysis completed but did not produce a structured result."}
) : ( <> {/* ── Post-update acknowledgement ─────────────── */} {updateStatus === "success" && graph && ( )} {/* Completion state (only when there is no next question and nothing remains) */} {status === "success" && !canAnswer && graph && genuineCompletion && ( )} {status === "success" && !canAnswer && graph && unresolvedRemaining && updateStatus !== "success" && (

There is no further question the engine can justify at the moment.

More evidence may be needed before a next step is clear.

)} {graph && } {graph && } {canAnswer && } {graph && } {/* ── Answer form ──────────────────────────────── */} {canAnswer && (