🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.🚧 Under construction — I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.
Open menu
Switch to Darkhello@product.inc
AIUX patterns

Show the machine listening

Tags: voice, input, liveness

Typed input carries its own receipt: you press a key and the character appears. Continuous capture — voice, camera, screen, an agent watching a repository — carries none. A muted microphone, a silent room and a crashed listener all look exactly alike, and the user learns which one it was after they have finished talking.

Why this is a category problem, not a voice problem

Every discrete input confirms itself through the medium. The click depresses the button. The drag moves the thing. The keystroke lands. You never need a separate indicator that a text field is receiving text, because the text is the indicator.

Continuous capture breaks that, and it breaks it in the direction that costs most: the failure and the success look the same until the end. Nothing arrives to confirm anything, so the interface has to manufacture the confirmation, continuously, for as long as capture runs.

It reads as a voice-interface concern because that is where it was first solved. It isn't one. An agent watching a screen, a camera running pose detection, a background process watching a directory for changes — all of them are asking a person to produce input into something that gives no sign of receiving it.

Three states, and the middle one is the point

Most implementations ship two: listening and not listening. That is not enough to be useful, because it cannot distinguish the two situations a user actually needs told apart.

The middle state is the whole reason to build this. It is the one that tells someone their microphone is muted while they still have time to unmute it. Skip it, and a person can talk for a full minute into a product that is faithfully reporting that it is listening — which was true, and useless.

Derive it from the signal, not from a timer

This is the part that separates an indicator from a decoration, and it is worth being strict about.

A spinner that runs while capture is open proves that code is executing. It does not prove that anything is arriving, and it will keep spinning contentedly over a dead microphone. It is confident at precisely the moment it knows least.

A waveform drawn from the actual audio buffer proves something different: it moves when you speak and flattens when you stop. That flattening is the middle state, rendered for free. You do not have to detect the muted microphone as a special case; you have to render the signal honestly and let the user read the flat line.

The general form: bind a visual property to a live measurement of the input, not to the fact that capture is open. For audio it is amplitude. For a screen-watching agent it is the last thing it actually saw, named. For a file watcher it is the last event and when.

Which signal, and how loud

Rendering diagram…

Intensity should scale with how much the user is actively producing. A waveform is right for the moment someone is dictating into the product and watching it happen. It is wrong for an agent quietly watching a repository all afternoon, where the same treatment would be a dancing distraction — there the requirement is that the indicator never disappears, not that it moves.

For anything watching a screen, a room, or a camera, the capture indicator is not an affordance. It is the surface through which a person exercises a right to know whether they are being recorded.

Two consequences. It has to stay visible when the product is not in focus — backgrounded is exactly when it matters, and it is exactly when most implementations stop drawing it. And stopping has to be as visible as starting, because a capture you cannot obviously end is the thing that makes people quietly stop using ambient products without ever filing a complaint.

Grounded in

Poppy's dictation runs on the Web Speech API with a live waveform drawn on canvas from the audio itself, rather than an animation that starts when the microphone opens.

The reason is in the use case. The users are teachers, and the realistic moment of use is a few minutes between classes with their hands full — putting away one lesson, setting up the next, talking a parent email into existence in the gap. Fire-and-forget dictation is unusable in that window, and not because it is imprecise. It is unusable because if it was not listening, the ninety seconds they had are gone and they find out at the end, when there is no second attempt available. The waveform is what makes it worth starting.

Anti-patterns

The smallest version worth building

One canvas, one animation frame loop, and the analyser node's frequency data — bound to a visual property and drawn where the user is already looking. That is an afternoon.

Then the state everyone skips: if the level stays at zero for more than a second or so while capture is open, say so in words. "Not hearing anything — is your mic muted?" is the entire feature, and it is the difference between a person losing ten seconds and losing the only window they had.