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({