17 KiB
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:
- ASR text color: Make all transcribed text appear black from the first word (currently grey italic until sentence completion)
- Rename button: "Submit" → "Final Submit"
- 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.tsxline 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,
partialTranscriptis cleared → text turns black
Change
File: frontend/src/components/QueryInput.tsx, line 55
- 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-400anditalicclasses 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
- {isLoading ? 'Processing...' : 'Submit'}
+ {isLoading ? 'Processing...' : 'Final Submit'}
Tests to Update
frontend/src/test/components/QueryInput.test.tsx— tests referencing "Submit" textfrontend/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
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:
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:
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
export interface QueryRequest {
question: string
+ stop_after_decompose?: boolean
}
File: frontend/src/types/index.ts, line 35 (the completed event in QueryStreamEvent)
- | { 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:
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:
- Remove
text-gray-400 italic(Change 1, line 55) - Rename "Submit" → "Final Submit" (Change 2, line 75)
- Add
onHalfQuestionprop and "Half Question" button (Change 3)
Updated interface (lines 3-8):
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">:
<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)onClickhandler callsonHalfQuestionwith the current text- Does NOT clear
questionstate (keeps text in querybox for ASR to continue adding) - Does NOT clear
hasUserInput(user hasn't typed — ASR is filling) - Sets
submittedQuestionso 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
onHalfQuestionprop is provided (backward compatible)
6.5 LTTPage Changes
File: frontend/src/pages/LTTPage.tsx
Add handleHalfQuestion handler (after line 45):
const handleHalfQuestion = (question: string): void => {
queryStream.decomposeOnly({ question })
// NOTE: do NOT call setQueryText('') — keep text in querybox
}
Update <QueryInput> rendering (lines 114-119):
<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:
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
queryDocumentStreamSSE infrastructure - Uses existing
QueryDecomposerservice - Uses existing
ExtractedQuestionsDisplaycomponent - Button styling follows existing inline Tailwind conventions (no shadcn/ui)