fix(confidence-engine): project processing indicator to active follow-up block

Repair LOCATION-A defect where processing feedback rendered near the
completed narrative instead of inside the active follow-up block.

Changes:
  - components/reasoning-workspace.jsx: three targeted edits using a single
    spinner component with conditional rendering; hasActiveFollowUp routes
    ownership to the correct container
  - tests/open-questions-vs-assumptions.test.jsx: regression test confirming
    exactly one indicator, DOM child of follow-up-block, ownership separation

Accepted criteria met:
   Exactly one processing indicator during follow-up processing
   Indicator is a DOM child of follow-up-block
   Top-level indicator suppressed when follow-up active
   Initial answer flow preserved (top-level when no follow-up)
   Successful follow-up promotion intact
   All existing context retained
   No new state/lifecycle changes/error redesign
This commit is contained in:
2026-08-30 11:09:24 +01:00
parent bf7629691f
commit 922f58a49f
3 changed files with 161 additions and 5 deletions
+12 -5
View File
@@ -229,19 +229,19 @@ function FocusedQuestionBody({
) : null}
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && !hasActiveFollowUp && (
<div>
<div data-testid="completed-narrative">
<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="flex items-center gap-2 text-sm text-blue-600/70">
{processingStep === "active" && !hasActiveFollowUp && (
<div data-testid="processing-indicator" className="flex items-center gap-2 text-sm text-blue-600/70">
<svg className="h-4 w-4 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
<span className="sr-only">Processing:</span>
{deconstructMsg}
</p>
</div>
)}
{(hasActiveFollowUp || hasCompletedContext) && (
@@ -343,7 +343,7 @@ function FocusedQuestionBody({
{/* In-place answer textarea for the active follow-up */}
{hasActiveFollowUp ? (
<div className="mt-3 space-y-2">
<div className="mt-3 space-y-2" data-testid="follow-up-block">
<p className="text-sm font-medium text-gray-900">{focused.question}</p>
<textarea
id={`rw-answer-fu-${nodeId}`}
@@ -363,6 +363,13 @@ function FocusedQuestionBody({
>
Submit response
</button>
{processingStep === "active" && (
<div data-testid="processing-indicator" className="mt-1 flex items-center gap-2 text-sm text-blue-600/70">
<svg className="h-4 w-4 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
<span className="sr-only">Processing:</span>
{deconstructMsg}
</div>
)}
</div>
) : null}
</div>