evOo-local/apps/web/src/lib/voice-aurora.test.ts
cynarAI 0beed6ad98
feat(web): aurora voice stage with live word-by-word caption (#100)
Replace the full-bleed cyan/violet/emerald bloom field with one dark glass
panel: the Oo, a live caption whose new words fade in, and a compact control
row (phase label, transcript, interrupt, hang up). An iridescent aurora rises
from the panel's bottom edge, follows mic energy while listening, drifts while
thinking and blooms while speaking. Its spectrum is the shared bot palette
rotated so the active Oo's color sits in the middle, so no new hex values.

Voice entry buttons drop the hardcoded cyan/amber styling for neutral tokens
plus a thin aurora rim when voice is ready. Dead voice CSS is removed and the
voice e2e now captures the stage.


Claude-Session: https://claude.ai/code/session_012G3dUMVDCDL3PwQE4WTQxL

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 22:41:30 +02:00

64 lines
2.5 KiB
TypeScript

import { botColors } from "@rakazo/ui-tokens";
import { describe, expect, it } from "vitest";
import { auroraSpectrum, captionWords, continuesCaption, voiceAuroraBlobs } from "./voice-aurora";
describe("auroraSpectrum", () => {
it("leads with the bot's own color and keeps the whole palette", () => {
const color = botColors[3];
const spectrum = auroraSpectrum(color.toLowerCase());
expect(spectrum[0]).toBe(color);
expect([...spectrum].sort()).toEqual([...botColors].sort());
});
it("puts an unknown color first, ahead of the palette", () => {
expect(auroraSpectrum("#123456")).toEqual(["#123456", ...botColors]);
});
});
describe("voiceAuroraBlobs", () => {
it("centers the bot color and fans out symmetrically", () => {
const blobs = voiceAuroraBlobs(botColors[0], "speaking", 0);
expect(blobs.map((blob) => blob.x)).toEqual([50, 32, 68, 14, 86]);
expect(blobs[0]?.color).toBe(botColors[0]);
expect(blobs[1]?.rise).toBe(blobs[2]?.rise);
expect(blobs[0]?.rise).toBeGreaterThan(blobs[3]?.rise ?? 0);
});
it("rises with mic energy while listening", () => {
const quiet = voiceAuroraBlobs(botColors[0], "listening", 0)[0];
const loud = voiceAuroraBlobs(botColors[0], "listening", 1)[0];
expect(loud?.rise).toBeGreaterThan(quiet?.rise ?? 0);
expect(loud?.opacity).toBeGreaterThan(quiet?.opacity ?? 0);
});
it("ignores energy outside listening and clamps bad input", () => {
expect(voiceAuroraBlobs(botColors[0], "thinking", 1)).toEqual(
voiceAuroraBlobs(botColors[0], "thinking", 0),
);
expect(voiceAuroraBlobs(botColors[0], "listening", 5)).toEqual(
voiceAuroraBlobs(botColors[0], "listening", 1),
);
expect(voiceAuroraBlobs(botColors[0], "listening", Number.NaN)).toEqual(
voiceAuroraBlobs(botColors[0], "listening", 0),
);
});
});
describe("live caption", () => {
it("splits on any whitespace", () => {
expect(captionWords(" Set a\ttimer \n")).toEqual(["Set", "a", "timer"]);
expect(captionWords(" ")).toEqual([]);
});
it("treats appended words and a completing last word as a continuation", () => {
expect(continuesCaption("Set a", "Set a timer for ten")).toBe(true);
expect(continuesCaption("remind me to wat", "remind me to water the plants")).toBe(true);
});
it("restarts for a new utterance", () => {
expect(continuesCaption("", "Hello")).toBe(false);
expect(continuesCaption("Set a timer", "Set a")).toBe(false);
expect(continuesCaption("Set a timer", "Get a timer now")).toBe(false);
});
});