feat(confidence-engine): stabilize investigation workspace with semantic decomposition and deterministic presentation anchors
This commit is contained in:
@@ -798,7 +798,7 @@ function OpenQuestionsPanel({
|
||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||
);
|
||||
|
||||
if (openNodes.length <= 1) return null;
|
||||
if (openNodes.length <= 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -1178,6 +1178,11 @@ export default function ReasoningWorkspace({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || "Formulation failed");
|
||||
|
||||
// Deterministic presentation anchor: explicitly set the focused item to the
|
||||
// target node so formulation success always renders correctly.
|
||||
setFocusedPresentationItemId(nodeId);
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: { ...prev[nodeId], question: data.question, status: "formulated", error: null },
|
||||
@@ -1235,12 +1240,20 @@ export default function ReasoningWorkspace({
|
||||
|
||||
setProcessingStep("idle");
|
||||
|
||||
// Deterministic presentation anchor: explicitly set the focused item to the
|
||||
// target node so the post-API success path always renders the correct state
|
||||
// regardless of render timing or concurrent parent updates.
|
||||
setFocusedPresentationItemId(targetNodeId);
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[targetNodeId]: { ...prev[targetNodeId], result: data, answer: answerText, error: null },
|
||||
}));
|
||||
} catch (err) {
|
||||
setProcessingStep("idle");
|
||||
|
||||
setFocusedPresentationItemId(targetNodeId);
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[targetNodeId]: { ...prev[targetNodeId], result: null, error: err.message || "Deconstruction failed" },
|
||||
@@ -1267,6 +1280,10 @@ export default function ReasoningWorkspace({
|
||||
function setFollowUpQuestion(followUpText) {
|
||||
const target = focusedPresentationItemId;
|
||||
if (!target || !followUpText?.trim()) return;
|
||||
|
||||
// Deterministic presentation anchor: explicitly set after follow-up selection.
|
||||
setFocusedPresentationItemId(target);
|
||||
|
||||
setFocusedInvestigations((prev) => ({
|
||||
...prev,
|
||||
[target]: { ...prev[target], question: followUpText.trim(), answer: null },
|
||||
@@ -1393,46 +1410,75 @@ export default function ReasoningWorkspace({
|
||||
</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(
|
||||
<div className="space-y-6" data-testid="initial-proposed-findings">
|
||||
{(() => {
|
||||
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
|
||||
|
||||
// Open Questions: unresolved unknown nodes only (investigable)
|
||||
const openUnknowns = (graph?.nodes || []).filter(
|
||||
(n) =>
|
||||
candidateKinds.includes(n.kind) &&
|
||||
n.kind === "unknown" &&
|
||||
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>
|
||||
);
|
||||
|
||||
// Possible Interpretations: unresolved assumption nodes only (informational, not investigable)
|
||||
const possibleInterpretations = (graph?.nodes || []).filter(
|
||||
(n) =>
|
||||
n.kind === "assumption" &&
|
||||
n.status !== "resolved" &&
|
||||
!resolvedIds.has(n.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* OPEN QUESTIONS — unknown nodes (clickable → focused investigation) */}
|
||||
{openUnknowns.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Open Questions
|
||||
</h2>
|
||||
{openUnknowns.map((node) => (
|
||||
<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">Unclear</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* POSSIBLE INTERPRETATIONS — assumption nodes (informational, not investigable) */}
|
||||
{possibleInterpretations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Possible Interpretations
|
||||
</h2>
|
||||
{possibleInterpretations.map((node) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className="w-full text-left rounded-lg border border-blue-100 bg-blue-50/40 px-5 py-4"
|
||||
>
|
||||
<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-blue-400">Plausible interpretation</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1448,9 +1494,8 @@ export default function ReasoningWorkspace({
|
||||
</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) */}
|
||||
{/* Left column: Investigation + Open Questions (rows 2-3, columns 1-2) */}
|
||||
<div className="lg:row-start-2 lg:row-end-4 lg:col-start-1 lg:col-span-2 space-y-6">
|
||||
{postAnalyseStatus !== "success" && (
|
||||
<CurrentInvestigationCard selectedQuestion={result?.selectedQuestion} graph={graph} />
|
||||
)}
|
||||
@@ -1551,22 +1596,48 @@ export default function ReasoningWorkspace({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right lane: stable supporting reference (independent column) ───────── */}
|
||||
{hasCurrentSummaryCondition && (
|
||||
<div className="lg:row-start-2 lg:col-start-3 space-y-6">
|
||||
{/* Right lane: stable supporting reference (independent column) */}
|
||||
{(scenario || graph?.centralStatement) && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
|
||||
<div className="lg:row-start-2 lg:row-end-4 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} />
|
||||
)}
|
||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement || scenario} />
|
||||
{/* RTO.25B — temporarily hidden to reduce competing navigation while branch-experiment is active */}
|
||||
<div className="hidden">
|
||||
<InvestigationMap turnCount={investigationHistory.length} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Possible Interpretations — persistent provisional hypothesis cards (spans full workspace width, below Investigation) */}
|
||||
{(() => {
|
||||
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
|
||||
const interpretationNodes = (graph?.nodes || []).filter(
|
||||
(n) =>
|
||||
n.kind === "assumption" &&
|
||||
n.status !== "resolved" &&
|
||||
!resolvedIds.has(n.id),
|
||||
);
|
||||
|
||||
return postAnalyseStatus !== "success" && interpretationNodes.length > 0 ? (
|
||||
<div className="lg:row-start-4 lg:col-span-full space-y-3">
|
||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||
Possible Interpretations
|
||||
</h2>
|
||||
{interpretationNodes.map((node) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className="w-full text-left rounded-lg border border-blue-100 bg-blue-50/40 px-5 py-4"
|
||||
>
|
||||
<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-blue-400">Plausible interpretation</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user