legco_ai_assistant/.plans/half_question_enhancement_p...

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:

  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

- 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

- {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

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:

  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):

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)
  • 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):

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 queryDocumentStream SSE infrastructure
  • Uses existing QueryDecomposer service
  • Uses existing ExtractedQuestionsDisplay component
  • Button styling follows existing inline Tailwind conventions (no shadcn/ui)