fix(confidence-engine): render focused investigation in current presentation

This commit is contained in:
2026-08-26 11:52:18 +01:00
parent 6288ef1031
commit fd02be0f29
2 changed files with 394 additions and 80 deletions
+175 -80
View File
@@ -148,6 +148,99 @@ function useSessionPersistence() {
return { saveSession, loadSession, clearSession, sessionReady: true };
}
// ── Shared focused-question body (extracted from OpenQuestionsPanel) ──
function FocusedQuestionBody({
nodeId,
isFocused,
hasCompletedInvestigation: hasCompleted,
focused,
formulationStep,
formulateMsg,
processingStep,
deconstructMsg,
focusedAnswer,
handleDeconstructSubmit,
retryFormulation,
setFocusedAnswer,
setSelectedPresentationItemId,
setFocusedPresentationItemId,
setDoneForNowIds,
setFollowUpQuestion,
}) {
return (
<>
{isFocused && (() => {
const hasContent = focused?.question?.trim() || formulationStep === "active" || processingStep === "active" || focused?.error;
if (!hasContent) return null;
return (
<div className="mt-4 space-y-4">
<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-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
<textarea id={`rw-answer-${nodeId}`} 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(nodeId, 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 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">Questions this raises</h3>
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
<div className="space-y-1 mt-1">
{focused.result.possibleFollowUpQuestions.map((q, i) => (
<button
key={i}
onClick={(e) => { e.stopPropagation(); setFollowUpQuestion(q); }}
style={{ cursor: "pointer" }}
className="w-full text-left rounded-lg border border-blue-200/60 bg-blue-50/40 px-3 py-2.5 text-sm leading-relaxed text-gray-800 hover:border-blue-300 hover:bg-blue-100/60 transition"
data-testid="follow-up-question"
>
{q} pick this question
</button>
))}
</div>
) : (
<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>
</>
)}
{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, nodeId]); }} 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>
);
})()}
</>
);
}
// ── Current understanding card ────────────────────────────────
// Evidence-limit text that must not appear inside Current understanding
@@ -855,72 +948,24 @@ function OpenQuestionsPanel({
</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 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">Questions this raises</h3>
{(focused.result.possibleFollowUpQuestions || []).length > 0 ? (
<div className="space-y-1 mt-1">
{focused.result.possibleFollowUpQuestions.map((q, i) => (
<button
key={i}
onClick={(e) => { e.stopPropagation(); setFollowUpQuestion(q); }}
style={{ cursor: "pointer" }}
className="w-full text-left rounded-lg border border-blue-200/60 bg-blue-50/40 px-3 py-2.5 text-sm leading-relaxed text-gray-800 hover:border-blue-300 hover:bg-blue-100/60 transition"
data-testid="follow-up-question"
>
{q} pick this question
</button>
))}
</div>
) : (
<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>
</>
)}
{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>
);
})()}
<FocusedQuestionBody
nodeId={node.id}
isFocused={isFocused}
hasCompletedInvestigation={() => hasCompletedInvestigation(node.id)}
focused={focused}
formulationStep={formulationStep}
formulateMsg={formulateMsg}
processingStep={processingStep}
deconstructMsg={deconstructMsg}
focusedAnswer={focusedAnswer}
handleDeconstructSubmit={handleDeconstructSubmit}
retryFormulation={retryFormulation}
setFocusedAnswer={setFocusedAnswer}
setSelectedPresentationItemId={setSelectedPresentationItemId}
setFocusedPresentationItemId={setFocusedPresentationItemId}
setDoneForNowIds={setDoneForNowIds}
setFollowUpQuestion={setFollowUpQuestion}
/>
{/* Thread contributions for this node */}
<ThreadContributionsBadge nodeId={node.id} contributions={focusedContributions || []} />
@@ -1469,20 +1514,70 @@ export default function ReasoningWorkspace({
<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>
))}
{openUnknowns.map((node) => {
const isFocused = focusedPresentationItemId === node.id;
const hasCompleted = Boolean(
focusedInvestigations[node.id] &&
focusedInvestigations[node.id].question
);
function handleNodeClick(n) {
if (hasCompleted && selectedPresentationItemId === n.id) {
setFocusedPresentationItemId(n.id);
return;
}
startFocused(n.id);
}
return (
<button
key={node.id}
onClick={() => handleNodeClick(node)}
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 leading-snug ${isFocused ? "text-sm font-medium text-gray-900" : "text-sm text-gray-600"}`}>{node.label}</span>
{node.description && node.description !== node.label && (
<p className="mt-1.5 text-xs leading-snug text-gray-500">{node.description}</p>
)}
{!isFocused && <span className="mt-2 block text-[10px] uppercase tracking-wider text-gray-400">Unclear</span>}
</button>
);
})}
{/* Focused content — rendered inline in the initial reflection surface */}
{(() => {
const activeForFocus = openUnknowns.find((n) => focusedPresentationItemId === n.id);
if (!activeForFocus) return null;
const node = activeForFocus;
const hasCompleted = Boolean(
focusedInvestigations[node.id] &&
focusedInvestigations[node.id].question
);
const isFocused = focusedPresentationItemId === node.id;
return (
<FocusedQuestionBody
nodeId={node.id}
isFocused={isFocused}
hasCompletedInvestigation={() => hasCompleted}
focused={getFocusedInvestigation()}
formulationStep={formulationStep}
formulateMsg={formulateMsg}
processingStep={processingStep}
deconstructMsg={deconstructMsg}
focusedAnswer={focusedAnswer}
handleDeconstructSubmit={handleDeconstructSubmit}
retryFormulation={retryFormulation}
setFocusedAnswer={setFocusedAnswer}
setSelectedPresentationItemId={setSelectedPresentationItemId}
setFocusedPresentationItemId={setFocusedPresentationItemId}
setDoneForNowIds={setDoneForNowIds}
setFollowUpQuestion={setFollowUpQuestion}
/>
);
})()}
</div>
)}