"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";
// ── 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 }) {
if (currentSummary) {
return (
Current understanding
{currentSummary}
);
}
return (
We have started to separate what is known from what still needs checking.
);
}
// ── Current focus card ────────────────────────────────────────
function CurrentFocus({ graph }) {
if (!graph?.activeUnknownNodeId || !graph.nodes?.length) return null;
const activeNode = graph.nodes.find(
(n) => n.id === graph.activeUnknownNodeId
);
if (!activeNode) return null;
// Find the unknown label that maps to activeUnknownNodeId from selectedQuestion or nodes
const statusText =
activeNode.status === "resolved" ? "Answered" : "Under investigation";
return (
What we are working out
{activeNode.label}
{activeNode.description && activeNode.description !== activeNode.label && (
Why it matters: {activeNode.description}
)}
{statusText}
);
}
// ── Next question card (prominent) ────────────────────────────
function NextQuestionCard({ selectedQuestion }) {
if (!selectedQuestion) return null;
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
if (!q) return null;
return (
);
}
// ── Reasoning progress card ────────────────────────────────────
function ReasoningProgress({ graph }) {
if (!graph?.nodes?.length) return null;
const unknowns = graph.nodes.filter((n) => n.kind === "unknown");
const remainingCount = unknowns.filter((u) => u.status !== "resolved").length;
return (
Reasoning progress
{remainingCount > 0 ? (
We have identified {remainingCount} area{remainingCount === 1 ? "" : "s"} that still need investigation.
) : (
All areas under investigation are now complete.
)}
{graph.activeUnknownNodeId && (() => {
const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
if (!activeNode) return null;
return (
<>
Current focus
{activeNode.label}
{activeNode.description && activeNode.description !== activeNode.label && (
Why this matters: {activeNode.description}
)}
>
);
})()}
{!graph.activeUnknownNodeId && remainingCount === 0 && (
There is no active area of investigation at the moment.
)}
);
}
// ── No question state ─────────────────────────────────────────
function NoQuestionMessage({ noQuestionReason }) {
let message = "There is no next question at the moment.";
if (noQuestionReason) {
const reason = String(noQuestionReason);
if (reason.toLowerCase().includes("satisfied") || reason.toLowerCase().includes("complete")) {
message += " The situation has been fully investigated.";
} else if (reason.toLowerCase().includes("insufficient")) {
message += " We need more information to determine the next step.";
} 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.
)}
);
}
// ── 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,
}) {
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
INITIAL_MESSAGES,
status === "loading"
);
const { elapsed: updateElapsed, currentMessage: updateMsg } = useLoadingStatus(
UPDATE_MESSAGES,
updateStatus === "loading"
);
const canAnswer =
status === "success" &&
updateStatus === "idle" &&
Boolean(result?.situationGraph) &&
Boolean(result?.selectedQuestion);
const selectedQ = result?.selectedQuestion ?? null;
const graph = result?.situationGraph ?? null;
const diagnostics = result?.diagnostics ?? null;
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
const noQuestionReason = diagnostics?.noQuestionReason ?? null;
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."}
) : (
<>
{/* When analysis succeeded but there's no question to answer */}
{status === "success" && !canAnswer && graph && (
)}
{graph &&
}
{graph &&
}
{graph &&
}
{canAnswer &&
}
{graph &&
}
{/* ── Answer form ──────────────────────────────── */}
{canAnswer && (
)}
{/* ── 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 };