Examples

A collection of building blocks for agents and audio that you can customize and extend.

Files
app/speaker/page.tsx
import { Speaker } from "@/components/speaker"

export default function Page() {
  return <Speaker />
}
EL-01 Speaker
speaker-01
Files
app/transcriber-01/page.tsx
"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
Files
app/music-player/page.tsx
"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
Files
app/music-player/page.tsx
"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
Files
app/voice-form/page.tsx
"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
Files
app/voice-nav/page.tsx
"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., &ldquo;Take me to
                    the quickstart guide&rdquo;)
                  </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