"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({ 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.
);
}
// ── 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 (
);
}
// ── 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,
currentUnderstanding: propUnderstanding,
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);
// 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" && (
)}
{/* ── 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."}
) : (
<>
{/* ── Active investigation: question + form (top priority) ─ */}
{canAnswer && (
<>
{/* Post-update acknowledgement */}
{updateStatus === "success" &&
}
{/* Response form */}
>
)}
{/* ── Terminal state: outcome card (no active question) ─ */}
{status === "success" && !hasSelectedQuestion && graph && genuineCompletion && (
)}
{status === "success" && !hasSelectedQuestion && graph && !genuineCompletion && (
)}
{/* ── Current understanding (active investigation only) ─ */}
{hasCurrentSummaryCondition && !hasSelectedQuestion === false && (
)}
{investigationHistory.length > 0 &&
}
{/* ── Developer details (collapsed by default) ── */}
{(status === "success" || status === "error") && graph && (
)}
>
)}
{/* ── Errors (always visible above debug) ─────────── */}
{(status === "error" || updateStatus === "error") && (
{status === "error" && result?.error && (
Error: {result.error}
)}
{updateStatus === "error" && result?.updateError && (
Update error: {result.updateError.error || JSON.stringify(result.updateError)}
)}
)}
);
}
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary };