Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
An unhandled error has occurred.
Sedna.UI
v0.31.0 · main

Voice

tier 2 — classes

A spoken conversation with an agent. The app owns the microphone, the audio and the recogniser; the library paints what they report. One number connects them: the app writes --level, from 0 to 1, and the avatar, the orb, Mochi, the bars and the mic button all move with it.

Writing the level Read the loudness from an AnalyserNode on each animation frame, smooth it, and write it with element.style.setProperty('--level', value) on the session — once, on the element above everything that follows it. The examples here hold a fixed level so the page stays still. Under prefers-reduced-motion the bars and the button stand at the level instead of easing to it, and the orb holds a still frame.

A voice session

.voice-session centres the call: .voice-stage holds the one thing to look at — an .agent-avatar--hero, an .orb--hero or Mochi — then .voice-who, the .voice-caption, the bars and .voice-controls. The caption is aria-live="polite", so what the agent says is also read out to a reader who cannot hear it.

.voice-caption-interim is the words the recogniser may still change: muted until they settle, so a correction does not look like the agent changing its mind.

Ops agent

Done. The vacuum now starts at 03:30, after the sync window

<div class="card" style="max-width: 460px">
    <div class="voice-session" style="--level: 0.45">
        <div class="voice-stage">
            <span class="agent-avatar agent-avatar--hero" data-state="speaking" role="img" aria-label="Ops agent, speaking"></span>
        </div>
        <div class="sedna-col sedna-center sedna-gap-2">
            <span class="voice-who">Ops agent</span>
            <p class="voice-caption" aria-live="polite">Done. The vacuum now starts at 03:30, <span class="voice-caption-interim">after the sync window</span></p>
        </div>
        <span class="voice-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
        <div class="voice-controls">
            <button class="btn btn-mic" type="button" aria-label="Mute"><i class="ri-mic-line"></i></button>
            <button class="btn" type="button" aria-label="Captions" aria-pressed="true"><i class="ri-closed-captioning-line"></i></button>
            <button class="btn btn--danger" type="button" aria-label="End the call"><i class="ri-phone-line" style="rotate: 135deg"></i></button>
        </div>
    </div>
</div>

Listening

While the reader speaks, the stage shows the agent listening and the bars are .voice-bars--mine, in the brand colour, so it is clear whose voice is moving them.

Listening

Move the vacuum to half past three

<div class="card" style="max-width: 460px">
    <div class="voice-session" style="--level: 0.6">
        <div class="voice-stage">
            <span class="orb orb--hero" data-orb="listening" role="img" aria-label="Listening"></span>
        </div>
        <div class="sedna-col sedna-center sedna-gap-2">
            <span class="voice-who">Listening</span>
            <p class="voice-caption" aria-live="polite">Move the vacuum to <span class="voice-caption-interim">half past three</span></p>
        </div>
        <span class="voice-bars voice-bars--mine" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
        <div class="voice-controls">
            <button class="btn btn-mic" type="button" data-state="listening" aria-label="Stop listening"><i class="ri-mic-fill"></i></button>
            <button class="btn btn--danger" type="button" aria-label="End the call"><i class="ri-phone-line" style="rotate: 135deg"></i></button>
        </div>
    </div>
</div>

The mic button

.btn-mic on any .btn, round. data-state="listening" fills it and rings it out with --level; "muted" paints it in danger. With no microphone, disable it and say why nearby. Change the aria-label with the state — it names what pressing does next.

idle
listening
muted
no microphone
<div class="sedna-row-wrap sedna-gap-3">
    <div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" aria-label="Talk"><i class="ri-mic-line"></i></button><code>idle</code></div>
    <div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" data-state="listening" style="--level: 0.5" aria-label="Stop listening"><i class="ri-mic-fill"></i></button><code>listening</code></div>
    <div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" data-state="muted" aria-label="Unmute"><i class="ri-mic-off-line"></i></button><code>muted</code></div>
    <div class="sedna-col sedna-center sedna-gap-1"><button class="btn btn--icon btn-mic" type="button" disabled aria-label="Talk"><i class="ri-mic-line"></i></button><code>no microphone</code></div>
</div>

Live bars

.voice-bars is a live voice, as many <i> as fit; each bar has its own share of the level, so the row reads as a voice rather than a meter. It is decoration: aria-hidden. A recorded clip is .audio-wave, which draws a stored waveform rather than a live level.

.voice-bars
.voice-bars--mine
<div class="sedna-row-wrap sedna-gap-3" style="--level: 0.5">
    <div class="sedna-col sedna-center sedna-gap-1"><span class="voice-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><code>.voice-bars</code></div>
    <div class="sedna-col sedna-center sedna-gap-1"><span class="voice-bars voice-bars--mine" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><code>.voice-bars--mine</code></div>
</div>

The transcript

A .chat--flat beside the call, with the time into the call in .chat-time. The line still being recognised ends in .voice-caption-interim.

AF
Alex Fischer
Move the vacuum to half past three.
Ops agent
Done. It now starts at 03:30, after the sync window
<div class="chat chat--flat" role="log" aria-label="Call transcript" style="max-width: 460px">
    <div class="chat-message">
        <span class="avatar avatar--sm">AF</span>
        <div class="chat-body">
            <div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time">0:04</time></div>
            <div class="chat-bubble">Move the vacuum to half past three.</div>
        </div>
    </div>
    <div class="chat-message">
        <span class="agent-avatar agent-avatar--sm" role="img" aria-label="Ops agent"></span>
        <div class="chat-body">
            <div class="chat-meta"><span class="chat-author">Ops agent</span><time class="chat-time">0:07</time></div>
            <div class="chat-bubble">Done. It now starts at 03:30, <span class="voice-caption-interim">after the sync window</span></div>
        </div>
    </div>
</div>