fix(confidence-engine): preserve follow-up progression ownership
This commit is contained in:
@@ -149,6 +149,20 @@ function FocusedQuestionBody({
|
||||
// Without this guard, selecting a follow-up question would erase "Previously answered" + "Your response".
|
||||
const hasCompletedContext = processingStep !== "active" && Boolean(focused?.result);
|
||||
|
||||
// ── Source of completed context: latest canonical Contribution when follow-up is active ──
|
||||
// After setFollowUpQuestion() mutates focused.question/answer, derive from the
|
||||
// latest completed Contribution so the narrative remains correct.
|
||||
const hasActiveFollowUp = hasCompletedContext && !hasAnswer
|
||||
&& (focused.result?.possibleFollowUpQuestions || []).some((q) => q === focused?.question);
|
||||
const latestCompletedContrib = [...(focusedContributions || [])].reverse().find((c) => c?.question && c?.answer);
|
||||
|
||||
const displayedCompletedQuestion = hasActiveFollowUp
|
||||
? (latestCompletedContrib?.question ?? focused?.question)
|
||||
: focused?.question;
|
||||
const displayedCompletedAnswer = hasActiveFollowUp
|
||||
? (latestCompletedContrib?.answer ?? focused?.answer ?? "")
|
||||
: focused?.answer;
|
||||
|
||||
// ── Local correction state (FQB-owned, not propagated upward) ─
|
||||
const [editingFindingId, setEditingFindingId] = useState(null);
|
||||
const [draft, setDraft] = useState("");
|
||||
@@ -180,15 +194,15 @@ function FocusedQuestionBody({
|
||||
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
{(hasAnswer || hasCompletedContext) && focused?.question?.trim() ? (
|
||||
<div className="space-y-3">
|
||||
{/* Previously answered question */}
|
||||
{/* Previously answered question — sourced from contribution when follow-up is active */}
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Previously answered</h3>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">{focused.question}</p>
|
||||
<p className="text-base font-medium leading-relaxed text-gray-900">{displayedCompletedQuestion}</p>
|
||||
</div>
|
||||
{/* User's verbatim response — distinct provenance from Engine-derived content */}
|
||||
<div>
|
||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Your response</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-800">{focused.answer}</p>
|
||||
<p className="text-sm leading-relaxed text-gray-800">{displayedCompletedAnswer}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : focused?.question?.trim() ? (
|
||||
@@ -200,7 +214,7 @@ function FocusedQuestionBody({
|
||||
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
||||
) : null}
|
||||
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && (
|
||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && !hasActiveFollowUp && (
|
||||
<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?" />
|
||||
@@ -212,9 +226,8 @@ function FocusedQuestionBody({
|
||||
|
||||
{focused?.result && (
|
||||
<>
|
||||
{/* Prior accumulated learning (prior turns, current turn excluded — shown above) */}
|
||||
<PriorContributionsSummary nodeId={nodeId} contributions={focusedContributions || []} findings={findings} />
|
||||
|
||||
{/* Prior accumulated learning removed from left pane — SecondaryPreviousLearning on the right owns historical Previous Learning exclusively */}
|
||||
{/* PriorContributionsSummary was causing duplication in the two-column focused workspace */}
|
||||
<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-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||
const disposition = isFinding ? item.userDisposition : null;
|
||||
@@ -284,6 +297,30 @@ function FocusedQuestionBody({
|
||||
) : (
|
||||
<p className="text-xs text-gray-400">None yet</p>
|
||||
)}
|
||||
|
||||
{/* In-place answer textarea for the active follow-up — renders only when a candidate is selected */}
|
||||
{hasActiveFollowUp ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
<p className="text-sm font-medium text-gray-900">{focused.question}</p>
|
||||
<textarea
|
||||
id={`rw-answer-fu-${nodeId}`}
|
||||
value={focusedAnswer}
|
||||
onChange={(e) => setFocusedAnswer(e.target.value)}
|
||||
rows={4}
|
||||
data-testid="follow-up-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="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>
|
||||
) : null}
|
||||
</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>
|
||||
@@ -1074,7 +1111,7 @@ function FocusedInvestigationWorkspace({
|
||||
setFocusedPresentationItemId={setFocusedPresentationItemId}
|
||||
setDoneForNowIds={setDoneForNowIds}
|
||||
setFollowUpQuestion={setFollowUpQuestion}
|
||||
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
||||
focusedContributions={focusedContributions || []}
|
||||
currentFindings={currentFindings || []}
|
||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||
@@ -1208,6 +1245,9 @@ function OpenQuestionsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
// Export for testability of in-place follow-up ownership repair
|
||||
export { FocusedQuestionBody, SecondaryPreviousLearning };
|
||||
|
||||
export default function ReasoningWorkspace({
|
||||
scenario,
|
||||
status,
|
||||
|
||||
Reference in New Issue
Block a user