449 lines
17 KiB
Markdown
449 lines
17 KiB
Markdown
# Development Plan: Half Question Enhancement
|
|
|
|
**Created:** 2026-05-14
|
|
**Status:** Draft
|
|
**Source:** User request — three enhancements to the LTT (Live Transcript) page
|
|
|
|
---
|
|
|
|
## 1. Objective
|
|
|
|
Three changes on the LTT page in the RAG Video Q&A app:
|
|
|
|
1. **ASR text color**: Make all transcribed text appear black from the first word (currently grey italic until sentence completion)
|
|
2. **Rename button**: "Submit" → "Final Submit"
|
|
3. **New button**: Add "Half Question" button before "Final Submit" — sends current accumulated ASR text to decomposition only, keeps text in the querybox (does NOT clear it), ASR continues adding new text. User can click repeatedly to re-decompose with updated text.
|
|
|
|
---
|
|
|
|
## 2. User Flow (Half Question)
|
|
|
|
```
|
|
Video plays → ASR transcribes → text accumulates in querybox (black, real-time)
|
|
│
|
|
┌───────────────────┴───────────────────┐
|
|
▼ ▼
|
|
[Half Question] [Final Submit]
|
|
│ │
|
|
Send current text to Send complete text
|
|
DECOMPOSITION ONLY through FULL PIPELINE
|
|
│ (decompose→retrieve→filter→generate)
|
|
┌─────────────┴─────────────┐ │
|
|
▼ ▼ ▼
|
|
Sub-questions appear Text STAYS in Text CLEARED from
|
|
in ExtractedQuestionsDisplay querybox querybox
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
ASR continues adding User can click again Full answer appears
|
|
more text to querybox (or click Final Submit) in ResponsePanel
|
|
```
|
|
|
|
**Key difference from full submit**:
|
|
- Half Question does NOT clear the querybox text
|
|
- Half Question does NOT reset `hasUserInput` (since user didn't type)
|
|
- Half Question only runs decomposition (Stage 1 of the 3-step pipeline)
|
|
- Half Question result is displayed in `ExtractedQuestionsDisplay` (already on page)
|
|
|
|
---
|
|
|
|
## 3. Changes Overview
|
|
|
|
| # | Change | Files Affected | Complexity |
|
|
|---|--------|---------------|------------|
|
|
| 1 | ASR grey → black | 1 file (+ tests) | Trivial |
|
|
| 2 | Rename "Submit" → "Final Submit" | 1 file (+ tests) | Trivial |
|
|
| 3 | Half Question button + backend | 5 files (+ tests) | Medium |
|
|
|
|
---
|
|
|
|
## 4. Change 1: ASR Text Color (Grey → Black)
|
|
|
|
### Current Behavior
|
|
- `QueryInput.tsx` line 23: `showPartialStyle = !hasUserInput && !!partialText`
|
|
- Line 55: `showPartialStyle ? 'text-gray-400 italic' : ''`
|
|
- When ASR sends interim (delta) text via `partialTranscript`, text appears grey italic
|
|
- When ASR sends final (is_final) sentence, `partialTranscript` is cleared → text turns black
|
|
|
|
### Change
|
|
**File**: `frontend/src/components/QueryInput.tsx`, **line 55**
|
|
|
|
```diff
|
|
- showPartialStyle ? 'text-gray-400 italic' : '',
|
|
+ '',
|
|
```
|
|
|
|
This is a one-character-effective change (`''` replaces the ternary). The `showPartialStyle` variable and `partialText` prop remain (they may be useful for other purposes), but the visual distinction is removed.
|
|
|
|
### Tests to Update
|
|
**File**: `frontend/src/test/test_phase2_QueryInput_integration.test.tsx`
|
|
- ~9 tests assert `text-gray-400` and `italic` classes are present/absent
|
|
- Update to assert that these classes are NEVER applied
|
|
|
|
---
|
|
|
|
## 5. Change 2: Rename "Submit" → "Final Submit"
|
|
|
|
### Change
|
|
**File**: `frontend/src/components/QueryInput.tsx`, **line 75**
|
|
|
|
```diff
|
|
- {isLoading ? 'Processing...' : 'Submit'}
|
|
+ {isLoading ? 'Processing...' : 'Final Submit'}
|
|
```
|
|
|
|
### Tests to Update
|
|
- `frontend/src/test/components/QueryInput.test.tsx` — tests referencing "Submit" text
|
|
- `frontend/src/test/test_phase2_LTTPage_integration.test.tsx` — line ~298: `screen.getByRole('button', { name: /submit/i })` → `/final submit/i`
|
|
|
|
---
|
|
|
|
## 6. Change 3: Half Question Button + Backend
|
|
|
|
### 6.1 Backend Changes
|
|
|
|
#### 6.1.1 Add `stop_after_decompose` to QueryRequest
|
|
**File**: `backend/app/models/query.py`, **line 8-9**
|
|
|
|
```diff
|
|
class QueryRequest(BaseModel):
|
|
question: str
|
|
+ stop_after_decompose: bool = False
|
|
```
|
|
|
|
#### 6.1.2 Modify `_query_stream` to stop after decompose
|
|
**File**: `backend/app/routers/query.py`, **lines 206-235**
|
|
|
|
After yielding the `decomposed` event (line 206), check the flag:
|
|
|
|
```python
|
|
yield _format_sse({
|
|
"phase": "decomposed",
|
|
"extracted_questions": extracted_questions,
|
|
})
|
|
|
|
# NEW: Stop after decompose for half-question requests
|
|
if request.stop_after_decompose:
|
|
_schedule_history(history_service, request, extracted_questions,
|
|
decompose_prompt, decomposer_time_ms, 0, 0, "", "",
|
|
0, 0, "", "", 0, active_profile, "",
|
|
"[]", int((time.perf_counter() - overall_start) * 1000))
|
|
yield _format_sse({
|
|
"phase": "completed",
|
|
"answer": "",
|
|
"half_question": True,
|
|
"extracted_questions": extracted_questions,
|
|
"sources": [],
|
|
})
|
|
return
|
|
|
|
# Existing code continues (Stage 2: Retrieve)
|
|
stage_start = time.perf_counter()
|
|
...
|
|
```
|
|
|
|
The `half_question: True` flag in the completed event lets the frontend distinguish this from a full completion.
|
|
|
|
#### 6.1.3 Update SSE event types (backend)
|
|
**File**: `backend/app/models/query.py`
|
|
|
|
Add `half_question` and `extracted_questions` fields to `CompletedEvent`:
|
|
|
|
```diff
|
|
class CompletedEvent(BaseModel):
|
|
phase: Literal["completed"]
|
|
answer: str
|
|
+ half_question: bool = False
|
|
+ extracted_questions: List[str] = []
|
|
sub_question_sources: List[SubQuestionSources] = []
|
|
sources: List[SourceMetadata] = []
|
|
```
|
|
|
|
### 6.2 Frontend Type Changes
|
|
|
|
**File**: `frontend/src/types/index.ts`, **line 18-20**
|
|
|
|
```diff
|
|
export interface QueryRequest {
|
|
question: string
|
|
+ stop_after_decompose?: boolean
|
|
}
|
|
```
|
|
|
|
**File**: `frontend/src/types/index.ts`, **line 35** (the `completed` event in `QueryStreamEvent`)
|
|
|
|
```diff
|
|
- | { phase: 'completed'; answer: string; sub_question_sources?: SubQuestionSources[]; sources?: SourceMetadata[] }
|
|
+ | { phase: 'completed'; answer: string; half_question?: boolean; extracted_questions?: string[]; sub_question_sources?: SubQuestionSources[]; sources?: SourceMetadata[] }
|
|
```
|
|
|
|
### 6.3 Frontend Hook Changes
|
|
|
|
**File**: `frontend/src/lib/queries.tsx`
|
|
|
|
Two approaches:
|
|
|
|
**Option A (Recommended): Add `decomposeOnly` method to existing hook**
|
|
|
|
Add a `decomposeOnly` method to `useQueryDocumentStream`:
|
|
|
|
```tsx
|
|
const decomposeOnly = useCallback(async (request: QueryRequest) => {
|
|
setState({
|
|
extractedQuestions: null,
|
|
answer: null,
|
|
sources: null,
|
|
subQuestionSources: null,
|
|
phase: 'decomposing',
|
|
historyId: null,
|
|
error: null,
|
|
})
|
|
|
|
abortRef.current = new AbortController()
|
|
|
|
try {
|
|
await queryDocumentStream(
|
|
{ question: request.question, stop_after_decompose: true },
|
|
(event: QueryStreamEvent) => {
|
|
switch (event.phase) {
|
|
case 'decomposed':
|
|
setState(prev => ({
|
|
...prev,
|
|
extractedQuestions: event.extracted_questions ?? null,
|
|
phase: 'retrieving',
|
|
}))
|
|
break
|
|
case 'completed':
|
|
// For half-question: keep extractedQuestions, clear answer
|
|
setState(prev => ({
|
|
...prev,
|
|
answer: event.half_question ? null : (event.answer ?? null),
|
|
sources: event.sources ?? null,
|
|
subQuestionSources: event.sub_question_sources ?? null,
|
|
phase: 'completed',
|
|
historyId: (event as any).history_id ?? null,
|
|
}))
|
|
break
|
|
// ... other cases unchanged
|
|
}
|
|
},
|
|
abortRef.current.signal
|
|
)
|
|
} catch (err) {
|
|
// ... same error handling
|
|
}
|
|
}, [])
|
|
|
|
return { ...state, mutate, decomposeOnly, reset }
|
|
```
|
|
|
|
**Option B: Separate `useDecomposeOnly` hook** — cleaner separation but duplicates SSE handling. Only use if Option A gets too messy.
|
|
|
|
### 6.4 QueryInput Component Changes
|
|
|
|
**File**: `frontend/src/components/QueryInput.tsx`
|
|
|
|
Changes to make:
|
|
1. Remove `text-gray-400 italic` (Change 1, line 55)
|
|
2. Rename "Submit" → "Final Submit" (Change 2, line 75)
|
|
3. Add `onHalfQuestion` prop and "Half Question" button (Change 3)
|
|
|
|
#### Updated interface (lines 3-8):
|
|
|
|
```diff
|
|
export interface QueryInputProps {
|
|
onSubmit: (question: string) => void
|
|
isLoading: boolean
|
|
partialText?: string
|
|
value?: string
|
|
+ onHalfQuestion?: (question: string) => void
|
|
+ isHalfQuestionLoading?: boolean
|
|
}
|
|
```
|
|
|
|
#### New "Half Question" button (insert between lines 69 and 70):
|
|
|
|
Add a gray secondary button BEFORE the Final Submit button in the `<div className="flex items-center gap-3">`:
|
|
|
|
```tsx
|
|
<div className="flex items-center gap-3">
|
|
{onHalfQuestion && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const trimmed = question.trim()
|
|
if (trimmed && !isLoading) {
|
|
onHalfQuestion(trimmed)
|
|
// NOTE: do NOT clear question text, do NOT reset hasUserInput
|
|
setSubmittedQuestion(trimmed)
|
|
}
|
|
}}
|
|
disabled={isDisabled}
|
|
className="shrink-0 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Half Question
|
|
</button>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={isDisabled}
|
|
className="shrink-0 px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 transition-all duration-200"
|
|
>
|
|
{isLoading ? 'Processing...' : 'Final Submit'}
|
|
</button>
|
|
...
|
|
</div>
|
|
```
|
|
|
|
**Key design decisions for Half Question button**:
|
|
- `type="button"` — NOT a form submit (form submit = Final Submit)
|
|
- `onClick` handler calls `onHalfQuestion` with the current text
|
|
- Does NOT clear `question` state (keeps text in querybox for ASR to continue adding)
|
|
- Does NOT clear `hasUserInput` (user hasn't typed — ASR is filling)
|
|
- Sets `submittedQuestion` so the "Your question:" echo appears
|
|
- Disabled when empty or loading (same as Final Submit)
|
|
- Gray secondary style (`bg-gray-100`) to visually distinguish from blue "Final Submit"
|
|
- Conditionally rendered only when `onHalfQuestion` prop is provided (backward compatible)
|
|
|
|
### 6.5 LTTPage Changes
|
|
|
|
**File**: `frontend/src/pages/LTTPage.tsx`
|
|
|
|
#### Add `handleHalfQuestion` handler (after line 45):
|
|
|
|
```tsx
|
|
const handleHalfQuestion = (question: string): void => {
|
|
queryStream.decomposeOnly({ question })
|
|
// NOTE: do NOT call setQueryText('') — keep text in querybox
|
|
}
|
|
```
|
|
|
|
#### Update `<QueryInput>` rendering (lines 114-119):
|
|
|
|
```diff
|
|
<QueryInput
|
|
onSubmit={handleQuerySubmit}
|
|
isLoading={isLoading}
|
|
partialText={asr.partialTranscript}
|
|
value={queryText}
|
|
+ onHalfQuestion={handleHalfQuestion}
|
|
/>
|
|
```
|
|
|
|
#### Update `isLoading` to account for half question:
|
|
|
|
The current `isLoading` disables the QueryInput whenever the full pipeline is running. For half questions, we need a separate loading state:
|
|
|
|
```tsx
|
|
const isFullLoading = queryStream.phase !== 'idle' && queryStream.phase !== 'completed' && queryStream.phase !== 'error'
|
|
```
|
|
|
|
Actually, since `decomposeOnly` uses the same state machine, `isLoading` will already be `true` during half-question decomposition and `false` after completion. This works as-is. No change needed.
|
|
|
|
---
|
|
|
|
## 7. Test Files
|
|
|
|
### 7.1 Backend Tests
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `backend/app/test/test_phase3_half_question.py` | **NEW** — Test decompose-only endpoint: SSE yields `decomposed` then `completed`, answer is empty, `extracted_questions` populated, `half_question: true` |
|
|
| `backend/app/test/acceptance/test_acceptance_phase3_half_question.py` | **NEW** — Acceptance test: real LLM decomposes a question, stops before retrieval |
|
|
|
|
### 7.2 Frontend Tests
|
|
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `frontend/src/test/components/QueryInput.test.tsx` | Update: "Final Submit" text, new "Half Question" button, no grey class |
|
|
| `frontend/src/test/test_phase2_QueryInput_integration.test.tsx` | Update: remove assertions for `text-gray-400`/`italic` classes |
|
|
| `frontend/src/test/test_phase2_LTTPage_integration.test.tsx` | Update: rename `{ name: /submit/i }` → `{ name: /final submit/i }` |
|
|
| `frontend/src/test/test_phase3_half_question.test.tsx` | **NEW** — Integration test: Half Question button click → SSE with decompose-only, text stays in querybox |
|
|
|
|
---
|
|
|
|
## 8. Acceptance Criteria
|
|
|
|
### Change 1 — ASR Text Color
|
|
- [ ] ASR transcribed text appears in black from the first word (no grey italic phase)
|
|
- [ ] User-typed text still appears correctly
|
|
- [ ] All existing QueryInput tests pass after updates
|
|
|
|
### Change 2 — Rename
|
|
- [ ] Button displays "Final Submit" (not "Submit")
|
|
- [ ] Loading state shows "Processing..."
|
|
- [ ] All existing tests pass after updates
|
|
|
|
### Change 3 — Half Question
|
|
- [ ] "Half Question" button visible when querybox has text
|
|
- [ ] Clicking "Half Question" sends text to backend decomposition only
|
|
- [ ] Decomposed sub-questions appear in `ExtractedQuestionsDisplay`
|
|
- [ ] Querybox text remains after clicking "Half Question" (NOT cleared)
|
|
- [ ] ASR continues adding text to querybox after half question
|
|
- [ ] Clicking "Half Question" multiple times re-decomposes with updated text
|
|
- [ ] Clicking "Final Submit" sends text through full pipeline and clears querybox
|
|
- [ ] "Half Question" button is gray (secondary), "Final Submit" is blue (primary)
|
|
- [ ] "Half Question" is disabled when querybox is empty or loading
|
|
|
|
---
|
|
|
|
## 9. Implementation Tasks (Test-First Order)
|
|
|
|
### Phase 3.1: Backend — Decompose-Only Endpoint
|
|
|
|
| # | Task | Test File | Description |
|
|
|---|------|-----------|-------------|
|
|
| 1 | Write backend test | `test_phase3_half_question.py` | Test that `POST /api/v1/query` with `stop_after_decompose: true` yields `decomposed` then `completed` events, answer is empty, `half_question: true` |
|
|
| 2 | Add `stop_after_decompose` to `QueryRequest` | — | Add `bool = False` field to Pydantic model |
|
|
| 3 | Modify `_query_stream` | — | Check flag after decompose, yield completed early |
|
|
| 4 | Update `CompletedEvent` | — | Add `half_question` and `extracted_questions` fields |
|
|
| 5 | Run backend tests | — | `pytest app/test/test_phase3_half_question.py -v` |
|
|
|
|
### Phase 3.2: Frontend — Types & API
|
|
|
|
| # | Task | Test File | Description |
|
|
|---|------|-----------|-------------|
|
|
| 1 | Update `QueryRequest` type | — | Add `stop_after_decompose?: boolean` |
|
|
| 2 | Update `QueryStreamEvent` type | — | Add `half_question?` and `extracted_questions?` to completed event |
|
|
| 3 | Add `decomposeOnly` to hook | — | New method on `useQueryDocumentStream` that passes `stop_after_decompose: true` |
|
|
|
|
### Phase 3.3: Frontend — QueryInput Changes
|
|
|
|
| # | Task | Test File | Description |
|
|
|---|------|-----------|-------------|
|
|
| 1 | Write frontend test | `test_phase3_half_question.test.tsx` | Integration test: Half Question button renders, click calls handler, text not cleared |
|
|
| 2 | Remove grey text class | `QueryInput.tsx` line 55 | `showPartialStyle ? 'text-gray-400 italic' : ''` → `''` |
|
|
| 3 | Rename "Submit" → "Final Submit" | `QueryInput.tsx` line 75 | String change |
|
|
| 4 | Add `onHalfQuestion` prop | `QueryInput.tsx` lines 3-8 | Update interface |
|
|
| 5 | Add "Half Question" button | `QueryInput.tsx` line ~69 | Insert gray secondary button before Final Submit |
|
|
| 6 | Update LTTPage | `LTTPage.tsx` | Add `handleHalfQuestion`, pass `onHalfQuestion` prop |
|
|
| 7 | Update existing tests | `QueryInput.test.tsx`, `test_phase2_*.tsx` | Fix assertions for renamed button, removed grey class |
|
|
| 8 | Run frontend tests | — | `pnpm test` |
|
|
|
|
### Phase 3.4: Acceptance Testing
|
|
|
|
| # | Task | Test File | Description |
|
|
|---|------|-----------|-------------|
|
|
| 1 | Write acceptance test | `test_acceptance_phase3_half_question.py` | Real LLM decompose → stop before retrieval, verify SSE events |
|
|
| 2 | Run acceptance tests | — | `pytest app/test/acceptance/ -v -m acceptance` |
|
|
|
|
---
|
|
|
|
## 10. Risk Assessment
|
|
|
|
| Risk | Likelihood | Mitigation |
|
|
|------|-----------|------------|
|
|
| `decomposeOnly` sharing state with `mutate` causes race conditions | Low | They use same state but `decomposeOnly` completes quickly (just decompose). If user clicks "Half Question" then "Final Submit", the abort controller from `reset()` handles cleanup. |
|
|
| `ExtractedQuestionsDisplay` not updating on half question | Low | It reads `queryStream.extractedQuestions` which is set from `decomposed` event. Same mechanism as full pipeline. |
|
|
| ASR `partialTranscript` conflicts with half question text | Low | Half Question submits `question` (from `value` prop, set by `onFinalTranscript`), not `partialTranscript`. The partial flow is unaffected. |
|
|
| Backward compatibility | None | `stop_after_decompose` defaults to `false`. `onHalfQuestion` is optional. Existing behavior is unchanged. |
|
|
|
|
---
|
|
|
|
## 11. Dependencies
|
|
|
|
- No new npm packages or Python packages required
|
|
- Uses existing `queryDocumentStream` SSE infrastructure
|
|
- Uses existing `QueryDecomposer` service
|
|
- Uses existing `ExtractedQuestionsDisplay` component
|
|
- Button styling follows existing inline Tailwind conventions (no shadcn/ui)
|