fix: keep textarea editable during half-question API call
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
531e7c435e
commit
6678f81283
|
|
@ -0,0 +1,133 @@
|
|||
# Debug Plan: Half Question Button Locks QueryBox
|
||||
|
||||
**Date:** 2026-05-18
|
||||
**Symptom:** After clicking "Half Question", the textarea appears locked (grayed out, cursor-not-allowed), raising concern that incoming ASR text chunks won't accumulate.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### The Disable Chain
|
||||
|
||||
```
|
||||
User clicks "Half Question"
|
||||
→ QueryInput.onClick: onHalfQuestion(trimmed)
|
||||
→ LTTPage.handleHalfQuestion: queryStream.decomposeOnly({ question })
|
||||
→ queries.tsx decomposeOnly: setState({ phase: 'decomposing' }) [IMMEDIATE]
|
||||
→ LTTPage: isLoading = true (phase !== 'idle' && !== 'completed' && !== 'error')
|
||||
→ QueryInput.textarea: disabled={isLoading} = true ← TEXTAREA LOCKS
|
||||
→ QueryInput.buttons: disabled={isDisabled} = true ← BUTTONS LOCK
|
||||
```
|
||||
|
||||
### What ACTUALLY Happens vs What User Perceives
|
||||
|
||||
| Concern | Reality |
|
||||
|---------|---------|
|
||||
| Text accumulation stops? | **No.** React controlled `value={displayValue}` updates even when `disabled`. ASR text continues to display. Playwright confirmed: 250→421 chars during Half Question processing. |
|
||||
| Buttons permanently locked? | **No.** When SSE `completed` event arrives, phase→`completed`, `isLoading`→`false`, all re-enabled. |
|
||||
| Textarea editable during wait? | **No.** This IS the issue. Textarea is `disabled={isLoading}`, so user cannot edit/select text during the 5-30s LLM decomposition call. |
|
||||
|
||||
### The Real Bug
|
||||
|
||||
The textarea should NOT be disabled during half-question processing. The half-question feature is meant to be non-blocking — users should be able to:
|
||||
|
||||
1. **Continue seeing ASR text accumulate** ✅ (works via controlled component)
|
||||
2. **Edit the accumulating text** ❌ (textarea disabled prevents typing/selection)
|
||||
3. **Click "Final Submit" later** ❌ (button disabled during API call)
|
||||
4. **Click "Half Question" again** ❌ (button disabled during API call)
|
||||
|
||||
**But** #2 is the primary concern. Users physically cannot interact with the textarea while the LLM decomposes the question — and they can't even see that text IS still accumulating because the grayed-out disabled style makes it look frozen.
|
||||
|
||||
---
|
||||
|
||||
## Where Every Prop Wires
|
||||
|
||||
```
|
||||
frontend/src/pages/LTTPage.tsx
|
||||
line 73: handleHalfQuestion → queryStream.decomposeOnly({ question })
|
||||
line 81: isLoading = phase !== 'idle' && !== 'completed' && !== 'error'
|
||||
line 166: <QueryInput isLoading={isLoading} ... /> ← shared isLoading
|
||||
|
||||
frontend/src/components/QueryInput.tsx
|
||||
line 52: isDisabled = isLoading || displayValue.trim() === ''
|
||||
line 66: <textarea disabled={isLoading} ... /> ← THIS locks the textarea
|
||||
line 81: <button "Half Question" disabled={isDisabled} />
|
||||
line 89: <button "Final Submit" disabled={isDisabled} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Summary
|
||||
|
||||
**Line 66 of QueryInput.tsx**: `disabled={isLoading}`
|
||||
|
||||
The textarea shares `isLoading` with the buttons. During the API call, `isLoading=true`, which:
|
||||
- `disabled` attribute on textarea → gray background, no cursor, no user input
|
||||
- Buttons become disabled via `isDisabled` (line 52)
|
||||
|
||||
The textarea **does not need to be disabled** during an API call — the buttons are sufficient to prevent duplicate submissions. The textarea should remain interactive so users can continue seeing and editing the accumulating ASR text.
|
||||
|
||||
---
|
||||
|
||||
## Fix Plan
|
||||
|
||||
### Step 1: Remove `disabled={isLoading}` from textarea (line 66)
|
||||
|
||||
Change:
|
||||
```tsx
|
||||
disabled={isLoading}
|
||||
```
|
||||
To:
|
||||
```tsx
|
||||
disabled={false} // or remove the disabled prop entirely
|
||||
```
|
||||
|
||||
The textarea should never be disabled — `isDisabled` on the buttons already prevents submissions when text is empty.
|
||||
|
||||
### Step 2: Keep buttons on `isDisabled` (lines 81, 89)
|
||||
|
||||
Buttons continue to use `isDisabled = isLoading || displayValue.trim() === ''` — they remain disabled during API calls AND when text is empty.
|
||||
|
||||
### Step 3: (Optional) Remove `disabled:bg-gray-100 disabled:cursor-not-allowed` from textarea CSS
|
||||
|
||||
Line 55-57 of the `textareaClassName` includes `disabled:bg-gray-100 disabled:cursor-not-allowed` — if the textarea is no longer disabled, these are inert. Can be removed for cleanliness but not required.
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|------|----------|------------|
|
||||
| User types while LLM uses old text | Low | Half Question already sent with the text at click time. ASR accumulation doesn't invalidate previous half-question results. |
|
||||
| Double-click Half Question | Low | `isDisabled` on the button prevents this (isLoading=true during call). |
|
||||
| User edits text then clicks Final Submit | None | This is desired behavior. |
|
||||
|
||||
### Test Update
|
||||
|
||||
File: `frontend/src/test/test_phase2_QueryInput_integration.test.tsx`
|
||||
|
||||
No test currently asserts that the textarea is disabled during `isLoading`. Only tests check button disabled state. If any test relies on textarea being disabled, update it to expect `toBeEnabled()`.
|
||||
|
||||
---
|
||||
|
||||
## Phase Sequence Timeline (decomposeOnly)
|
||||
|
||||
```
|
||||
t=0ms setState({ phase: 'decomposing' }) → isLoading=true (textarea locked, buttons locked)
|
||||
t=2s SSE event 'decomposed' → phase='retrieving' (still locked)
|
||||
t=3s SSE event 'completed' → phase='completed' → isLoading=false (UNLOCKED)
|
||||
```
|
||||
|
||||
The textarea is locked for ~3 seconds (LLM network round trip + decomposition time). This is the window where users see the grayed-out textarea and think text has stopped accumulating.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] Textarea never gets `disabled` attribute, regardless of `isLoading`
|
||||
- [ ] Textarea remains editable/selectable during API calls
|
||||
- [ ] ASR partial text continues to display in textarea during API calls
|
||||
- [ ] Half Question button disabled during API call (prevents double-click)
|
||||
- [ ] Final Submit button disabled during API call
|
||||
- [ ] Both buttons re-enable after API call completes
|
||||
- [ ] Both buttons disabled when textarea is empty
|
||||
- [ ] `pnpm test` passes
|
||||
- [ ] Playwright confirms: textarea interactive during ASR streaming + half-question processing
|
||||
|
|
@ -52,7 +52,7 @@ export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, onHalfQuestion
|
|||
const isDisabled = isLoading || displayValue.trim() === ''
|
||||
|
||||
const textareaClassName = [
|
||||
'w-full rounded border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed',
|
||||
'w-full rounded border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
|
||||
showPartialStyle ? 'text-gray-400 italic' : '',
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
|
|
@ -63,7 +63,6 @@ export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, onHalfQuestion
|
|||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask a question about your documents..."
|
||||
disabled={isLoading}
|
||||
rows={3}
|
||||
className={textareaClassName}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ describe('QueryInput', () => {
|
|||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('textarea is disabled when isLoading is true', () => {
|
||||
it('textarea is NOT disabled when isLoading is true (remains editable)', () => {
|
||||
render(<QueryInput onSubmit={mockOnSubmit} isLoading={true} />)
|
||||
const textarea = screen.getByPlaceholderText('Ask a question about your documents...')
|
||||
expect(textarea).toBeDisabled()
|
||||
expect(textarea).toBeEnabled()
|
||||
})
|
||||
|
||||
it('button is disabled when isLoading is true even with text', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue