feat(confidence-engine): stabilize user-directed investigation flow
Intentional changes in this checkpoint: - Deconstruct route: use body.targetNodeId (client identity) over raw.model-invented ID - ThreadContributionsBadge: compact per-thread contribution indicator with expandable history - Reopen continuation: resume from accumulated contributions instead of reformulating - showEvidenceLimit gate: hide evidence-limit card during active investigation paths - Evidence-limit visibility correction in rendering pipeline - Section ordering: assumptions and connections after 'Still unclear' in focused result - Prompt v0.3: preserve user-stated alternatives as separate unknowns; no count inflation - 3 durable regression tests (target identity, contribution persistence, reopen state) - evidence-limit card visibility gate test suite Temporary residue removed: - test-analysis.mjs (scratch diagnostic) - 5 diagnostic console.log blocks from reasoning-workspace.jsx
This commit is contained in:
+264
-138
@@ -310,6 +310,88 @@ function EvidenceLimitCard({ summary }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Per-thread contribution badge (compact indicator) ──────────────
|
||||
|
||||
function ThreadContributionsBadge({ nodeId, contributions }) {
|
||||
const threadContribs = contributions.filter((c) => c.targetNodeId === nodeId);
|
||||
if (!threadContribs.length) return null;
|
||||
|
||||
// Show most recent contribution summary inline
|
||||
const latest = threadContribs[threadContribs.length - 1];
|
||||
|
||||
const nonEmptyGroups = [];
|
||||
for (const key of ["observations", "uncertainties", "assumptions", "relationships"]) {
|
||||
const arr = latest[key];
|
||||
if (Array.isArray(arr) && arr.length > 0) nonEmptyGroups.push(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{/* Thread learning indicator — collapsed by default; user can expand to inspect history */}
|
||||
<details open={false} className="rounded-lg border border-gray-200/80 bg-white/60">
|
||||
<summary className="cursor-pointer px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-gray-800 select-none">
|
||||
📝 {threadContribs.length} learned contribution{threadContribs.length !== 1 ? "s" : ""}
|
||||
</summary>
|
||||
<div className="px-3 pb-3 pt-1 space-y-4">
|
||||
{/* All contributions listed in order */}
|
||||
{threadContribs.map((c, idx) => (
|
||||
<div key={c.id || idx} className="space-y-2">
|
||||
{idx > 0 && <div className="text-[9px] text-gray-400 tracking-wider uppercase mt-3">Contribution #{c.sequence || idx + 1}</div>}
|
||||
{/* What this tells us */}
|
||||
{c.observations?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.observations.map((o, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{o}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Still unclear */}
|
||||
{c.uncertainties?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Still unclear</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.uncertainties.map((u, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{u}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Assumptions */}
|
||||
{c.assumptions?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.assumptions.map((a, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{a}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Connections */}
|
||||
{c.relationships?.length ? (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold tracking-widest uppercase text-gray-500">Connections</h4>
|
||||
<ul className="list-disc pl-5 space-y-0.5">
|
||||
{c.relationships.map((r, i) => (
|
||||
<li key={i} className="text-xs leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Quiet facilitator state (experiment mode) ─────────────────────
|
||||
|
||||
function QuietStateCard() {
|
||||
@@ -710,11 +792,12 @@ function OpenQuestionsPanel({
|
||||
focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds,
|
||||
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
|
||||
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
||||
setFollowUpQuestion,
|
||||
setFollowUpQuestion, focusedContributions,
|
||||
}) {
|
||||
const openNodes = (graph?.nodes || []).filter(
|
||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||
);
|
||||
|
||||
if (openNodes.length <= 1) return null;
|
||||
|
||||
return (
|
||||
@@ -788,8 +871,6 @@ function OpenQuestionsPanel({
|
||||
<>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</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>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</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>
|
||||
<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>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Questions this raises</h3>
|
||||
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
|
||||
<div className="space-y-1 mt-1">
|
||||
@@ -809,6 +890,8 @@ function OpenQuestionsPanel({
|
||||
<p className="text-xs text-gray-400">None yet</p>
|
||||
)}
|
||||
</div>
|
||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -825,6 +908,9 @@ function OpenQuestionsPanel({
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Thread contributions for this node */}
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -836,9 +922,10 @@ function OpenQuestionsPanel({
|
||||
<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">
|
||||
<div key={node.id} className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-4 py-3 space-y-1">
|
||||
<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>
|
||||
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} />
|
||||
<button onClick={() => setDoneForNowIds(doneForNowIds.filter(id => id !== node.id))} style={{ cursor: "pointer" }} className="mt-1 rounded border border-gray-300 px-3 py-1 text-xs font-medium text-gray-500 hover:bg-white transition">Reopen</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -930,6 +1017,7 @@ export default function ReasoningWorkspace({
|
||||
|
||||
const graph = result?.situationGraph ?? null;
|
||||
const hasGraph = Boolean(graph);
|
||||
|
||||
const diagnostics = result?.diagnostics ?? null;
|
||||
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
|
||||
const genuineCompletion = hasGenuineCompletion(graph);
|
||||
@@ -1020,13 +1108,56 @@ export default function ReasoningWorkspace({
|
||||
|
||||
const focused = getFocusedInvestigation();
|
||||
|
||||
// Gate evidence-limit card: do NOT show when active investigation paths remain.
|
||||
const showEvidenceLimit = !(
|
||||
processingStep === "active" ||
|
||||
(focused?.question?.trim() && !processingStep) ||
|
||||
(hasGraph && !genuineCompletion)
|
||||
);
|
||||
|
||||
// ── RTO.13B — workflow handlers ──────────────────────────────
|
||||
|
||||
function startFocused(nodeId) {
|
||||
const target = nodeId || focusedPresentationItemId;
|
||||
if (!target) return;
|
||||
|
||||
const priorContribs = (focusedContributions || []).filter(
|
||||
(c) => c.targetNodeId === target,
|
||||
);
|
||||
|
||||
setFocusedPresentationItemId(target);
|
||||
setFocusedAnswer("");
|
||||
|
||||
if (priorContribs.length > 0) {
|
||||
// Reopen path: resume from accumulated contribution history.
|
||||
// Do NOT call doFormulate — the user's prior investigation direction
|
||||
// is preserved; only follow-up questions surface for explicit selection.
|
||||
const latest = priorContribs[priorContribs.length - 1];
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[target]: {
|
||||
status: "formulated",
|
||||
question: latest.question || "",
|
||||
answer: latest.answer ?? null,
|
||||
result: latest.possibleFollowUpQuestions
|
||||
? {
|
||||
observations: latest.observations || [],
|
||||
uncertainties: latest.uncertainties || [],
|
||||
assumptions: latest.assumptions || [],
|
||||
relationships: latest.relationships || [],
|
||||
possibleFollowUpQuestions: latest.possibleFollowUpQuestions,
|
||||
}
|
||||
: null,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
|
||||
setFormulationStep("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fresh thread path — unchanged original behaviour.
|
||||
setFormulationStep("active");
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
@@ -1103,6 +1234,7 @@ export default function ReasoningWorkspace({
|
||||
});
|
||||
|
||||
setProcessingStep("idle");
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[targetNodeId]: { ...prev[targetNodeId], result: data, answer: answerText, error: null },
|
||||
@@ -1230,68 +1362,24 @@ export default function ReasoningWorkspace({
|
||||
{/* ── RTO.29D — initial post-Analyse reflection ──────── */}
|
||||
{postAnalyseStatus === "success" && (
|
||||
<div className="space-y-6" data-testid="initial-reflection-surface">
|
||||
{/* Current Understanding + Situation — prominent two-column orienting surface */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className={`space-y-6 ${hasGraph ? 'lg:col-span-2' : 'lg:col-span-full'}`}>
|
||||
{/* Current Understanding — prominent orienting surface */}
|
||||
<div className="rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm">
|
||||
<h2 className="mb-4 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/60">
|
||||
Current Understanding
|
||||
</h2>
|
||||
<p className="text-lg leading-relaxed text-gray-800">{propUnderstanding}</p>
|
||||
</div>
|
||||
|
||||
{/* Initial proposed findings — unknowns + plausible interpretations from reconstruction */}
|
||||
<div className="space-y-3" data-testid="initial-proposed-findings">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Open Questions
|
||||
</h2>
|
||||
{(() => {
|
||||
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
|
||||
// Surface only candidate items from the semantic reconstruction that are worth investigating:
|
||||
// — unknowns (importantUnknowns from the LLM's reconstruction)
|
||||
// — assumptions (plausibleInterpretations from the LLM's reconstruction)
|
||||
// Skips observations, states, relationships, transitions — these are already established facts/context.
|
||||
// Both kinds check status !== "resolved" and excluded resolvedIds to mirror OpenQuestionsPanel logic.
|
||||
const candidateKinds = ["unknown", "assumption"];
|
||||
return (
|
||||
(graph?.nodes || [])
|
||||
.filter(
|
||||
(n) =>
|
||||
candidateKinds.includes(n.kind) &&
|
||||
n.status !== "resolved" &&
|
||||
!resolvedIds.has(n.id),
|
||||
)
|
||||
.map((node) => {
|
||||
const tag = node.kind === "assumption" ? "Plausible interpretation" : "Unclear";
|
||||
return (
|
||||
<button
|
||||
key={node.id}
|
||||
onClick={() => startFocused(node.id)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
<span className="block text-sm leading-relaxed text-gray-900">{node.label}</span>
|
||||
{node.description && node.description !== node.label && (
|
||||
<p className="mt-1.5 text-xs leading-snug text-gray-500">{node.description}</p>
|
||||
)}
|
||||
<span className="mt-2 block text-[10px] uppercase tracking-wider text-gray-400">{tag}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{/* Current Understanding + Situation — independent vertical flow */}
|
||||
<div className="flex gap-3 items-start flex-wrap">
|
||||
{/* Current Understanding — prominent orienting surface */}
|
||||
<div className={`rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm flex-1 min-w-0 ${hasGraph ? 'lg:max-w-2xl' : ''}`}>
|
||||
<h2 className="mb-4 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/60">
|
||||
Current Understanding
|
||||
</h2>
|
||||
<p className="text-lg leading-relaxed text-gray-800">{propUnderstanding}</p>
|
||||
</div>
|
||||
|
||||
{/* Situation panel during initial reflection */}
|
||||
{hasGraph && (
|
||||
<div className="space-y-6 lg:col-span-1">
|
||||
<div className="space-y-6 lg:max-w-xs">
|
||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />
|
||||
</div>
|
||||
)}
|
||||
{!hasGraph && scenario && (
|
||||
<div className="space-y-6 lg:col-span-1">
|
||||
<div className="space-y-6 lg:max-w-xs">
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-5 py-4">
|
||||
<h2 className="mb-2 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Situation
|
||||
@@ -1303,20 +1391,70 @@ export default function ReasoningWorkspace({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Initial proposed findings — unknowns + plausible interpretations from reconstruction */}
|
||||
<div className="space-y-3" data-testid="initial-proposed-findings">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Open Questions
|
||||
</h2>
|
||||
{(() => {
|
||||
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
|
||||
// Surface only candidate items from the semantic reconstruction that are worth investigating:
|
||||
// — unknowns (importantUnknowns from the LLM's reconstruction)
|
||||
// — assumptions (plausibleInterpretations from the LLM's reconstruction)
|
||||
// Skips observations, states, relationships, transitions — these are already established facts/context.
|
||||
// Both kinds check status !== "resolved" and excluded resolvedIds to mirror OpenQuestionsPanel logic.
|
||||
const candidateKinds = ["unknown", "assumption"];
|
||||
return (
|
||||
(graph?.nodes || [])
|
||||
.filter(
|
||||
(n) =>
|
||||
candidateKinds.includes(n.kind) &&
|
||||
n.status !== "resolved" &&
|
||||
!resolvedIds.has(n.id),
|
||||
)
|
||||
.map((node) => {
|
||||
const tag = node.kind === "assumption" ? "Plausible interpretation" : "Unclear";
|
||||
return (
|
||||
<button
|
||||
key={node.id}
|
||||
onClick={() => startFocused(node.id)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
<span className="block text-sm leading-relaxed text-gray-900">{node.label}</span>
|
||||
{node.description && node.description !== node.label && (
|
||||
<p className="mt-1.5 text-xs leading-snug text-gray-500">{node.description}</p>
|
||||
)}
|
||||
<span className="mt-2 block text-[10px] uppercase tracking-wider text-gray-400">{tag}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* ── Workspace grid: persistent whenever a graph exists ─── */}
|
||||
{hasGraph && (
|
||||
<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'}`}>
|
||||
|
||||
{/* Current Understanding — independent row, full-width of left area (cols 1-2) */}
|
||||
{propUnderstanding && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
|
||||
<div className="lg:row-start-1 lg:col-span-full rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm">
|
||||
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Left column below Understanding: Investigation + Open Questions */}
|
||||
<div className="lg:row-start-2 lg:col-start-1 lg:col-span-2 space-y-6">
|
||||
{/* Current investigation (prominent hero section) */}
|
||||
{postAnalyseStatus !== "success" && (
|
||||
<CurrentInvestigationCard selectedQuestion={result?.selectedQuestion} graph={graph} />
|
||||
)}
|
||||
|
||||
{/* ── Open questions (case workspace) — no ranking bias ── */}
|
||||
{postAnalyseStatus !== "success" && (
|
||||
<OpenQuestionsPanel
|
||||
graph={graph}
|
||||
@@ -1338,11 +1476,12 @@ export default function ReasoningWorkspace({
|
||||
focusedAnswer={focusedAnswer}
|
||||
setDoneForNowIds={setDoneForNowIds}
|
||||
setFollowUpQuestion={setFollowUpQuestion}
|
||||
focusedContributions={focusedContributions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Terminal state — suppressed during initial reflection */}
|
||||
{postAnalyseStatus !== "success" && status === "success" && !hasSelectedQuestion && (
|
||||
{postAnalyseStatus !== "success" && status === "success" && !hasSelectedQuestion && showEvidenceLimit && (
|
||||
<>
|
||||
{genuineCompletion && (
|
||||
<CompletionCard summary={resolveCurrentSummary(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary)} />
|
||||
@@ -1357,84 +1496,71 @@ export default function ReasoningWorkspace({
|
||||
<InvestigationHistory turns={investigationHistory} />
|
||||
)}
|
||||
|
||||
{/* Supporting context within conversation lane */}
|
||||
{hasCurrentSummaryCondition && (
|
||||
<>
|
||||
{postAnalyseStatus !== "success" && (
|
||||
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding || null} />
|
||||
)}
|
||||
{/* ── Experiment 12: progress panel A / B / C toggle (temporary experimental UI) — de-emphasised by branch-as-context experiment RTO.25D */}
|
||||
{hasGraph && (
|
||||
<div className="hidden space-y-2">
|
||||
<div className="flex items-center gap-2" role="radiogroup" aria-label="Progress panel variant">
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "a"}
|
||||
onClick={() => setPanelVariant("a")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("b");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("c");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "a" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
A
|
||||
</button>
|
||||
<span className="text-gray-300">/</span>
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "b"}
|
||||
onClick={() => setPanelVariant("b")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("c");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("a");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "b" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
B
|
||||
</button>
|
||||
<span className="text-gray-300">/</span>
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "c"}
|
||||
onClick={() => setPanelVariant("c")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("a");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("b");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "c" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
C (exp)
|
||||
</button>
|
||||
</div>
|
||||
<div className="opacity-75">
|
||||
{panelVariant === "a"
|
||||
? <InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
||||
: panelVariant === "b"
|
||||
? <InvestigationSummaryPanelV2 graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
||||
: <InvestigationSummaryPanelV3 graph={graph} selectedQuestion={selectedQ} result={result} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{/* ── Experiment 12: progress panel A / B / C toggle (temporary experimental UI) — de-emphasised by branch-as-context experiment RTO.25D */}
|
||||
{hasGraph && (
|
||||
<div className="hidden space-y-2">
|
||||
<div className="flex items-center gap-2" role="radiogroup" aria-label="Progress panel variant">
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "a"}
|
||||
onClick={() => setPanelVariant("a")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("b");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("c");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "a" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
A
|
||||
</button>
|
||||
<span className="text-gray-300">/</span>
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "b"}
|
||||
onClick={() => setPanelVariant("b")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("c");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("a");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "b" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
B
|
||||
</button>
|
||||
<span className="text-gray-300">/</span>
|
||||
<button
|
||||
role="radio"
|
||||
aria-checked={panelVariant === "c"}
|
||||
onClick={() => setPanelVariant("c")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowRight") setPanelVariant("a");
|
||||
if (e.key === "ArrowLeft") setPanelVariant("b");
|
||||
}}
|
||||
className={`text-xs transition ${panelVariant === "c" ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
|
||||
>
|
||||
C (exp)
|
||||
</button>
|
||||
</div>
|
||||
<div className="opacity-75">
|
||||
{panelVariant === "a"
|
||||
? <InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
||||
: panelVariant === "b"
|
||||
? <InvestigationSummaryPanelV2 graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
|
||||
: <InvestigationSummaryPanelV3 graph={graph} selectedQuestion={selectedQ} result={result} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right lane: stable supporting reference ───────── */}
|
||||
{/* ── Right lane: stable supporting reference (independent column) ───────── */}
|
||||
{hasCurrentSummaryCondition && (
|
||||
<div className="space-y-6 lg:col-span-1">
|
||||
{/* RTO.26B — situation card from scenario text when no graph */}
|
||||
{!graph && scenario && (
|
||||
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-5 py-4">
|
||||
<h2 className="mb-2 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Situation
|
||||
</h2>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-gray-600">
|
||||
{scenario}
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:row-start-2 lg:col-start-3 space-y-6">
|
||||
{/* Situation — always here when condition met, independent of left column height */}
|
||||
{(scenario || graph?.centralStatement) && (
|
||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement || scenario} />
|
||||
)}
|
||||
{!propUnderstanding && graph && postAnalyseStatus !== "success" && (
|
||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />
|
||||
)}
|
||||
{graph && postAnalyseStatus !== "success" && <OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />}
|
||||
{/* RTO.25B — temporarily hidden to reduce competing navigation while branch-experiment is active */}
|
||||
<div className="hidden">
|
||||
<InvestigationMap turnCount={investigationHistory.length} />
|
||||
|
||||
Reference in New Issue
Block a user