feat: dictation round 1 — primary-input routing, no interims, optimistic meter, wide button

Owner's first live mic test (2026-09-17) drove four changes:

- ONE primary transcription input at all times: while a quiz/ask
  question waits, finals land in its field (text mode → 'Your answer',
  select modes → 'Your thinking') via a module-level claim registry
  (useDictationTarget in dictation.tsx); the composer is the fallback.
  Esc in a question field also stops dictation (threaded through
  DockOverlay → QuestionDock).
- No interim results: live words were distracting and shifted the meter.
  Params now match the pi dictate extension exactly (no interim_results,
  endpointing=300, no utterance_end_ms); the meter's width stays fixed.
- Optimistic meter: the click enters the meter view immediately (Stop
  pill + meter within one frame, verified at 150ms in the dev shell);
  token mint and mic acquisition run concurrently behind it.
- Wide idle button: the stats-bar middle is one full-width affordance —
  a quiet bordered 'Dictate' button when idle (failure text replaces the
  label), Stop pill + fixed meter while live.

Live-verified in the Electron dev shell: wide button renders on home
(167px) and chat (358px), meter + Stop appear at 150ms after click,
flat 2px bars in silence, clean return to 'Dictate' on stop.
This commit is contained in:
amosblomqvist
2026-09-17 19:59:14 +03:00
parent 573e369db5
commit 4dbbfa6fe7
4 changed files with 180 additions and 94 deletions
+9 -1
View File
@@ -57,5 +57,13 @@ against our codebase. Decisions at the bottom.
- **Activation: toggle mic** — click to start a dictation run, click again (or Esc) to end; no push-to-talk before the shortcuts milestone.
- **Wave scope: cloud first, local second** — capture component + Deepgram path ship and get live-smoked as one wave; whisper.cpp follows as a second wave.
- Renderer never holds the real key (server mints short-lived Deepgram tokens); audio never loops through our server (renderer → wss direct).
- Interim text ghosts in the composer; finals commit. Engine picker = small popover off the mic button (localStorage for now; settings milestone absorbs it).
- Engine picker = small popover off the mic button (localStorage for now; settings milestone absorbs it).
- Bands: 1) capture + mic states (no engine), 2) cloud end-to-end, 3) local end-to-end, 4) smoke + docs.
## Round-1 refinements (owner, 2026-09-17 — first live mic test)
- **No interims.** Live words beside the meter are distracting AND shift the meter around — the meter's width stays fixed while recording. Params now match the pi dictate extension exactly: `interim_results` omitted (false), `endpointing=300`, no `utterance_end_ms`.
- **One primary transcription input at all times**: while a quiz/ask question waits, finals land in ITS field (text mode → "Your answer", select modes → "Your thinking") — never the hidden composer. Implemented as a module-level claim registry (`useDictationTarget`); the composer is the fallback when nothing claims.
- **Optimistic meter**: the click enters the meter view immediately (Stop pill + meter within one frame); the token mint and mic acquisition run concurrently (Promise.all) behind it. Esc or click-again stops.
- **Wide idle button**: the stats-bar middle is one full-width affordance — a quiet bordered "Dictate" button when idle (error text replaces the label on failure), the Stop pill + fixed meter while live.
- Final-appending keeps the dictate-flush separator rule (`joinDictation`).