feat(web): voice input — record, check the recognised text, send
CI / ruff · pytest (pull_request) Successful in 2m24s

Microphone button in the chat composer and in free-text steps. The browser
records 16 kHz mono WAV (no server-side ffmpeg), /me/{token}/stt transcribes,
the text lands in the field for the patient to check and edit — never sent
automatically, so safety and scrub() apply on send. Hidden when the server has
voice input off or the browser lacks a microphone API. nginx: 4 MB body limit.

Refs #117

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 11:45:36 +03:00
co-authored by Claude Opus 5.5
parent 63cc5372be
commit e20da1f21c
9 changed files with 431 additions and 1 deletions
+100
View File
@@ -1566,6 +1566,106 @@ button.row-card:hover:not(:disabled) {
color: var(--c-text-3);
}
/* ---------------------------------------------------------------- voice input (#117, slide 11) */
.voice-btn {
display: inline-flex;
align-items: center;
gap: var(--s-2);
justify-self: start;
min-height: 44px;
padding: 0 var(--s-4);
border: 1.5px solid var(--c-border-strong);
border-radius: 999px;
background: var(--c-surface);
color: var(--c-teal-strong);
font-weight: 600;
font-size: var(--fs-14);
}
.voice-btn:hover:not(:disabled) {
background: var(--c-teal-soft);
}
.mic {
flex: none;
width: 44px;
height: 44px;
display: grid;
place-items: center;
border: 1.5px solid var(--c-border);
border-radius: 50%;
background: var(--c-surface);
color: var(--c-teal-strong);
}
.mic:hover:not(:disabled) {
background: var(--c-teal-soft);
}
.voice {
display: grid;
gap: var(--s-3);
padding: var(--s-4);
border: 1px solid var(--c-border);
border-radius: var(--r-tile);
background: #fbf6ee; /* warm, as on the slide */
}
.voice--float {
position: absolute;
left: 0;
right: 0;
bottom: calc(100% + var(--s-2));
box-shadow: 0 8px 24px rgb(33 59 56 / 12%);
}
.voice__hint {
margin: 0;
font-weight: 600;
color: var(--c-text);
}
.voice__wave {
display: flex;
align-items: center;
gap: 5px;
height: 44px;
}
.voice__wave span {
flex: 1;
max-width: 6px;
border-radius: 3px;
background: #b5452f;
transition: height 90ms linear;
}
.voice__row {
display: grid;
grid-template-columns: auto 1fr 1.4fr;
align-items: center;
gap: var(--s-2);
}
.voice__time {
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--c-text-2);
}
.voice__row .btn {
min-height: 44px;
padding: 0 var(--s-3);
font-size: var(--fs-14);
}
.voice__privacy {
margin: 0;
font-size: var(--fs-12);
color: var(--c-text-3);
}
/* ---------------------------------------------------------------- emergency */
.sosblock {