feat(experiment): checkpoint RTO question lifecycle
This commit is contained in:
+111
-114
@@ -686,9 +686,12 @@ export default function ReasoningWorkspace({
|
||||
<div className="space-y-1">
|
||||
{openNodes.map((node) => {
|
||||
const isSelected = selectedPresentationItemId === node.id;
|
||||
const isFocused = focusedPresentationItemId === node.id;
|
||||
const hasReasoningSupport = result?.selectedQuestion?.nodeId === node.id && node.id === focusedPresentationItemId;
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
{/* One invariant outer shell — same border/padding/position across all states */}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (result?.selectedQuestion?.nodeId !== node.id) {
|
||||
@@ -700,43 +703,123 @@ export default function ReasoningWorkspace({
|
||||
}
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className={`w-full text-left rounded-lg px-4 py-3 transition ${
|
||||
isSelected
|
||||
? "border border-gray-300 bg-white/80 hover:bg-white"
|
||||
: "border border-gray-200 hover:border-gray-300 hover:bg-white"
|
||||
}`}
|
||||
className="w-full text-left rounded-lg border border-gray-200 px-4 py-3 transition hover:border-gray-300 hover:bg-white"
|
||||
>
|
||||
<p className={
|
||||
isSelected
|
||||
? "text-sm font-medium text-gray-900 leading-snug"
|
||||
: "text-sm text-gray-600 leading-snug"
|
||||
}>
|
||||
<p className={`leading-snug ${isSelected ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>
|
||||
{node.label}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isSelected && (
|
||||
<div className="px-4 pb-4 pt-1 space-y-3">
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
We have not explored this yet. Do you want to work through it?
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPresentationItemId(null);
|
||||
setFocusedPresentationItemId(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"
|
||||
>
|
||||
Work through this
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Invitation / returned state — one block for both, since isSelected!==isFocused covers both */}
|
||||
{isSelected && !isFocused && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
We have not explored this yet. Do you want to work through it?
|
||||
</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPresentationItemId(node.id);
|
||||
setFocusedPresentationItemId(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"
|
||||
>
|
||||
Work through this
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Focused state (Work through this clicked) */}
|
||||
{isFocused && (() => {
|
||||
const focusedNode = graph?.nodes.find((n) => n.id === node.id);
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Real Engine-backed content — only for matching question */}
|
||||
{hasReasoningSupport ? (
|
||||
<>
|
||||
<div className="text-xs text-gray-400">
|
||||
Chosen investigation: {" "}
|
||||
{focusedNode?.label}
|
||||
</div>
|
||||
<CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />
|
||||
|
||||
{updateStatus === "success" && !isUpdating && (
|
||||
<UpdateAcknowledgement updateResult={result} />
|
||||
)}
|
||||
|
||||
{/* Answer form — only when focused item matches Engine question */}
|
||||
{!isUpdating && canAnswer && (
|
||||
<form onSubmit={handleUpdateCaptureAndSubmit} className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
<div>
|
||||
<label htmlFor={`rw-answer-${node.id}`} className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Response
|
||||
</label>
|
||||
<textarea
|
||||
id={`rw-answer-${node.id}`}
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer(e.target.value)}
|
||||
rows={4}
|
||||
disabled={updateStatus === "loading"}
|
||||
data-testid="response-textarea"
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
placeholder="What do you know about this?"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-400">
|
||||
One update turn only in this prototype.
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!answer.trim()}
|
||||
className="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-100/50 px-8 py-6 text-center">
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
We have not explored this yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFocusedPresentationItemId(null);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="text-sm text-gray-400 underline hover:text-gray-600 transition"
|
||||
>
|
||||
Back to open questions
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFocusedPresentationItemId(null);
|
||||
setDoneForNowIds((prev) => [...prev, node.id]);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 transition"
|
||||
>
|
||||
Done for now
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Done for now — separate visual area */}
|
||||
{doneForNowIds.length > 0 && (
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
@@ -765,92 +848,6 @@ export default function ReasoningWorkspace({
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ── Focused question workspace ── */}
|
||||
{focusedPresentationItemId !== null && (() => {
|
||||
const focusedNode = graph?.nodes.find(
|
||||
(n) => n.id === focusedPresentationItemId,
|
||||
);
|
||||
const hasReasoningSupport = result?.selectedQuestion?.nodeId === focusedPresentationItemId;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<button
|
||||
onClick={() => setFocusedPresentationItemId(null)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="text-sm text-gray-400 underline hover:text-gray-600 transition"
|
||||
>
|
||||
Back to open questions
|
||||
</button>
|
||||
|
||||
{hasReasoningSupport && (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-gray-400">
|
||||
Chosen investigation: {" "}
|
||||
{focusedNode?.label}
|
||||
</div>
|
||||
<CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasReasoningSupport && (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-100/50 px-8 py-6 text-center">
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
We have not explored this yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updateStatus === "success" && !isUpdating && (
|
||||
<UpdateAcknowledgement updateResult={result} />
|
||||
)}
|
||||
|
||||
{/* Answer form — only when focused item matches Engine question */}
|
||||
{!isUpdating && hasReasoningSupport && canAnswer && (
|
||||
<form onSubmit={handleUpdateCaptureAndSubmit} className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
<div>
|
||||
<label htmlFor="rw-answer" className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Response
|
||||
</label>
|
||||
<textarea
|
||||
id="rw-answer"
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer(e.target.value)}
|
||||
rows={4}
|
||||
disabled={updateStatus === "loading"}
|
||||
data-testid="response-textarea"
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
placeholder="What do you know about this?"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-400">
|
||||
One update turn only in this prototype.
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!answer.trim()}
|
||||
className="rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setFocusedPresentationItemId(null);
|
||||
setDoneForNowIds((prev) => [...prev, focusedPresentationItemId]);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 transition"
|
||||
>
|
||||
Done for now
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Terminal state */}
|
||||
{status === "success" && !hasSelectedQuestion && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user