# 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 `
`: ```tsx
{onHalfQuestion && ( )} ...
``` **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 `` rendering (lines 114-119): ```diff ``` #### 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)