From dafc020f6677e88b0fd8a001522d5620d108a275 Mon Sep 17 00:00:00 2001
From: robbond
Date: Wed, 19 Aug 2026 07:07:10 +0100
Subject: [PATCH] feat(experiment): checkpoint one-turn focused investigation
UI
---
components/reasoning-workspace.jsx | 332 ++++++++++++++++++++++++-----
1 file changed, 284 insertions(+), 48 deletions(-)
diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx
index 4d29bee..18f6bac 100644
--- a/components/reasoning-workspace.jsx
+++ b/components/reasoning-workspace.jsx
@@ -599,6 +599,11 @@ export default function ReasoningWorkspace({
errorType === "provider-unavailable" || errorType === "provider-error";
const isMalformedResponse = errorType === "malformed-response";
+ // ── RTO.13B — focused investigation localized state ───────────
+
+ const [formulationStep, setFormulationStep] = useState("idle");
+ const [processingStep, setProcessingStep] = useState("idle");
+
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
INITIAL_MESSAGES,
status === "loading"
@@ -609,9 +614,156 @@ export default function ReasoningWorkspace({
updateStatus === "loading"
);
+ // ── RTO.13B — focused investigation loading hooks ─────────────
+
+ const FORMULATING_MESSAGES = [
+ { min: 0, text: "Working out a question…" },
+ { min: 15, text: "Still working on that question" },
+ { min: 30, text: "Formulating the right question for this" },
+ { min: 45, text: "A moment longer — this can take around a minute" },
+ ];
+
+ const DECONSTRUCT_MESSAGES = [
+ { min: 0, text: "Working through your response…" },
+ { min: 15, text: "Still working on that response" },
+ { min: 30, text: "A moment longer — this can take around a minute" },
+ ];
+
+ const { elapsed: formulateElapsed, currentMessage: formulateMsg } = useLoadingStatus(
+ FORMULATING_MESSAGES,
+ formulationStep === "active",
+ );
+
+ const { elapsed: deconstructElapsed, currentMessage: deconstructMsg } = useLoadingStatus(
+ DECONSTRUCT_MESSAGES,
+ processingStep === "active",
+ );
+
const isUpdating = updateStatus === "loading";
const hasSelectedQuestion = Boolean(result?.selectedQuestion);
+ // ── RTO.13B — focused investigation localized state (keyed by node ID) ──
+ const [focusedInvestigations, setFocusedInvestigations] = useState({});
+ const [focusedAnswer, setFocusedAnswer] = useState("");
+
+ function getFocusedInvestigation() {
+ if (!focusedPresentationItemId) return null;
+ return focusedInvestigations[focusedPresentationItemId] || null;
+ }
+
+ function focusItem(nodeId) {
+ setSelectedPresentationItemId(nodeId);
+ setFocusedPresentationItemId(nodeId);
+ }
+
+ const focused = getFocusedInvestigation();
+
+ // ── RTO.13B — workflow handlers ──────────────────────────────
+
+ function startFocused(nodeId) {
+ const target = nodeId || focusedPresentationItemId;
+ if (!target) return;
+ setFocusedPresentationItemId(target);
+ setFocusedAnswer("");
+ setFormulationStep("active");
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [target]: { status: "formulating", question: "", answer: null, result: null, error: null },
+ }));
+ doFormulate(target);
+ }
+
+ async function doFormulate(targetNodeId) {
+ const nodeId = targetNodeId || focusedPresentationItemId;
+ if (!nodeId || !hasGraph) return;
+ try {
+ const url = "/api/focused-investigation/formulate";
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ situationGraph: graph, targetNodeId: nodeId }),
+ });
+ const data = await res.json();
+ if (!data.success) throw new Error(data.error || "Formulation failed");
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [nodeId]: { ...prev[nodeId], question: data.question, status: "formulated", error: null },
+ }));
+ setFormulationStep("idle");
+ } catch (err) {
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [focusedPresentationItemId]: { ...prev[focusedPresentationItemId], question: "", error: err.message || "Formulation failed" },
+ }));
+ }
+ }
+
+ async function handleDeconstructSubmit(targetNodeId, answerText) {
+ if (!hasGraph) return;
+
+ const targetNode = graph?.nodes?.find((n) => n.id === targetNodeId);
+ const centralStmt = graph?.centralStatement || scenario || "";
+
+ setProcessingStep("active");
+
+ try {
+ const url = "/api/focused-investigation/deconstruct";
+
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ targetNodeId: targetNodeId,
+ targetLabel: targetNode?.label || "",
+ targetDescription: targetNode?.description || "",
+ centralStatement: centralStmt,
+ question: focused.question,
+ answer: answerText,
+ }),
+ });
+
+ const data = await res.json();
+
+ if (!data.success) throw new Error(data.error || "Deconstruction failed");
+
+ setProcessingStep("idle");
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [targetNodeId]: { ...prev[targetNodeId], result: data, answer: answerText, error: null },
+ }));
+ } catch (err) {
+ setProcessingStep("idle");
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [targetNodeId]: { ...prev[targetNodeId], result: null, error: err.message || "Deconstruction failed" },
+ }));
+ }
+ }
+
+ async function submitFocusedAnswer() {
+ if (!focused?.question?.trim() || !hasGraph) return;
+ const targetNodeId = focusedPresentationItemId;
+ await handleDeconstructSubmit(targetNodeId, focusedAnswer);
+ }
+
+ function retryFormulation() {
+ const target = focusedPresentationItemId;
+ if (!target) return;
+ setFocusedInvestigations((prev) => ({
+ ...prev,
+ [target]: { ...prev[target], question: "", error: null },
+ }));
+ doFormulate(target);
+ }
+
+ function hasFocusedContent() {
+ if (!focused) return false;
+ const q = focused.question;
+ return Boolean(q?.trim()) || formulationStep === "active" || processingStep === "active" || focused.error;
+ }
+
+ // ── End RTO.13B ──────────────────────────────────────────────
+
const canAnswer =
status === "success" &&
!isUpdating &&
@@ -709,8 +861,8 @@ export default function ReasoningWorkspace({
{node.label}
- {/* Invitation / returned state — one block for both, since isSelected!==isFocused covers both */}
- {isSelected && !isFocused && (
+ {/* Invitation — shows when selected but no content yet */}
+ {isSelected && !hasFocusedContent() && (
We have not explored this yet. Do you want to work through it?
@@ -718,8 +870,7 @@ export default function ReasoningWorkspace({
{
e.stopPropagation();
- setSelectedPresentationItemId(node.id);
- setFocusedPresentationItemId(node.id);
+ startFocused(node.id);
}}
style={{ cursor: "pointer" }}
className="rounded-lg border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50 transition"
@@ -734,57 +885,142 @@ export default function ReasoningWorkspace({
const focusedNode = graph?.nodes.find((n) => n.id === node.id);
return (
- {/* Real Engine-backed content — only for matching question */}
- {hasReasoningSupport ? (
- <>
-
- Chosen investigation: {" "}
- {focusedNode?.label}
-
-
+ {/* Real focused investigation content */}
+ {hasFocusedContent() && (() => {
+ return null;
+ })()}
+ {hasFocusedContent() && (
+
+ {/* Formulated question */}
+ {focused?.question?.trim() ? (
+
+
+ Question
+
+
+ {focused.question}
+
+
+ ) : formulationStep === "active" ? (
+
{formulateMsg}
+ ) : null}
- {updateStatus === "success" && !isUpdating && (
-
+ {/* Answer textarea (hidden while processing) */}
+ {focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (
+
+
+ Your response
+
+
)}
- {/* Answer form — only when focused item matches Engine question */}
- {!isUpdating && canAnswer && (
-