feat(frontend): split Add detail into a segmented control with voice
The microphone becomes the second segment of the Add detail button, built like the detail chip's trash affordance in the same file — an overflow-hidden rounded wrapper holding two raw <button>s divided by border-s — rather than two shared Buttons, which each hardcode their own rounding and would fight a segmented control. border-s puts the mic at the logical end: visually right in en/nl, visually left in fa, on the same side as the chip's trash in both directions. The two halves share a wrapper and nothing else. Add keeps its exact behaviour. The control never changes size while recording; the timer and level meter live in a bar between the header row and the chip strip, because the header is sm:justify-between and growing the button would shove the row on every start and stop. The meter exists to prove the microphone is actually hearing something — silence and a dead mic look identical otherwise. Voice reaches the editor as one optional `voice` prop, so its absence *is* the unavailable state and the two cannot disagree. Fixes from review of this commit: - mountedRef was set false on unmount and never re-armed, so under StrictMode the hook was permanently "unmounted" in dev and recording silently never started. - onStart guarded only on `phase`, which does not change until getUserMedia resolves; a second click during the permission prompt orphaned the first MediaStream, leaving the mic indicator lit. - Week start is now per locale. "Next Thursday" is week-relative, and hardcoding Saturday put an en/nl clinician's deadline a week out. - A missing `which` on a weekday intent is read as "this" rather than failing — a bare weekday carries no qualifier, and rejecting it discarded a real deadline. - durationMs is client-reported and so is a claim, not enforcement; the cap is now also checked against the vendor's own usage.seconds. - Blob type falls back to the recorder's actual mimeType before webm, so old Safari's mp4/aac clips are not mislabelled. Two review findings were rejected as incorrect, both re-verified against live sources: google/gemini-3.7-flash does exist on OpenRouter (1M context, $0.375/$1.875 per M), and base64 JSON input_audio is the documented primary path for /audio/transcriptions, with multipart as the OpenAI-compatible alternative. The spec's stale "unverified" note is corrected, and the provider now has unit tests covering the request shape, usage parsing, and that a vendor error body never reaches the thrown message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -309,6 +309,13 @@ Both roles, one API key.
|
||||
**no transcode dependency is required**.
|
||||
- Limits: 25 MB; 60s upstream *processing* timeout. The 2-minute recording cap (§2) sits
|
||||
comfortably inside both.
|
||||
- Request shape **verified** against OpenRouter's STT docs: base64 JSON `input_audio` is
|
||||
the documented primary path (multipart `file` is the OpenAI-compatible alternative), and
|
||||
both default model slugs exist on the live models API.
|
||||
- The cap is enforced twice: the client's reported `durationMs`, and again against the
|
||||
vendor's own `usage.seconds` — the client's figure is a claim, not enforcement. The
|
||||
server allows a 2s tolerance, because the client measures length *after* the recorder
|
||||
stops and a recording that runs to the cap always reports slightly over it.
|
||||
- Response: `{ text, usage: { seconds, total_tokens, cost } }`.
|
||||
- Price: `openai/whisper-1` is **$0.006/minute, billed to the nearest second** → $0.002 for
|
||||
a typical 20s utterance, **$0.012 at the 2-minute cap**. OpenRouter forwards this model
|
||||
@@ -417,7 +424,12 @@ that justified this whole design.
|
||||
### `resolveDueDate()`
|
||||
|
||||
- Takes `clientTodayIso` + IANA `timeZone`; reuses `common/zoned-civil-time.ts`.
|
||||
- Week starts **Saturday** (Iranian week) — one place, tested.
|
||||
- Week start is **per locale**, because "next Thursday" is week-relative: `fa` starts
|
||||
Saturday, `en` and `nl` start Monday. Hardcoding Saturday put an en/nl clinician's
|
||||
deadline a week out. One place (`weekStartForLocale`), tested in both directions.
|
||||
- `'this'` is occurrence-anchored (soonest strictly-future, never resolves into the past);
|
||||
`'next'` is week-anchored. A missing qualifier is read as `'this'` — a bare weekday
|
||||
carries none, and failing would discard a real spoken deadline.
|
||||
- Jalali conversion is arithmetic, not inference: port `jalaliToGregorian` and
|
||||
`toLatinDigits` from `frontend/src/lib/i18n/persianCalendar.ts` into
|
||||
`backend/src/common/jalali.ts` with a spec. It is dependency-free integer math
|
||||
@@ -645,7 +657,7 @@ enabling this for real clinics.
|
||||
|
||||
- `cd backend && npm test` — new suites for `resolveToothIntent` (quadrant mapping in all
|
||||
four quadrants, out-of-range rejection, deciduous → unresolved), `resolveDueDate`
|
||||
(Saturday week start, "this" vs "next" weekday, Jalali leap year, month-end), the
|
||||
(per-locale week start, "this" vs "next" weekday, Jalali leap year, month-end), the
|
||||
Jalali port, prosthesis expansion + completeness, and connected-span validation.
|
||||
- `cd backend && npm run build` — cross-cutting backend gate.
|
||||
- `cd frontend && npx tsc --noEmit` — frontend gate.
|
||||
|
||||
Reference in New Issue
Block a user