AI Chat Fancy Stable
VoiceInput
A mic button that opens into a recording panel: a live waveform painted from amplitude levels you supply, a stopwatch, the transcript as it arrives, and a cross and a check to throw it away or keep it
Preview
svelte
<script lang="ts">
import { onMount } from "svelte";
import { VoiceInput } from "fancy-ui-svelte";
/** How fast the pretend recogniser hands back another word. */
const WORD_MS = 400;
const WORDS = "show me the retry policy for failed webhook deliveries".split(" ");
let active = $state(false);
let transcript = $state("");
// Plain, not reactive: it is the rig's cursor into WORDS, not something the
// markup reads.
let spoken = 0;
function reset() {
spoken = 0;
transcript = "";
}
onMount(() => {
// A transcript that types itself is exactly the motion this setting asks us
// not to run, so it gets the finished state instead: the panel already open,
// the sentence already said, and no timer of any kind.
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
active = true;
transcript = WORDS.join(" ");
return;
}
// One interval for the life of the example rather than one per recording:
// it reads `active` each tick and does nothing until the mic is pressed.
const timer = setInterval(() => {
if (!active || spoken >= WORDS.length) return;
transcript = transcript ? `${transcript} ${WORDS[spoken]}` : WORDS[spoken];
spoken += 1;
}, WORD_MS);
return () => clearInterval(timer);
});
</script>
<div class="flex w-full max-w-md flex-col gap-3 p-6">
<!--
`demo` draws the synthetic waveform, because this page has no microphone
behind it. A real integration drops that and passes `samples` from its own
audio pipeline instead.
-->
<VoiceInput bind:active {transcript} demo onStop={reset} onCancel={reset} />
<p class="text-muted-foreground text-xs">
Press the mic and the words arrive as a recogniser would hand them over. The check keeps the
transcript and closes the panel; the cross throws it away.
</p>
</div>Installation
pnpm add fancy-ui-svelte
import { VoiceInput } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { VoiceInput } from 'fancy-ui-svelte';
</script>
<VoiceInput />Examples
Basic Usage
svelte
<script lang="ts">
import { onMount } from "svelte";
import { VoiceInput } from "$lib/fancy-ui/voice-input";
/** How fast the pretend recogniser hands back another word. */
const WORD_MS = 400;
const WORDS = "show me the retry policy for failed webhook deliveries".split(" ");
let active = $state(false);
let transcript = $state("");
// Plain, not reactive: it is the rig's cursor into WORDS, not something the
// markup reads.
let spoken = 0;
function reset() {
spoken = 0;
transcript = "";
}
onMount(() => {
// A transcript that types itself is exactly the motion this setting asks us
// not to run, so it gets the finished state instead: the panel already open,
// the sentence already said, and no timer of any kind.
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
active = true;
transcript = WORDS.join(" ");
return;
}
// One interval for the life of the example rather than one per recording:
// it reads `active` each tick and does nothing until the mic is pressed.
const timer = setInterval(() => {
if (!active || spoken >= WORDS.length) return;
transcript = transcript ? `${transcript} ${WORDS[spoken]}` : WORDS[spoken];
spoken += 1;
}, WORD_MS);
return () => clearInterval(timer);
});
</script>
<div class="flex w-full max-w-md flex-col gap-3 p-6">
<!--
`demo` draws the synthetic waveform, because this page has no microphone
behind it. A real integration drops that and passes `samples` from its own
audio pipeline instead.
-->
<VoiceInput bind:active {transcript} demo onStop={reset} onCancel={reset} />
<p class="text-muted-foreground text-xs">
Press the mic and the words arrive as a recogniser would hand them over. The check keeps the
transcript and closes the panel; the cross throws it away.
</p>
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Whether a recording is in progress (bindable). Every button writes through it; setting it from outside opens or closes the panel without firing a callback |
transcript | string | "" | Live text pushed by the consumer as their recogniser produces it, rendered muted under the waveform; a growing string grows in place |
samples | ArrayLike<number> | - | Amplitude levels in 0..1 bridged from your own audio pipeline, one entry per bar. The component never opens a microphone itself |
demo | boolean | false | Draw a deterministic synthetic wave when no samples arrive, so the panel is legible on a page with no audio behind it. Real samples always win |
onStart | () => void | - | Called when the mic button starts a recording |
onStop | () => void | - | Called when the check ends the recording and keeps the transcript |
onCancel | () => void | - | Called when the cross abandons the recording |
height | number | 48 | Waveform height in CSS pixels, clamped to 16..240 |
color | string | var(--ft-voice-color, currentColor) | Any CSS colour for the bars, including a var() or currentColor — written onto the canvas as its CSS color and read back resolved, because a 2D context understands neither |
Links
Related components
Ch
ChatError
Quiet inline failure banner for a chat turn: the error, an optional detail line, and a retry button that disables itself while the retry is in flight
AI Chat Stable
Ch
ChatMessage
One conversation turn, aligned and dressed by its role, streaming its body while the answer arrives, with an action rail that fades in on hover and a version navigator underneath
AI Chat Stable
Ch
ChatPanel
The shell a conversation lives in: a sticky header, a transcript that pins itself to the bottom while an answer arrives and offers the way back once you scroll up, and a sticky composer row
AI Chat Stable