Build a voice assistant interface with a live transcript
Connect a voice visualizer and a live transcript to shared session state, with controls for playback and audio level.
Install the components
Add these to a React project with Tailwind CSS configured.
npx shadcn@latest add https://gradientdeck.com/ai-components/r/voice-orb-wave.json
npx shadcn@latest add https://gradientdeck.com/ai-components/r/live-transcript.jsonComponents used: Voice orb, Live transcript.
1. Use one session state
VoiceOrbWave accepts idle, listening, thinking or speaking. Map your session events to those states. The guide's sample switches between idle and speaking and advances a fixed transcript; it does not open a microphone or generate audio.
2. Supply the measured audio level
Pass a normalized level between zero and one from your audio session. The slider here lets you explore that input manually. The visualizer renders it without owning the microphone, transport or speech model.
3. Reveal transcript words from timing events
LiveTranscript receives a speaker map, turns and the active word index. In production, use your speech service's word timestamps or transcript deltas. Stop the updates when the session ends and clean up microphone tracks and audio nodes owned by your app.
Complete example
Save this as a client component after installing the components above.
"use client";
import { useEffect, useState } from "react";
import { VoiceOrbWave, type VoiceState } from "@/components/ai/voice-orb-wave";
import { LiveTranscript } from "@/components/ai/live-transcript";
const words = "Your next meeting is at two o'clock. You have time for a short break before it starts.".split(" ");
const speakers = { assistant: { name: "Assistant", color: "#c4b5fd" } };
export default function VoiceInterfaceExample() {
const [state, setState] = useState<VoiceState>("idle");
const [word, setWord] = useState(-1);
const [level, setLevel] = useState(0.4);
useEffect(() => {
if (state !== "speaking") return;
const timer = setInterval(() => setWord(current => Math.min(words.length - 1, current + 1)), 200);
return () => clearInterval(timer);
}, [state]);
useEffect(() => { if (word === words.length - 1) setState("idle"); }, [word]);
return <div className="w-full max-w-xl space-y-4">
<VoiceOrbWave state={state} level={state === "speaking" ? level : 0} width={400} height={100} />
<div className="flex items-center gap-4 text-sm">
<button type="button" onClick={() => { setWord(-1); setState("speaking"); }} disabled={state === "speaking"}>Play sample transcript</button>
<button type="button" onClick={() => setState("idle")} disabled={state !== "speaking"}>Stop</button>
<span role="status">{state === "speaking" ? "Speaking" : "Ready"}</span>
</div>
<label className="flex items-center gap-3 text-xs">Sample audio level<input aria-label="Sample audio level" type="range" min="0" max="1" step="0.05" value={level} onChange={event => setLevel(Number(event.target.value))} /></label>
<LiveTranscript speakers={speakers} turns={word < 0 ? [] : [{ speaker: "assistant", time: "0:00", words }]} activeTurn={0} activeWord={word} live={state === "speaking"} />
</div>;
}Connect your backend
Show explicit microphone permission and connection states in a real call. Keep a readable listening or speaking label alongside motion, respect reduced motion, and use actual audio timing rather than the demo timer for word highlighting.
All guides โ