test(ui): explore branch notebook composition
This commit is contained in:
+315
-304
@@ -310,6 +310,157 @@ function EvidenceLimitCard({ summary }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Quiet facilitator state (experiment mode) ─────────────────────
|
||||
|
||||
function QuietStateCard() {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-5 py-5 text-center">
|
||||
<p className="text-sm leading-relaxed text-gray-500 mb-1">
|
||||
Nothing more to add here right now
|
||||
</p>
|
||||
<p className="text-xs leading-relaxed text-gray-400">
|
||||
You can continue with another question, switch branches, or return when
|
||||
you have more information.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── New connection / late-result indicator ────────────────────────
|
||||
|
||||
function UpdatedConnectionCard({ update }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
New update
|
||||
</h2>
|
||||
<div className="rounded-lg border border-blue-200/60 bg-blue-50/40 px-4 py-3">
|
||||
<p className="text-sm leading-relaxed text-blue-800/80">{update.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Branch notebook content (RTO.27A) ────────────────────────────
|
||||
// Smallest useful branch-scoped composition from fixture data.
|
||||
// Only renders sections grounded in existing records.
|
||||
|
||||
function BranchNotebookContent({
|
||||
branchContext,
|
||||
contributions,
|
||||
openQuestions,
|
||||
lateResults,
|
||||
inactiveBranchNewResults,
|
||||
}) {
|
||||
const hasOpenItems = openQuestions && openQuestions.length > 0;
|
||||
const hasContributions = contributions && contributions.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Why this branch exists */}
|
||||
{branchContext && (
|
||||
<div className="rounded-lg border border-blue-200/60 bg-blue-50/40 px-5 py-4">
|
||||
<h2 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Current branch
|
||||
</h2>
|
||||
<p className="text-base font-medium leading-tight text-gray-900">{branchContext.label}</p>
|
||||
{branchContext.origin && (
|
||||
<p className="mt-2 text-sm leading-relaxed text-gray-600">
|
||||
Trying to understand: {branchContext.origin}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* What we know here */}
|
||||
{hasContributions && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
What we know here
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{contributions.map((c, i) => (
|
||||
<div key={i} className="rounded-lg border border-gray-200/80 bg-gray-50/60 px-4 py-3">
|
||||
<p className="text-sm leading-relaxed text-gray-700">{c.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Still uncertain here */}
|
||||
{hasOpenItems && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Still uncertain / open questions
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
{openQuestions.map((q, i) => (
|
||||
<QuestionRow key={q.id} question={q} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New connection / updates (active branch) */}
|
||||
{lateResults && lateResults.length > 0 && (
|
||||
<UpdatedConnectionCard update={lateResults[0]} />
|
||||
)}
|
||||
|
||||
{/* Quiet state — only when there are no open items and no updates */}
|
||||
{!hasOpenItems && !lateResults?.length && (
|
||||
<QuietStateCard />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Question row (renders clickable card with optional focused content) ─
|
||||
|
||||
function QuestionRow({ question }) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
if (!isFocused) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsFocused(true)}
|
||||
style={{ cursor: "pointer" }}
|
||||
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="text-sm leading-snug text-gray-600">{question.label}</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white px-4 py-4 space-y-3">
|
||||
<p className="text-sm font-medium text-gray-900 leading-snug">{question.label}</p>
|
||||
<div className="space-y-2">
|
||||
<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();
|
||||
// Placeholder — production would call API for focused investigation
|
||||
}}
|
||||
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>
|
||||
<button
|
||||
onClick={() => setIsFocused(false)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="text-xs text-gray-400 underline hover:text-gray-600 transition"
|
||||
>
|
||||
Back to open questions
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Current understanding card ────────────────────────────────
|
||||
function CurrentUnderstandingCard({ currentSummary, plainLanguage }) {
|
||||
if (plainLanguage) return <PlainLanguageCard summary={plainLanguage} />;
|
||||
@@ -510,6 +661,132 @@ function getErrorType(errorStr, stage, hasGraph) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Open questions panel (production, non-experiment mode) ────────
|
||||
|
||||
function OpenQuestionsPanel({
|
||||
graph, selectedPresentationItemId, focusedPresentationItemId, hasFocusedContent,
|
||||
focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds,
|
||||
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
|
||||
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
||||
}) {
|
||||
const openNodes = (graph?.nodes || []).filter(
|
||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||
);
|
||||
if (openNodes.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Open questions
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
{openNodes.map((node) => {
|
||||
const isSelected = selectedPresentationItemId === node.id;
|
||||
const isFocused = focusedPresentationItemId === node.id;
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!focused?.question?.trim() || (focused && !hasFocusedContent())) {
|
||||
setSelectedPresentationItemId(
|
||||
selectedPresentationItemId === node.id ? null : node.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
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={`leading-snug ${isSelected ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>
|
||||
{node.label}
|
||||
</p>
|
||||
|
||||
{isSelected && !hasFocusedContent() && (
|
||||
<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(); 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"
|
||||
>
|
||||
Work through this
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFocused && (() => {
|
||||
const focusedNode = graph?.nodes.find((n) => n.id === node.id);
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
{hasFocusedContent() && (
|
||||
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
{focused?.question?.trim() ? (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Question</h3>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">{focused.question}</p>
|
||||
</div>
|
||||
) : formulationStep === "active" ? (
|
||||
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
||||
) : null}
|
||||
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (
|
||||
<div>
|
||||
<label htmlFor={`rw-answer-${node.id}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
|
||||
<textarea id={`rw-answer-${node.id}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} 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?" />
|
||||
<button onClick={(e) => { e.stopPropagation(); handleDeconstructSubmit(focusedPresentationItemId, focusedAnswer); }} disabled={!focusedAnswer.trim() || processingStep === "active"} style={{ cursor: !focusedAnswer.trim() || processingStep === "active" ? "not-allowed" : "pointer" }} className="mt-3 rounded-lg border border-green-600 bg-white px-4 py-2 text-sm font-medium text-green-700 hover:bg-green-50 transition disabled:opacity-50">Submit response</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{processingStep === "active" && <p className="text-sm text-blue-600/70">{deconstructMsg}</p>}
|
||||
|
||||
{focused?.result && (
|
||||
<>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What we learned</h3><ul className="list-disc pl-5 space-y-1">{focused.result.observations.map((o, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{o}</li>))}</ul></div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h3><ul className="list-disc pl-5 space-y-1">{focused.result.uncertainties.map((u, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{u}</li>))}</ul></div>
|
||||
{focused.result.assumptions?.length > 0 && <div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions in this response</h3><ul className="list-disc pl-5 space-y-1">{focused.result.assumptions.map((a, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>))}</ul></div>}
|
||||
{focused.result.relationships?.length > 0 && <div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Connections</h3><ul className="list-disc pl-5 space-y-1">{focused.result.relationships.map((r, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>))}</ul></div>}
|
||||
{focused.result.possibleFollowUpQuestions?.length > 0 && <div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Questions this raises</h3><ul className="list-disc pl-5 space-y-1">{focused.result.possibleFollowUpQuestions.map((q, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{q}</li>))}</ul></div>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{focused?.error && processingStep !== "active" && (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">We were unable to process your request right now. Please try again later.<button onClick={(e) => { e.stopPropagation(); retryFormulation(); }} className="ml-2 font-medium underline">Retry</button></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 mb-3 flex items-center justify-between gap-4">
|
||||
<button onClick={(e) => { e.stopPropagation(); setFocusedPresentationItemId(null); }} style={{ cursor: "pointer" }} className="text-sm text-gray-400 underline hover:text-gray-600 transition whitespace-nowrap">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 whitespace-nowrap">Done for now</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{doneForNowIds.length > 0 && (
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<h3 className="mb-2 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Done for now</h3>
|
||||
<div className="space-y-1">
|
||||
{graph.nodes.filter((n) => doneForNowIds.includes(n.id)).map((node) => (
|
||||
<div key={node.id} className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3">
|
||||
<p className="text-sm text-gray-500 leading-snug">{node.label}</p>
|
||||
<button onClick={() => setDoneForNowIds(doneForNowIds.filter(id => id !== node.id))} style={{ cursor: "pointer" }} className="mt-2 rounded border border-gray-300 px-3 py-1 text-xs font-medium text-gray-500 hover:bg-white transition">Reopen</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReasoningWorkspace({
|
||||
scenario,
|
||||
status,
|
||||
@@ -526,6 +803,7 @@ export default function ReasoningWorkspace({
|
||||
experimentalBranches,
|
||||
branchLocalQuestions,
|
||||
branchLocalContributions,
|
||||
branchLocalLateResults,
|
||||
inactiveBranchNewResults,
|
||||
}) {
|
||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||
@@ -829,313 +1107,41 @@ export default function ReasoningWorkspace({
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* ── Left lane: active conversation & notebook ───────── */}
|
||||
<div className={`space-y-6 ${hasGraph ? 'lg:col-span-2' : 'lg:col-span-full'}`}>
|
||||
{/* RTO.26B — experiment header when using branch-scoped fixture */}
|
||||
{/* RTO.27A — branch notebook content (replaces question-queue when in experiment mode) */}
|
||||
{experimentalBranches && experimentalBranches.length > 0 && (
|
||||
<div className="rounded-lg border border-purple-300/60 bg-purple-50/30 px-4 py-2">
|
||||
<p className="text-[10px] font-semibold tracking-widest uppercase text-purple-400/70">
|
||||
RTO.26B — branch-scoped reasoning (experimental fixture)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active branch context (experiment RTO.25D) */}
|
||||
{branchContext && (hasGraph || experimentalBranches) && (
|
||||
<div className="rounded-lg border border-blue-200/60 bg-blue-50/40 px-5 py-4">
|
||||
<h2 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Current branch
|
||||
</h2>
|
||||
<p className="text-base font-medium leading-tight text-gray-900">{branchContext.label}</p>
|
||||
{branchContext.origin && (
|
||||
<p className="mt-2 text-sm leading-relaxed text-gray-600">
|
||||
Trying to understand: {branchContext.origin}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<BranchNotebookContent
|
||||
branchContext={branchContext}
|
||||
contributions={branchLocalContributions || []}
|
||||
openQuestions={branchLocalQuestions || []}
|
||||
lateResults={branchLocalLateResults || []}
|
||||
inactiveBranchNewResults={inactiveBranchNewResults || {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Open questions (case workspace) — no ranking bias ── */}
|
||||
{(() => {
|
||||
/* RTO.26B: when experimental branch-scoped data is present, use it as the source of truth */
|
||||
const openNodes = experimentalBranches && branchLocalQuestions && branchLocalQuestions.length > 0
|
||||
? branchLocalQuestions
|
||||
: ((graph?.nodes || []).filter(
|
||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||
));
|
||||
if (openNodes.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
In this branch — Open questions
|
||||
</h2>
|
||||
<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) {
|
||||
setSelectedPresentationItemId(
|
||||
selectedPresentationItemId === node.id
|
||||
? null
|
||||
: node.id,
|
||||
);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
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={`leading-snug ${isSelected ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>
|
||||
{node.label}
|
||||
</p>
|
||||
|
||||
{/* Invitation — shows when selected but no content yet */}
|
||||
{isSelected && !hasFocusedContent() && (
|
||||
<div className="mt-3 space-y-3">
|
||||
{/* Branch-local contribution scoped to this question */}
|
||||
{(experimentalBranchContributions?.length ?? 0) > 0 && (
|
||||
(() => {
|
||||
const qContribs = experimentalBranchContributions.filter(
|
||||
c => node.id === c.questionId,
|
||||
);
|
||||
if (!qContribs.length) return null;
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200/80 bg-gray-50/60 px-4 py-3">
|
||||
<h3 className="mb-1 text-[10px] font-semibold tracking-widest uppercase text-gray-400">
|
||||
Contribution
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700">{qContribs[0].text}</p>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
<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();
|
||||
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"
|
||||
>
|
||||
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 focused investigation content */}
|
||||
{hasFocusedContent() && (() => {
|
||||
return null;
|
||||
})()}
|
||||
{hasFocusedContent() && (
|
||||
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
{/* Formulated question */}
|
||||
{focused?.question?.trim() ? (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Question
|
||||
</h3>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">
|
||||
{focused.question}
|
||||
</p>
|
||||
</div>
|
||||
) : formulationStep === "active" ? (
|
||||
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
||||
) : null}
|
||||
|
||||
{/* Answer textarea (hidden while processing) */}
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && (
|
||||
<div>
|
||||
<label htmlFor={`rw-answer-${node.id}`} className="mb-2 block text-sm font-medium text-gray-700">
|
||||
Your response
|
||||
</label>
|
||||
<textarea
|
||||
id={`rw-answer-${node.id}`}
|
||||
value={focusedAnswer}
|
||||
onChange={(e) => setFocusedAnswer(e.target.value)}
|
||||
rows={4}
|
||||
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?"
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const targetNodeId = focusedPresentationItemId;
|
||||
handleDeconstructSubmit(targetNodeId, focusedAnswer);
|
||||
}}
|
||||
disabled={!focusedAnswer.trim() || processingStep === "active"}
|
||||
style={{ cursor: !focusedAnswer.trim() || processingStep === "active" ? "not-allowed" : "pointer" }}
|
||||
className="mt-3 rounded-lg border border-green-600 bg-white px-4 py-2 text-sm font-medium text-green-700 hover:bg-green-50 transition disabled:opacity-50"
|
||||
>
|
||||
Submit response
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deconstruction loading */}
|
||||
{processingStep === "active" && (
|
||||
<p className="text-sm text-blue-600/70">{deconstructMsg}</p>
|
||||
)}
|
||||
|
||||
{/* Focused result — structured response */}
|
||||
{focused?.result && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
What we learned
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{focused.result.observations.map((o, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Still unclear
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{focused.result.uncertainties.map((u, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{u}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{focused.result.assumptions && focused.result.assumptions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Assumptions in this response
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{focused.result.assumptions.map((a, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{focused.result.relationships && focused.result.relationships.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Connections
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{focused.result.relationships.map((r, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{focused.result.possibleFollowUpQuestions && focused.result.possibleFollowUpQuestions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Questions this raises
|
||||
</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{focused.result.possibleFollowUpQuestions.map((q, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed text-gray-700">{q}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Formulation or deconstruction failure */}
|
||||
{focused?.error && processingStep !== "active" && (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
We were unable to process your request right now. Please try again later.
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
retryFormulation();
|
||||
}}
|
||||
className="ml-2 font-medium underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer controls — horizontally separated (RTO.26B) */}
|
||||
<div className="mt-4 mb-3 flex items-center justify-between gap-4">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFocusedPresentationItemId(null);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="text-sm text-gray-400 underline hover:text-gray-600 transition whitespace-nowrap"
|
||||
>
|
||||
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 whitespace-nowrap"
|
||||
>
|
||||
Done for now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Done for now — separate visual area */}
|
||||
{doneForNowIds.length > 0 && (
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<h3 className="mb-2 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Done for now
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{graph.nodes.filter(
|
||||
(n) => doneForNowIds.includes(n.id)
|
||||
).map((node) => (
|
||||
<div key={node.id} className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3">
|
||||
<p className="text-sm text-gray-500 leading-snug">{node.label}</p>
|
||||
<button
|
||||
onClick={() => setDoneForNowIds(doneForNowIds.filter(id => id !== node.id))}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="mt-2 rounded border border-gray-300 px-3 py-1 text-xs font-medium text-gray-500 hover:bg-white transition"
|
||||
>
|
||||
Reopen
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* Only shown when NOT in experiment mode (experiment uses BranchNotebookContent) */}
|
||||
{!experimentalBranches || experimentalBranches.length === 0 ? (
|
||||
<OpenQuestionsPanel
|
||||
graph={graph}
|
||||
selectedPresentationItemId={selectedPresentationItemId}
|
||||
focusedPresentationItemId={focusedPresentationItemId}
|
||||
hasFocusedContent={hasFocusedContent}
|
||||
focused={getFocusedInvestigation()}
|
||||
formulationStep={formulationStep}
|
||||
formulateMsg={formulateMsg}
|
||||
processingStep={processingStep}
|
||||
deconstructMsg={deconstructMsg}
|
||||
doneForNowIds={doneForNowIds}
|
||||
startFocused={startFocused}
|
||||
handleDeconstructSubmit={handleDeconstructSubmit}
|
||||
retryFormulation={retryFormulation}
|
||||
setSelectedPresentationItemId={setSelectedPresentationItemId}
|
||||
setFocusedPresentationItemId={setFocusedPresentationItemId}
|
||||
setFocusedAnswer={setFocusedAnswer}
|
||||
focusedAnswer={focusedAnswer}
|
||||
setDoneForNowIds={setDoneForNowIds}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Terminal state */}
|
||||
{status === "success" && !hasSelectedQuestion && (
|
||||
@@ -1144,7 +1150,12 @@ export default function ReasoningWorkspace({
|
||||
<CompletionCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
||||
)}
|
||||
{!genuineCompletion && (
|
||||
<EvidenceLimitCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
||||
experimentalBranches && experimentalBranches.length > 0 ? (
|
||||
// In experiment mode: quiet facilitator state, not evidence limit verdict
|
||||
<QuietStateCard />
|
||||
) : (
|
||||
<EvidenceLimitCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -226,7 +226,8 @@ export default function ScenarioForm() {
|
||||
/* ── RTO.25A — passive late-result branch switcher (experimental) ── */
|
||||
|
||||
const [activeBranchId, setActiveBranchId] = useState("branch-b");
|
||||
const [branchNewResults, setBranchNewResults] = useState({});
|
||||
// Pre-seed Competitor development with a late result for RTO.27A testing
|
||||
const [branchNewResults, setBranchNewResults] = useState({ "branch-a": true });
|
||||
|
||||
/* ── RTO.26B — experimental branch-scoped reasoning fixture ───── */
|
||||
|
||||
@@ -262,6 +263,12 @@ export default function ScenarioForm() {
|
||||
return branchScoped.getBranchContributions(activeBranch.id);
|
||||
}, [activeBranch, branchScoped]);
|
||||
|
||||
// RTO.27A — late results for active branch
|
||||
const experimentalBranchLateResults = useMemo(() => {
|
||||
if (!activeBranch) return [];
|
||||
return (branchScoped.getBranchLateResults?.(activeBranch.id) || []).map(lr => ({ text: lr.text }));
|
||||
}, [activeBranch, branchScoped]);
|
||||
|
||||
// Determine which non-active branches have new results for passive indicator
|
||||
const inactiveBranchNewResults = useMemo(() => {
|
||||
const result = {};
|
||||
@@ -463,6 +470,7 @@ export default function ScenarioForm() {
|
||||
experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined}
|
||||
branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined}
|
||||
branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined}
|
||||
branchLocalLateResults={experimentalBranchLateResults.length > 0 ? experimentalBranchLateResults : undefined}
|
||||
inactiveBranchNewResults={Object.keys(inactiveBranchNewResults).length > 0 ? inactiveBranchNewResults : undefined}
|
||||
onRestart={() => { setStatus("idle"); setResult(null); setScenario(""); }}
|
||||
/>
|
||||
@@ -659,6 +667,7 @@ export default function ScenarioForm() {
|
||||
experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined}
|
||||
branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined}
|
||||
branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined}
|
||||
branchLocalLateResults={experimentalBranchLateResults.length > 0 ? experimentalBranchLateResults : undefined}
|
||||
inactiveBranchNewResults={Object.keys(inactiveBranchNewResults).length > 0 ? inactiveBranchNewResults : undefined}
|
||||
onRestart={() => {
|
||||
clearSession();
|
||||
|
||||
Reference in New Issue
Block a user