feat: Phase 3 — Half Question button, Final Submit rename, ASR text always black

- Backend: add stop_after_decompose flag to QueryRequest, early-return
  after decomposition in SSE stream with half_question:true event
- Frontend: add decomposeOnly method to useQueryDocumentStream hook
- QueryInput: remove grey italic from ASR partial text, rename Submit
  to Final Submit, add gray Half Question button that decomposes
  without clearing querybox text
- LTTPage: wire handleHalfQuestion to decomposeOnly
This commit is contained in:
Woody 2026-05-14 21:27:21 +08:00
parent 64a7a8a46b
commit 17db487dbb
7 changed files with 562 additions and 6 deletions

View File

@ -0,0 +1,448 @@
# 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)

View File

@ -1,12 +1,13 @@
from typing import List, Literal, Union from typing import List, Literal, Union
from pydantic import BaseModel from pydantic import BaseModel, Field
from app.models.common import SourceMetadata from app.models.common import SourceMetadata
class QueryRequest(BaseModel): class QueryRequest(BaseModel):
question: str question: str
stop_after_decompose: bool = False
class SubQuestionSources(BaseModel): class SubQuestionSources(BaseModel):
@ -57,6 +58,8 @@ class CompletedEvent(BaseModel):
answer: str answer: str
sub_question_sources: List[SubQuestionSources] = [] sub_question_sources: List[SubQuestionSources] = []
sources: List[SourceMetadata] = [] sources: List[SourceMetadata] = []
half_question: bool = False
extracted_questions: List[str] = Field(default_factory=list)
class ErrorEvent(BaseModel): class ErrorEvent(BaseModel):

View File

@ -205,6 +205,21 @@ async def _query_stream(request: QueryRequest):
"extracted_questions": extracted_questions, "extracted_questions": extracted_questions,
}) })
# Half-question mode: stop after decomposition
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
# Stage 2: Retrieve (per sub-question) # Stage 2: Retrieve (per sub-question)
stage_start = time.perf_counter() stage_start = time.perf_counter()
retrieval_results = rag.retrieve_per_subquestion( retrieval_results = rag.retrieve_per_subquestion(

View File

@ -2,12 +2,13 @@ import React, { useState, useEffect, type FormEvent, type KeyboardEvent } from '
export interface QueryInputProps { export interface QueryInputProps {
onSubmit: (question: string) => void onSubmit: (question: string) => void
onHalfQuestion?: (question: string) => void
isLoading: boolean isLoading: boolean
partialText?: string partialText?: string
value?: string value?: string
} }
export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, isLoading, partialText, value }) => { export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, onHalfQuestion, isLoading, partialText, value }) => {
const [question, setQuestion] = useState<string>('') const [question, setQuestion] = useState<string>('')
const [submittedQuestion, setSubmittedQuestion] = useState<string | null>(null) const [submittedQuestion, setSubmittedQuestion] = useState<string | null>(null)
const [hasUserInput, setHasUserInput] = useState(false) const [hasUserInput, setHasUserInput] = useState(false)
@ -52,7 +53,7 @@ export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, isLoading, par
const textareaClassName = [ 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 disabled:bg-gray-100 disabled:cursor-not-allowed',
showPartialStyle ? 'text-gray-400 italic' : '', '',
].filter(Boolean).join(' ') ].filter(Boolean).join(' ')
return ( return (
@ -67,12 +68,28 @@ export const QueryInput: React.FC<QueryInputProps> = ({ onSubmit, isLoading, par
className={textareaClassName} className={textareaClassName}
/> />
<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)
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 <button
type="submit" type="submit"
disabled={isDisabled} 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" 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...' : 'Submit'} {isLoading ? 'Processing...' : 'Final Submit'}
</button> </button>
{submittedQuestion && ( {submittedQuestion && (
<p data-testid="submitted-question" className="text-sm text-gray-500 italic break-words"> <p data-testid="submitted-question" className="text-sm text-gray-500 italic break-words">

View File

@ -101,6 +101,73 @@ export const 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 'retrieving':
setState(prev => ({ ...prev, phase: 'retrieving' }))
break
case 'filtering':
setState(prev => ({ ...prev, phase: 'filtering' }))
break
case 'generating':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'generating_subquestion':
setState(prev => ({ ...prev, phase: 'generating' }))
break
case 'completed':
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
case 'error':
setState(prev => ({
...prev,
phase: 'error',
error: new Error(event.message ?? 'Unknown error'),
}))
break
}
}, abortRef.current.signal)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setState(prev => ({ ...prev, phase: 'idle' }))
return
}
setState(prev => ({
...prev,
phase: 'error',
error: err instanceof Error ? err : new Error(String(err)),
}))
}
}, [])
const reset = useCallback(() => { const reset = useCallback(() => {
abortRef.current?.abort() abortRef.current?.abort()
setState({ setState({
@ -114,7 +181,7 @@ export const useQueryDocumentStream = () => {
}) })
}, []) }, [])
return { ...state, mutate, reset } return { ...state, mutate, decomposeOnly, reset }
} }
export const useIngestDocument = () => { export const useIngestDocument = () => {

View File

@ -44,6 +44,10 @@ export const LTTPage: React.FC = () => {
setQueryText('') setQueryText('')
} }
const handleHalfQuestion = (question: string): void => {
queryStream.decomposeOnly({ question })
}
const handleRequestFullTranscript = useCallback(() => { const handleRequestFullTranscript = useCallback(() => {
ft.requestFullTranscript() ft.requestFullTranscript()
}, [ft]) }, [ft])
@ -113,6 +117,7 @@ export const LTTPage: React.FC = () => {
<div className="h-full p-6 flex flex-col gap-4 overflow-y-auto"> <div className="h-full p-6 flex flex-col gap-4 overflow-y-auto">
<QueryInput <QueryInput
onSubmit={handleQuerySubmit} onSubmit={handleQuerySubmit}
onHalfQuestion={handleHalfQuestion}
isLoading={isLoading} isLoading={isLoading}
partialText={asr.partialTranscript} partialText={asr.partialTranscript}
value={queryText} value={queryText}

View File

@ -17,6 +17,7 @@ export interface SubQuestionSources {
export interface QueryRequest { export interface QueryRequest {
question: string question: string
stop_after_decompose?: boolean
} }
export interface QueryResponse { export interface QueryResponse {
@ -32,7 +33,7 @@ export type QueryStreamEvent =
| { phase: 'filtering' } | { phase: 'filtering' }
| { phase: 'generating' } | { phase: 'generating' }
| { phase: 'generating_subquestion'; sub_question_index: number; sub_question_text: string } | { phase: 'generating_subquestion'; sub_question_index: number; sub_question_text: string }
| { phase: 'completed'; answer: string; sub_question_sources?: SubQuestionSources[]; sources?: SourceMetadata[] } | { phase: 'completed'; answer: string; sub_question_sources?: SubQuestionSources[]; sources?: SourceMetadata[]; half_question?: boolean; extracted_questions?: string[] }
| { phase: 'error'; message: string } | { phase: 'error'; message: string }
export interface IngestResponse { export interface IngestResponse {