diff --git a/VOICE.md b/VOICE.md index af5ad12..a96c536 100644 --- a/VOICE.md +++ b/VOICE.md @@ -42,11 +42,11 @@ Voice is the **immersive primary mode** for 1:1 bot conversations. Text chat use | Composer **Voice** pill | Opens Voice Stage when a provider is connected; otherwise opens Voice settings | | Transcript “Speak this reply” | Reads one message aloud (text-first affordance, unchanged) | -The composer Voice control uses `h-control-lg`, cyan bloom when ready, and amber styling when setup is needed. +The composer and stage Voice controls use `h-control-lg` in neutral tokens; when voice is ready an iridescent aurora rim (`VoiceAuroraRim`) runs along the bottom edge. ## Voice Stage -Full-bleed immersive layer (`VoiceStage` + `VoiceCanvas`) replaces the modal call dialog. +Full-screen dark layer with one glass panel (`VoiceStage`): the Oo, the live caption, and a control row. An iridescent aurora (`VoiceAurora`) rises from the panel's bottom edge. Its spectrum is the bot palette (`botColors` from `@rakazo/ui-tokens`) rotated so the active Oo's own color sits in the center. ### Persona & consciousness layer @@ -72,14 +72,16 @@ Phase labels, idle prompts, and thinking lines are **character-specific** (DE/EN | Phase | Visual | Audio | |-------|--------|-------| -| **Listening** | Cyan dual-mind bloom, mic-reactive energy bars | Dictation active; silence endpoint sends | -| **Thinking** | Violet orbital ring, calm bars | TTS idle; awaiting bot reply | -| **Speaking** | Emerald bloom, conductor mascot pose | TTS playback; caption shown live | +| **Listening** | Aurora rises and brightens with mic energy, breathing | Dictation active; silence endpoint sends | +| **Thinking** | Aurora settles low and drifts side to side | TTS idle; awaiting bot reply | +| **Speaking** | Full aurora bloom, conductor mascot pose | TTS playback; caption shown live | -Presence visuals sync to `dictation.energy` (0–1) while listening. Mascot uses `EvooMascot` poses: idle / thinking / wave. +Aurora height and opacity follow `dictation.energy` (0–1) while listening (`voiceAuroraBlobs` in `apps/web/src/lib/voice-aurora.ts`). The caption (`VoiceLiveText`) fades each new word in; words already on screen stay put while an utterance grows and the caption restarts for a new one. Mascot uses `EvooMascot` poses: idle / thinking / wave. ### Controls +The control row holds the persona phase label (live region), Transcript, Interrupt (mic), and Hang up (×). + | Input | Action | |-------|--------| | **Space** | Interrupt (stop TTS or cancel dictation, resume listening) | @@ -92,18 +94,19 @@ Collapsible panel shows the last messages from the thread plus the live utteranc ## Accessibility -- `prefers-reduced-motion: reduce` disables bloom drift, ring spin, and composer pulse; static gradients remain. +- `prefers-reduced-motion: reduce` disables aurora breathing/drift, the panel entrance, and word fade-in; the static aurora remains. - Dialog semantics: `role="dialog"`, `aria-modal`, labelled bot name. - Hang up exposes `aria-label="Hang up"`. -- Keyboard exit paths documented in-stage. +- Shortcuts are exposed through `aria-keyshortcuts` on the matching controls instead of on-screen hint text. ## Architecture ``` Shell (composer Voice) └─ CallView (orchestration: dictation + speaker + thread send) - └─ VoiceStage (layout, controls, transcript drawer) - └─ VoiceCanvas (ambient bloom field) + └─ VoiceStage (panel, controls, transcript drawer) + ├─ VoiceAurora (bottom-edge glow, phase + energy) + └─ VoiceLiveText (word-by-word caption) ``` Shared singletons: `dictation` (`apps/web/src/lib/dictation.ts`), `speaker` (`apps/web/src/lib/tts.ts`). diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index c00acec..ee2295f 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -79,7 +79,10 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa .getByRole("button", { name: "Voice", exact: true }) .click(); await expect(page.getByTestId("call-view")).toBeVisible(); + await expect(page.getByTestId("voice-panel")).toBeVisible(); + await expect(page.getByRole("button", { name: "Interrupt" })).toBeVisible(); await expect(page.getByRole("button", { name: "Hang up" })).toBeVisible(); + await captureScreenshot(page, testInfo, "voice-stage"); await page.getByRole("button", { name: "Hang up" }).click(); await expect(page.getByTestId("call-view")).toHaveCount(0); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index 7f4e0d6..1fa1f26 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -243,7 +243,11 @@ export function OoStage({ {subtitle}

{onStartVoice ? ( - + ) : null} ) : ( @@ -550,7 +554,11 @@ export function OoGroupStage({ )} {onStartVoice && !hasMessages ? ( - + ) : null} diff --git a/apps/web/src/components/stage/StageVoiceEntry.tsx b/apps/web/src/components/stage/StageVoiceEntry.tsx index c256fde..14c4088 100644 --- a/apps/web/src/components/stage/StageVoiceEntry.tsx +++ b/apps/web/src/components/stage/StageVoiceEntry.tsx @@ -1,12 +1,15 @@ import { Trans, useLingui } from "@lingui/react/macro"; import { Button } from "@rakazo/ui-web"; import { Mic } from "lucide-react"; +import { VoiceAuroraRim } from "../voice/VoiceAurora"; export function StageVoiceEntry({ voiceReady, + botColor, onStartVoice, }: { voiceReady: boolean; + botColor?: string; onStartVoice: () => void; }) { const { t } = useLingui(); @@ -22,10 +25,13 @@ export function StageVoiceEntry({ title={voiceReady ? t`Sprechen starten` : t`Sprache einrichten`} className={`tactile-press h-control-lg w-full max-w-sm gap-2.5 rounded-full px-6 text-ui-body font-semibold transition-all ${ voiceReady - ? "border-cyan-400/40 bg-gradient-to-b from-cyan-500/25 to-cyan-950/50 text-cyan-50 shadow-[0_0_32px_rgba(34,211,238,0.28),inset_0_1px_0_rgba(255,255,255,0.18)] hover:border-cyan-300/55 hover:from-cyan-400/32 hover:to-cyan-950/55 voice-primary-ready" - : "border-amber-400/30 bg-white/[0.06] text-foreground/90 hover:bg-white/[0.12]" + ? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10" + : "border-border bg-foreground/5 text-foreground/80 hover:bg-foreground/10" }`} > + {voiceReady ? ( + + ) : null}