"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" },
];
const REVISION_MESSAGES = [
{ min: 0, text: "Reading what changed" },
{ min: 10, text: "Rebuilding the investigation" },
{ min: 25, text: "Checking what this affects" },
{ min: 45, text: "Choosing the next justified 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 };
}
// ── Revision warning banner ─────────────────────────────────
function RevisionWarningCard() {
return (
Changing this answer may alter the questions and conclusions that followed it.
);
}
// ── Revision inline editor ────────────────────────────────────
function RevisionEditor({ turn, onSave, onCancel }) {
const [draft, setDraft] = useState(turn.answer);
return (
);
}
// ── Stale turn summary (shown under "Earlier answers that may need reviewing") ───
function StaleTurnSummary({ turn, index }) {
return (
Earlier answer #{index + 1}: {turn.question} — "{turn.answer}" may no longer apply.
);
}
// ── 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, graph }) {
if (!selectedQuestion) return null;
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
if (!q) return null;
const activeNode = graph?.activeUnknownNodeId
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
: null;
return (
Current investigation
{q}
{activeNode?.description && activeNode.description !== activeNode.label && (
Why we are asking this
{activeNode.description}
)}
{graph?.activeUnknownNodeId && activeNode && (
What we are investigating
Understanding whether "{activeNode.label}" affects the confidence in this situation.
)}
);
}
// ── 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;
}
// ── Current focus card ───────────────────────────────────────
function CurrentFocusCard({ graph }) {
const activeNode = graph?.activeUnknownNodeId
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
: null;
if (!graph || !activeNode) return null;
return (
Current focus
We are investigating one part of your situation at a time.
{activeNode && (
<>
Right now we are trying to understand{" "}
{activeNode.label} .
>
)}
);
}
// ── Investigation history card ────────────────────────────────
function InvestigationHistoryCard({ turn, onReview }) {
const hasReview = !onReview && turn.status === "current";
return (
{new Date(turn.timestamp).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
{turn.question}
You told us: {turn.answer}
{turn.engineResponse && (
{turn.engineResponse}
)}
{hasReview && (
Review answer
)}
);
}
// ── Investigation history section ─────────────────────────────
function InvestigationHistory({ turns, onReviewTurn }) {
if (!turns || turns.length === 0) return null;
const activeTurns = turns.filter((t) => t.status !== "superseded");
if (activeTurns.length === 0) return null;
return (
Investigation history
{activeTurns.map((turn, idx) => (
onReviewTurn?.(idx) : undefined} />
))}
);
}
// ── 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 (
);
}
// ── 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({ 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 && }
);
}
// ── Main workspace component ──────────────────────────────────
export default function ReasoningWorkspace({
status,
updateStatus,
result,
answer,
setAnswer,
onAnswerSubmit,
lastSubmittedAnswer,
isRevisionLoading: propIsRevisionLoading,
}) {
const [investigationHistory, setInvestigationHistory] = useState([]);
// Inline revision editor state (local — no parent re-render until submit)
const [editingTurnIndex, setEditingTurnIndex] = useState(-1);
const [revisionDraft, setRevisionDraft] = useState("");
const [showRevisionEditor, setShowRevisionEditor] = useState(false);
// Track which turn was revised so we can mark stale turns after update succeeds
const revisedTurnIndexRef = useRef(-1);
// Capture the previous question before each new question is set
const prevQuestionRef = useRef(null);
const hasCapturedInitialQuestion = useRef(false);
useEffect(() => {
if (result?.selectedQuestion && !hasCapturedInitialQuestion.current) {
prevQuestionRef.current = result.selectedQuestion;
hasCapturedInitialQuestion.current = true;
}
}, [result?.selectedQuestion]);
// Append completed turn to history after a successful update (normal or revision)
useEffect(() => {
if (updateStatus !== "success" || !lastSubmittedAnswer) return;
const q = prevQuestionRef.current;
setInvestigationHistory((prev) => {
const newTurn = {
id: Date.now(),
question: typeof q === "string" ? q : q?.question ?? "",
answer: lastSubmittedAnswer,
engineResponse: result?.summary || null,
graphBeforeAnswer: result?.situationGraph ? JSON.parse(JSON.stringify(result.situationGraph)) : null,
selectedQuestionBeforeAnswer: q,
timestamp: Date.now(),
status: "current",
};
// If this was a revision, mark the target turn as superseded and all later turns as stale
const revIdx = revisedTurnIndexRef.current;
if (revIdx >= 0) {
return prev.map((t, i) => {
if (i === revIdx) return { ...t, status: "superseded" };
if (i > revIdx) return { ...t, status: t.status === "superseded" ? "superseded" : "stale" };
return t;
}).concat(newTurn);
}
return [...prev, newTurn];
});
revisedTurnIndexRef.current = -1;
}, [updateStatus, lastSubmittedAnswer]);
const isRevisionLoading = propIsRevisionLoading || false;
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 &&
!isRevisionLoading &&
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 genuineCompletion = hasGenuineCompletion(graph);
// Filter turns into active and stale groups after revision
const activeTurns = investigationHistory.filter((t) => t.status !== "superseded");
const staleTurns = investigationHistory.filter((t) => t.status === "superseded");
// Open inline revision editor for a specific turn
function openRevisionEditor(idx) {
const turn = investigationHistory[idx];
if (!turn) return;
setEditingTurnIndex(idx);
setRevisionDraft(turn.answer);
setShowRevisionEditor(true);
}
function closeRevisionEditor() {
setEditingTurnIndex(-1);
setShowRevisionEditor(false);
setRevisionDraft("");
}
// Save revised answer — use graph checkpoint from the target turn,
// or fall back to the current situationGraph as a replay starting point.
function saveRevision(draft) {
if (!draft?.trim()) return;
closeRevisionEditor();
const checkpoint = investigationHistory[editingTurnIndex]?.graphBeforeAnswer ?? graph;
revisedTurnIndexRef.current = editingTurnIndex;
setAnswer(draft.trim());
onAnswerSubmit({ preventDefault: () => {} }, { graphCheckpoint: checkpoint, turnIndex: editingTurnIndex });
}
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" && canAnswer &&
}
{/* Revision editor inline */}
{showRevisionEditor && investigationHistory[editingTurnIndex] && (
saveRevision(draft)}
onCancel={closeRevisionEditor}
/>
)}
{/* Stale turns summary — shown after revision */}
{staleTurns.length > 0 && (
Earlier answers that may need reviewing
{staleTurns.map((turn, idx) => (
))}
)}
{/* Completion state — suppressed during any loading or revision */}
{status === "success" && !canAnswer && graph && genuineCompletion && !isRevisionLoading && (
)}
{status === "success" && !canAnswer && graph && !genuineCompletion && updateStatus !== "success" && !isRevisionLoading && (
There is no further question the engine can justify at the moment.
More evidence may be needed before a next step is clear.
)}
{/* Situation context */}
{graph && }
{graph && }
{/* Active investigation (only when we have a question to answer) */}
{canAnswer && }
{canAnswer && }
{/* ── Answer form ──────────────────────────────── */}
{canAnswer && (
)}
{/* ── Investigation history (below the answer form) ─ */}
{/* ── 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, REVISION_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary };