Examples
A collection of building blocks for agents and audio that you can customize and extend.
Files
import { Speaker } from "@/components/speaker"
export default function Page() {
return <Speaker />
}
EL-01 Speaker
speaker-01
import { Speaker } from "@/components/speaker"
export default function Page() {
return <Speaker />
}
Files
"use client"
import { Fragment, useCallback, useEffect, useRef, useState } from "react"
import { Copy } from "lucide-react"
import { Streamdown } from "streamdown"
import { cn } from "@/lib/utils"
import {
transcribeAudio,
type TranscriptionResult,
} from "@/app/transcriber-01/actions/transcribe"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { LiveWaveform } from "@/components/ui/live-waveform"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
interface RecordingState {
isRecording: boolean
isProcessing: boolean
transcript: string
error: string
transcriptionTime?: number
}
export default function Transcriber01() {
const [recording, setRecording] = useState<RecordingState>({
isRecording: false,
isProcessing: false,
transcript: "",
error: "",
})
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const updateRecording = useCallback((updates: Partial<RecordingState>) => {
setRecording((prev) => ({ ...prev, ...updates }))
}, [])
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
updateRecording({ isRecording: false })
}, [cleanupStream, updateRecording])
const processAudio = useCallback(
async (audioBlob: Blob) => {
updateRecording({ isProcessing: true, error: "" })
try {
const result: TranscriptionResult = await transcribeAudio({
audio: new File([audioBlob], "recording.webm", {
type: "audio/webm",
}),
})
if (result.error) {
throw new Error(result.error)
}
updateRecording({
transcript: result.text || "",
transcriptionTime: result.transcriptionTime,
isProcessing: false,
})
} catch (err) {
console.error("Transcription error:", err)
updateRecording({
error:
err instanceof Error ? err.message : "Failed to transcribe audio",
isProcessing: false,
})
}
},
[updateRecording]
)
const startRecording = useCallback(async () => {
try {
updateRecording({
transcript: "",
error: "",
transcriptionTime: undefined,
})
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
updateRecording({ isRecording: true })
} catch (err) {
updateRecording({
error: "Microphone permission denied",
isRecording: false,
})
console.error("Microphone error:", err)
}
}, [processAudio, updateRecording])
const handleRecordToggle = useCallback(() => {
if (recording.isRecording) {
stopRecording()
} else {
startRecording()
}
}, [recording.isRecording, startRecording, stopRecording])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey && e.code === "Space") {
e.preventDefault()
handleRecordToggle()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [handleRecordToggle])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
return (
<div className="mx-auto w-full">
<Card className="border-border relative m-0 gap-0 overflow-hidden p-0 shadow-2xl">
<div className="relative py-6">
<div className="flex h-32 items-center justify-center">
{recording.isProcessing && <TranscriberProcessing />}
{(Boolean(recording.transcript) || Boolean(recording.error)) && (
<TranscriberTranscript
transcript={recording.transcript}
error={recording.error}
/>
)}
{!recording.isProcessing &&
!Boolean(recording.transcript) &&
!Boolean(recording.error) && (
<LiveWaveform
active={recording.isRecording}
barWidth={5}
barGap={2}
barRadius={8}
barColor="#71717a"
fadeEdges
fadeWidth={48}
sensitivity={0.8}
smoothingTimeConstant={0.85}
className="w-full"
/>
)}
</div>
</div>
<Separator />
<div className="bg-card px-4 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span
className={cn(
"text-muted-foreground/60 font-mono text-[10px] tracking-widest uppercase",
(recording.transcriptionTime &&
Boolean(recording.transcript)) ||
Boolean(recording.error)
? "animate-in fade-in duration-500"
: "opacity-0"
)}
>
{recording.error
? "Error"
: recording.transcriptionTime
? `${(recording.transcriptionTime / 1000).toFixed(2)}s`
: "0.00s"}
</span>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={handleRecordToggle}
disabled={recording.isProcessing}
aria-label={
recording.isRecording ? "Stop recording" : "Start recording"
}
>
{recording.isRecording || recording.isProcessing
? "Stop"
: "Record"}
<kbd className="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium select-none">
<span className="text-xs">⌥</span>Space
</kbd>
</Button>
</div>
</div>
</div>
</Card>
</div>
)
}
const TranscriberProcessing = () => {
return (
<LiveWaveform
active={false}
processing
barWidth={4}
barGap={1}
barRadius={8}
barColor="#71717a"
fadeEdges
fadeWidth={48}
className="w-full opacity-60"
/>
)
}
const TranscriberTranscript = ({
transcript,
error,
}: {
transcript: string
error: string
}) => {
const displayText = error || transcript
return (
<Fragment>
<div className="relative w-full max-w-2xl px-6">
<ScrollArea className="h-32 w-full">
<div
className={cn(
"text-foreground py-1 pr-8 text-left text-sm leading-relaxed",
error && "text-red-500"
)}
>
<Streamdown>{displayText}</Streamdown>
</div>
</ScrollArea>
{transcript && !error && (
<Button
variant="ghost"
size="icon"
className="absolute top-1 right-2 h-6 w-6 opacity-50 transition-opacity hover:opacity-100"
onClick={() => {
navigator.clipboard.writeText(transcript)
}}
aria-label="Copy transcript"
>
<Copy className="h-3.5 w-3.5" />
</Button>
)}
</div>
</Fragment>
)
}
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
Transcriber
transcriber-01
0.00s
"use client"
import { Fragment, useCallback, useEffect, useRef, useState } from "react"
import { Copy } from "lucide-react"
import { Streamdown } from "streamdown"
import { cn } from "@/lib/utils"
import {
transcribeAudio,
type TranscriptionResult,
} from "@/app/transcriber-01/actions/transcribe"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { LiveWaveform } from "@/components/ui/live-waveform"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
interface RecordingState {
isRecording: boolean
isProcessing: boolean
transcript: string
error: string
transcriptionTime?: number
}
export default function Transcriber01() {
const [recording, setRecording] = useState<RecordingState>({
isRecording: false,
isProcessing: false,
transcript: "",
error: "",
})
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const updateRecording = useCallback((updates: Partial<RecordingState>) => {
setRecording((prev) => ({ ...prev, ...updates }))
}, [])
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
updateRecording({ isRecording: false })
}, [cleanupStream, updateRecording])
const processAudio = useCallback(
async (audioBlob: Blob) => {
updateRecording({ isProcessing: true, error: "" })
try {
const result: TranscriptionResult = await transcribeAudio({
audio: new File([audioBlob], "recording.webm", {
type: "audio/webm",
}),
})
if (result.error) {
throw new Error(result.error)
}
updateRecording({
transcript: result.text || "",
transcriptionTime: result.transcriptionTime,
isProcessing: false,
})
} catch (err) {
console.error("Transcription error:", err)
updateRecording({
error:
err instanceof Error ? err.message : "Failed to transcribe audio",
isProcessing: false,
})
}
},
[updateRecording]
)
const startRecording = useCallback(async () => {
try {
updateRecording({
transcript: "",
error: "",
transcriptionTime: undefined,
})
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
updateRecording({ isRecording: true })
} catch (err) {
updateRecording({
error: "Microphone permission denied",
isRecording: false,
})
console.error("Microphone error:", err)
}
}, [processAudio, updateRecording])
const handleRecordToggle = useCallback(() => {
if (recording.isRecording) {
stopRecording()
} else {
startRecording()
}
}, [recording.isRecording, startRecording, stopRecording])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.altKey && e.code === "Space") {
e.preventDefault()
handleRecordToggle()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [handleRecordToggle])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
return (
<div className="mx-auto w-full">
<Card className="border-border relative m-0 gap-0 overflow-hidden p-0 shadow-2xl">
<div className="relative py-6">
<div className="flex h-32 items-center justify-center">
{recording.isProcessing && <TranscriberProcessing />}
{(Boolean(recording.transcript) || Boolean(recording.error)) && (
<TranscriberTranscript
transcript={recording.transcript}
error={recording.error}
/>
)}
{!recording.isProcessing &&
!Boolean(recording.transcript) &&
!Boolean(recording.error) && (
<LiveWaveform
active={recording.isRecording}
barWidth={5}
barGap={2}
barRadius={8}
barColor="#71717a"
fadeEdges
fadeWidth={48}
sensitivity={0.8}
smoothingTimeConstant={0.85}
className="w-full"
/>
)}
</div>
</div>
<Separator />
<div className="bg-card px-4 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span
className={cn(
"text-muted-foreground/60 font-mono text-[10px] tracking-widest uppercase",
(recording.transcriptionTime &&
Boolean(recording.transcript)) ||
Boolean(recording.error)
? "animate-in fade-in duration-500"
: "opacity-0"
)}
>
{recording.error
? "Error"
: recording.transcriptionTime
? `${(recording.transcriptionTime / 1000).toFixed(2)}s`
: "0.00s"}
</span>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={handleRecordToggle}
disabled={recording.isProcessing}
aria-label={
recording.isRecording ? "Stop recording" : "Start recording"
}
>
{recording.isRecording || recording.isProcessing
? "Stop"
: "Record"}
<kbd className="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium select-none">
<span className="text-xs">⌥</span>Space
</kbd>
</Button>
</div>
</div>
</div>
</Card>
</div>
)
}
const TranscriberProcessing = () => {
return (
<LiveWaveform
active={false}
processing
barWidth={4}
barGap={1}
barRadius={8}
barColor="#71717a"
fadeEdges
fadeWidth={48}
className="w-full opacity-60"
/>
)
}
const TranscriberTranscript = ({
transcript,
error,
}: {
transcript: string
error: string
}) => {
const displayText = error || transcript
return (
<Fragment>
<div className="relative w-full max-w-2xl px-6">
<ScrollArea className="h-32 w-full">
<div
className={cn(
"text-foreground py-1 pr-8 text-left text-sm leading-relaxed",
error && "text-red-500"
)}
>
<Streamdown>{displayText}</Streamdown>
</div>
</ScrollArea>
{transcript && !error && (
<Button
variant="ghost"
size="icon"
className="absolute top-1 right-2 h-6 w-6 opacity-50 transition-opacity hover:opacity-100"
onClick={() => {
navigator.clipboard.writeText(transcript)
}}
aria-label="Copy transcript"
>
<Copy className="h-3.5 w-3.5" />
</Button>
)}
</div>
</Fragment>
)
}
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
Files
"use client"
import { PauseIcon, PlayIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
AudioPlayerButton,
AudioPlayerDuration,
AudioPlayerProgress,
AudioPlayerProvider,
AudioPlayerTime,
exampleTracks,
useAudioPlayer,
} from "@/components/ui/audio-player"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
interface Track {
id: string
name: string
url: string
}
export default function Page() {
return (
<AudioPlayerProvider<Track>>
<MusicPlayer />
</AudioPlayerProvider>
)
}
const MusicPlayer = () => {
return (
<Card className="mx-auto w-full overflow-hidden p-0">
<div className="flex flex-col lg:h-[180px] lg:flex-row">
<div className="bg-muted/50 flex flex-col overflow-hidden lg:h-full lg:w-64">
<ScrollArea className="h-48 w-full lg:h-full">
<div className="space-y-1 p-3">
{exampleTracks.map((song, index) => (
<SongListItem
key={song.id}
song={song}
trackNumber={index + 1}
/>
))}
</div>
</ScrollArea>
</div>
<Player />
</div>
</Card>
)
}
const Player = () => {
const player = useAudioPlayer<Track>()
return (
<div className="flex flex-1 items-center p-4 sm:p-6">
<div className="mx-auto w-full max-w-2xl">
<div className="mb-4">
<h3 className="text-base font-semibold sm:text-lg">
{player.activeItem?.data?.name ?? "No track selected"}
</h3>
</div>
<div className="flex items-center gap-3 sm:gap-4">
<AudioPlayerButton
variant="outline"
size="default"
className="h-12 w-12 shrink-0 sm:h-10 sm:w-10"
disabled={!player.activeItem}
/>
<div className="flex flex-1 items-center gap-2 sm:gap-3">
<AudioPlayerTime className="text-xs tabular-nums" />
<AudioPlayerProgress className="flex-1" />
<AudioPlayerDuration className="text-xs tabular-nums" />
</div>
</div>
</div>
</div>
)
}
const SongListItem = ({
song,
trackNumber,
}: {
song: Track
trackNumber: number
}) => {
const player = useAudioPlayer<Track>()
const isActive = player.isItemActive(song.id)
const isCurrentlyPlaying = isActive && player.isPlaying
return (
<div className="group/song relative">
<Button
variant={isActive ? "secondary" : "ghost"}
size="sm"
className={cn(
"h-10 w-full justify-start px-3 font-normal sm:h-9 sm:px-2",
isActive && "bg-secondary"
)}
onClick={() => {
if (isCurrentlyPlaying) {
player.pause()
} else {
player.play({
id: song.id,
src: song.url,
data: song,
})
}
}}
>
<div className="flex w-full items-center gap-3">
<div className="flex w-5 shrink-0 items-center justify-center">
{isCurrentlyPlaying ? (
<PauseIcon className="h-4 w-4 sm:h-3.5 sm:w-3.5" />
) : (
<>
<span className="text-muted-foreground/60 text-sm tabular-nums group-hover/song:invisible">
{trackNumber}
</span>
<PlayIcon className="invisible absolute h-4 w-4 group-hover/song:visible sm:h-3.5 sm:w-3.5" />
</>
)}
</div>
<span className="truncate text-left text-sm">{song.name}</span>
</div>
</Button>
</div>
)
}
Music player with playlist
music-player-01
No track selected
0:00--:--
"use client"
import { PauseIcon, PlayIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
AudioPlayerButton,
AudioPlayerDuration,
AudioPlayerProgress,
AudioPlayerProvider,
AudioPlayerTime,
exampleTracks,
useAudioPlayer,
} from "@/components/ui/audio-player"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
interface Track {
id: string
name: string
url: string
}
export default function Page() {
return (
<AudioPlayerProvider<Track>>
<MusicPlayer />
</AudioPlayerProvider>
)
}
const MusicPlayer = () => {
return (
<Card className="mx-auto w-full overflow-hidden p-0">
<div className="flex flex-col lg:h-[180px] lg:flex-row">
<div className="bg-muted/50 flex flex-col overflow-hidden lg:h-full lg:w-64">
<ScrollArea className="h-48 w-full lg:h-full">
<div className="space-y-1 p-3">
{exampleTracks.map((song, index) => (
<SongListItem
key={song.id}
song={song}
trackNumber={index + 1}
/>
))}
</div>
</ScrollArea>
</div>
<Player />
</div>
</Card>
)
}
const Player = () => {
const player = useAudioPlayer<Track>()
return (
<div className="flex flex-1 items-center p-4 sm:p-6">
<div className="mx-auto w-full max-w-2xl">
<div className="mb-4">
<h3 className="text-base font-semibold sm:text-lg">
{player.activeItem?.data?.name ?? "No track selected"}
</h3>
</div>
<div className="flex items-center gap-3 sm:gap-4">
<AudioPlayerButton
variant="outline"
size="default"
className="h-12 w-12 shrink-0 sm:h-10 sm:w-10"
disabled={!player.activeItem}
/>
<div className="flex flex-1 items-center gap-2 sm:gap-3">
<AudioPlayerTime className="text-xs tabular-nums" />
<AudioPlayerProgress className="flex-1" />
<AudioPlayerDuration className="text-xs tabular-nums" />
</div>
</div>
</div>
</div>
)
}
const SongListItem = ({
song,
trackNumber,
}: {
song: Track
trackNumber: number
}) => {
const player = useAudioPlayer<Track>()
const isActive = player.isItemActive(song.id)
const isCurrentlyPlaying = isActive && player.isPlaying
return (
<div className="group/song relative">
<Button
variant={isActive ? "secondary" : "ghost"}
size="sm"
className={cn(
"h-10 w-full justify-start px-3 font-normal sm:h-9 sm:px-2",
isActive && "bg-secondary"
)}
onClick={() => {
if (isCurrentlyPlaying) {
player.pause()
} else {
player.play({
id: song.id,
src: song.url,
data: song,
})
}
}}
>
<div className="flex w-full items-center gap-3">
<div className="flex w-5 shrink-0 items-center justify-center">
{isCurrentlyPlaying ? (
<PauseIcon className="h-4 w-4 sm:h-3.5 sm:w-3.5" />
) : (
<>
<span className="text-muted-foreground/60 text-sm tabular-nums group-hover/song:invisible">
{trackNumber}
</span>
<PlayIcon className="invisible absolute h-4 w-4 group-hover/song:visible sm:h-3.5 sm:w-3.5" />
</>
)}
</div>
<span className="truncate text-left text-sm">{song.name}</span>
</div>
</Button>
</div>
)
}
Files
"use client"
import {
AudioPlayerButton,
AudioPlayerDuration,
AudioPlayerProgress,
AudioPlayerProvider,
AudioPlayerTime,
exampleTracks,
useAudioPlayer,
} from "@/components/ui/audio-player"
import { Card } from "@/components/ui/card"
export default function Page() {
return (
<AudioPlayerProvider>
<MusicPlayerDemo />
</AudioPlayerProvider>
)
}
const MusicPlayerDemo = () => {
const player = useAudioPlayer<{ name: string }>()
const track = exampleTracks[9]
return (
<Card className="w-full overflow-hidden p-4">
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold">
{player.activeItem?.data?.name || track.name}
</h3>
</div>
<div className="flex items-center gap-3">
<AudioPlayerButton
variant="outline"
size="default"
className="h-10 w-10 shrink-0"
item={{
id: track.id,
src: track.url,
data: track,
}}
/>
<div className="flex flex-1 items-center gap-2">
<AudioPlayerTime className="text-xs tabular-nums" />
<AudioPlayerProgress className="flex-1" />
<AudioPlayerDuration className="text-xs tabular-nums" />
</div>
</div>
</div>
</Card>
)
}
Simple music player
music-player-02
II - 09
0:00--:--
"use client"
import {
AudioPlayerButton,
AudioPlayerDuration,
AudioPlayerProgress,
AudioPlayerProvider,
AudioPlayerTime,
exampleTracks,
useAudioPlayer,
} from "@/components/ui/audio-player"
import { Card } from "@/components/ui/card"
export default function Page() {
return (
<AudioPlayerProvider>
<MusicPlayerDemo />
</AudioPlayerProvider>
)
}
const MusicPlayerDemo = () => {
const player = useAudioPlayer<{ name: string }>()
const track = exampleTracks[9]
return (
<Card className="w-full overflow-hidden p-4">
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold">
{player.activeItem?.data?.name || track.name}
</h3>
</div>
<div className="flex items-center gap-3">
<AudioPlayerButton
variant="outline"
size="default"
className="h-10 w-10 shrink-0"
item={{
id: track.id,
src: track.url,
data: track,
}}
/>
<div className="flex flex-1 items-center gap-2">
<AudioPlayerTime className="text-xs tabular-nums" />
<AudioPlayerProgress className="flex-1" />
<AudioPlayerDuration className="text-xs tabular-nums" />
</div>
</div>
</div>
</Card>
)
}
Files
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { cn } from "@/lib/utils"
import { voiceToFormAction } from "@/app/voice-form/actions/voice-to-form"
import {
exampleFormSchema,
ExampleFormValues,
} from "@/app/voice-form/schema"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { VoiceButton } from "@/components/ui/voice-button"
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
export default function Page() {
const [isRecording, setIsRecording] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const form = useForm<ExampleFormValues>({
resolver: zodResolver(exampleFormSchema),
defaultValues: {
firstName: "",
lastName: "",
},
mode: "onChange",
})
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const processAudio = useCallback(
async (audioBlob: Blob) => {
setIsProcessing(true)
setError("")
setSuccess(false)
try {
const audioFile = new File([audioBlob], "audio.webm", {
type: audioBlob.type,
})
const result = await voiceToFormAction(audioFile)
if (result.data && Object.keys(result.data).length > 0) {
Object.entries(result.data).forEach(([key, value]) => {
if (value) {
form.setValue(key as keyof ExampleFormValues, value as string, {
shouldValidate: true,
})
}
})
setSuccess(true)
setTimeout(() => setSuccess(false), 2000)
}
} catch (err) {
console.error("Voice input error:", err)
setError(err instanceof Error ? err.message : "Failed to process audio")
} finally {
setIsProcessing(false)
}
},
[form]
)
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
setIsRecording(false)
}, [cleanupStream])
const startRecording = useCallback(async () => {
try {
setError("")
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
setIsRecording(true)
} catch (err) {
setError("Microphone permission denied")
console.error("Microphone error:", err)
}
}, [processAudio])
const handleVoiceToggle = useCallback(() => {
if (isRecording) {
stopRecording()
} else {
startRecording()
}
}, [isRecording, startRecording, stopRecording])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
const onSubmit = (data: ExampleFormValues) => {
console.log("Form submitted:", data)
}
const voiceState = isProcessing
? "processing"
: isRecording
? "recording"
: success
? "success"
: error
? "error"
: "idle"
return (
<div className="mx-auto w-full">
<Card className="relative overflow-hidden">
<div className={cn("flex flex-col gap-2")}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle>Voice Fill</CardTitle>
<CardDescription>Powered by ElevenLabs Scribe</CardDescription>
</div>
<VoiceButton
state={voiceState}
onPress={handleVoiceToggle}
disabled={isProcessing}
trailing="Voice Fill"
/>
</div>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name *</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name *</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
)
}
Voice-fill form
voice-form-01
Voice Fill
Powered by ElevenLabs Scribe
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { cn } from "@/lib/utils"
import { voiceToFormAction } from "@/app/voice-form/actions/voice-to-form"
import {
exampleFormSchema,
ExampleFormValues,
} from "@/app/voice-form/schema"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { VoiceButton } from "@/components/ui/voice-button"
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
export default function Page() {
const [isRecording, setIsRecording] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const form = useForm<ExampleFormValues>({
resolver: zodResolver(exampleFormSchema),
defaultValues: {
firstName: "",
lastName: "",
},
mode: "onChange",
})
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const processAudio = useCallback(
async (audioBlob: Blob) => {
setIsProcessing(true)
setError("")
setSuccess(false)
try {
const audioFile = new File([audioBlob], "audio.webm", {
type: audioBlob.type,
})
const result = await voiceToFormAction(audioFile)
if (result.data && Object.keys(result.data).length > 0) {
Object.entries(result.data).forEach(([key, value]) => {
if (value) {
form.setValue(key as keyof ExampleFormValues, value as string, {
shouldValidate: true,
})
}
})
setSuccess(true)
setTimeout(() => setSuccess(false), 2000)
}
} catch (err) {
console.error("Voice input error:", err)
setError(err instanceof Error ? err.message : "Failed to process audio")
} finally {
setIsProcessing(false)
}
},
[form]
)
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
setIsRecording(false)
}, [cleanupStream])
const startRecording = useCallback(async () => {
try {
setError("")
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
setIsRecording(true)
} catch (err) {
setError("Microphone permission denied")
console.error("Microphone error:", err)
}
}, [processAudio])
const handleVoiceToggle = useCallback(() => {
if (isRecording) {
stopRecording()
} else {
startRecording()
}
}, [isRecording, startRecording, stopRecording])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
const onSubmit = (data: ExampleFormValues) => {
console.log("Form submitted:", data)
}
const voiceState = isProcessing
? "processing"
: isRecording
? "recording"
: success
? "success"
: error
? "error"
: "idle"
return (
<div className="mx-auto w-full">
<Card className="relative overflow-hidden">
<div className={cn("flex flex-col gap-2")}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle>Voice Fill</CardTitle>
<CardDescription>Powered by ElevenLabs Scribe</CardDescription>
</div>
<VoiceButton
state={voiceState}
onPress={handleVoiceToggle}
disabled={isProcessing}
trailing="Voice Fill"
/>
</div>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name *</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name *</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
)
}
Files
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
import { voiceToSiteAction } from "@/app/voice-nav/actions/voice-to-site"
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { VoiceButton } from "@/components/ui/voice-button"
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
export default function Page() {
const [url, setUrl] = useState("https://elevenlabs.io/docs")
const [isRecording, setIsRecording] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const processAudio = useCallback(async (audioBlob: Blob) => {
setIsProcessing(true)
setError("")
setSuccess(false)
try {
const audioFile = new File([audioBlob], "audio.webm", {
type: audioBlob.type,
})
const result = await voiceToSiteAction(audioFile)
if (result.data?.url) {
setUrl(result.data.url)
setSuccess(true)
}
} catch (err) {
console.error("Voice input error:", err)
setError(err instanceof Error ? err.message : "Failed to process audio")
} finally {
setIsProcessing(false)
}
}, [])
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
setIsRecording(false)
}, [cleanupStream])
const startRecording = useCallback(async () => {
try {
setError("")
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
setIsRecording(true)
} catch (err) {
setError("Microphone permission denied")
console.error("Microphone error:", err)
}
}, [processAudio])
const handleVoiceToggle = useCallback(() => {
if (isRecording) {
stopRecording()
} else {
startRecording()
}
}, [isRecording, startRecording, stopRecording])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
const voiceState = isProcessing
? "processing"
: isRecording
? "recording"
: success
? "success"
: error
? "error"
: "idle"
return (
<div className="mx-auto w-full">
<Card className="border-border relative m-0 gap-0 overflow-hidden p-0 shadow-2xl">
<div className={cn("flex flex-col gap-0")}>
<Card className="rounded-none border-x-0 border-t-0">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle>Voice Navigation</CardTitle>
<CardDescription>
Navigate websites with your voice (e.g., “Take me to
the quickstart guide”)
</CardDescription>
</div>
<VoiceButton
state={voiceState}
onPress={handleVoiceToggle}
disabled={isProcessing}
trailing="⌥Space"
label="Voice Nav"
title="Voice Navigation"
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
</CardHeader>
</Card>
<div className="h-[calc(100vh-180px)] w-full">
<iframe
key={url}
src={url}
className="h-full w-full border-0"
title="Voice Navigation Content"
/>
</div>
</div>
</Card>
</div>
)
}
Voice-nav site navigation
voice-nav-01
Voice Navigation
Navigate websites with your voice (e.g., “Take me to the quickstart guide”)
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
import { voiceToSiteAction } from "@/app/voice-nav/actions/voice-to-site"
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { VoiceButton } from "@/components/ui/voice-button"
const AUDIO_CONSTRAINTS: MediaStreamConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}
const SUPPORTED_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm"] as const
function getMimeType(): string {
for (const type of SUPPORTED_MIME_TYPES) {
if (MediaRecorder.isTypeSupported(type)) {
return type
}
}
return "audio/webm"
}
export default function Page() {
const [url, setUrl] = useState("https://elevenlabs.io/docs")
const [isRecording, setIsRecording] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const streamRef = useRef<MediaStream | null>(null)
const cleanupStream = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
streamRef.current = null
}
}, [])
const processAudio = useCallback(async (audioBlob: Blob) => {
setIsProcessing(true)
setError("")
setSuccess(false)
try {
const audioFile = new File([audioBlob], "audio.webm", {
type: audioBlob.type,
})
const result = await voiceToSiteAction(audioFile)
if (result.data?.url) {
setUrl(result.data.url)
setSuccess(true)
}
} catch (err) {
console.error("Voice input error:", err)
setError(err instanceof Error ? err.message : "Failed to process audio")
} finally {
setIsProcessing(false)
}
}, [])
const stopRecording = useCallback(() => {
if (mediaRecorderRef.current?.state !== "inactive") {
mediaRecorderRef.current?.stop()
}
cleanupStream()
setIsRecording(false)
}, [cleanupStream])
const startRecording = useCallback(async () => {
try {
setError("")
audioChunksRef.current = []
const stream =
await navigator.mediaDevices.getUserMedia(AUDIO_CONSTRAINTS)
streamRef.current = stream
const mimeType = getMimeType()
const mediaRecorder = new MediaRecorder(stream, { mimeType })
mediaRecorderRef.current = mediaRecorder
mediaRecorder.ondataavailable = (event: BlobEvent) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType })
processAudio(audioBlob)
}
mediaRecorder.start()
setIsRecording(true)
} catch (err) {
setError("Microphone permission denied")
console.error("Microphone error:", err)
}
}, [processAudio])
const handleVoiceToggle = useCallback(() => {
if (isRecording) {
stopRecording()
} else {
startRecording()
}
}, [isRecording, startRecording, stopRecording])
useEffect(() => {
return cleanupStream
}, [cleanupStream])
const voiceState = isProcessing
? "processing"
: isRecording
? "recording"
: success
? "success"
: error
? "error"
: "idle"
return (
<div className="mx-auto w-full">
<Card className="border-border relative m-0 gap-0 overflow-hidden p-0 shadow-2xl">
<div className={cn("flex flex-col gap-0")}>
<Card className="rounded-none border-x-0 border-t-0">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle>Voice Navigation</CardTitle>
<CardDescription>
Navigate websites with your voice (e.g., “Take me to
the quickstart guide”)
</CardDescription>
</div>
<VoiceButton
state={voiceState}
onPress={handleVoiceToggle}
disabled={isProcessing}
trailing="⌥Space"
label="Voice Nav"
title="Voice Navigation"
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
</CardHeader>
</Card>
<div className="h-[calc(100vh-180px)] w-full">
<iframe
key={url}
src={url}
className="h-full w-full border-0"
title="Voice Navigation Content"
/>
</div>
</div>
</Card>
</div>
)
}