Blocks
A collection of building blocks for agents and audio that you can customize and extend.
Files
<script setup lang="ts">
import type { AudioPlayerItem } from '@/components/elevenlabs-ui/audio-player'
import { AudioPlayerButton, useAudioPlayer } from '@/components/elevenlabs-ui/audio-player'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
export interface Track {
id: string
name: string
url: string
}
const props = defineProps<{
track: Track
}>()
const player = useAudioPlayer<Track>()
const item = computed<AudioPlayerItem<Track> | undefined>(() => {
if (!player.activeItem.value)
return undefined
return {
id: props.track.id,
src: props.track.url,
data: props.track,
}
})
const buttonClass = computed(() =>
cn(
'border-border h-14 w-14 rounded-full transition-all duration-300',
player.isPlaying.value
? 'bg-foreground/10 hover:bg-foreground/15 border-foreground/30 dark:bg-primary/20 dark:hover:bg-primary/30 dark:border-primary/50'
: 'bg-background hover:bg-muted',
),
)
</script>
<template>
<AudioPlayerButton
variant="outline"
size="icon"
:item="item"
:class="buttonClass"
/>
</template>
EL-01 Speaker
speaker-01
Component speaker-01 not found in examples.
Files
<script setup lang="ts">
import { Card } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
import Player from './Player.vue'
import SongListItem from './SongListItem.vue'
const exampleTracks = [
{ id: '0', name: 'II - 00', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/00.mp3' },
{ id: '1', name: 'II - 01', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/01.mp3' },
{ id: '2', name: 'II - 02', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/02.mp3' },
{ id: '3', name: 'II - 03', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/03.mp3' },
{ id: '4', name: 'II - 04', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/04.mp3' },
{ id: '5', name: 'II - 05', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/05.mp3' },
{ id: '6', name: 'II - 06', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/06.mp3' },
{ id: '7', name: 'II - 07', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/07.mp3' },
{ id: '8', name: 'II - 08', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/08.mp3' },
{ id: '9', name: 'II - 09', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/09.mp3' },
]
</script>
<template>
<Card class="mx-auto w-full overflow-hidden p-0">
<div class="flex flex-col lg:h-[180px] lg:flex-row">
<div class="bg-muted/50 flex flex-col overflow-hidden lg:h-full lg:w-64">
<ScrollArea class="h-48 w-full lg:h-full">
<div class="space-y-1 p-3">
<SongListItem
v-for="(song, index) in exampleTracks"
:key="song.id"
:song="song"
:track-number="index + 1"
/>
</div>
</ScrollArea>
</div>
<Player />
</div>
</Card>
</template>
Music player with playlist
music-player-01
Component music-player-01 not found in examples.
Files
<script setup lang="ts">
import {
AudioPlayerButton,
AudioPlayerDuration,
AudioPlayerProgress,
AudioPlayerTime,
useAudioPlayer,
} from '@/components/elevenlabs-ui/audio-player'
import { Card } from '@/components/ui/card'
import { onMounted } from 'vue'
const exampleTracks = [
{ id: '0', name: 'II - 00', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/00.mp3' },
{ id: '1', name: 'II - 01', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/01.mp3' },
{ id: '2', name: 'II - 02', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/02.mp3' },
{ id: '3', name: 'II - 03', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/03.mp3' },
{ id: '4', name: 'II - 04', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/04.mp3' },
{ id: '5', name: 'II - 05', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/05.mp3' },
{ id: '6', name: 'II - 06', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/06.mp3' },
{ id: '7', name: 'II - 07', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/07.mp3' },
{ id: '8', name: 'II - 08', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/08.mp3' },
{ id: '9', name: 'II - 09', url: 'https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/09.mp3' },
]
const player = useAudioPlayer<{ name: string }>()
const track = exampleTracks[9]
const trackItem = {
id: track.id,
src: track.url,
data: track,
}
onMounted(() => {
player.setActiveItem(trackItem)
})
</script>
<template>
<Card class="w-full overflow-hidden p-4">
<div class="space-y-4">
<div>
<h3 class="text-base font-semibold">
{{ player.activeItem.value?.data?.name || track.name }}
</h3>
</div>
<div class="flex items-center gap-3">
<AudioPlayerButton
variant="outline"
size="default"
class="h-10 w-10 shrink-0"
:item="trackItem"
/>
<div class="flex flex-1 items-center gap-2">
<AudioPlayerTime class="text-xs tabular-nums" />
<AudioPlayerProgress class="flex-1" />
<AudioPlayerDuration class="text-xs tabular-nums" />
</div>
</div>
</div>
</Card>
</template>
Simple music player
music-player-02
Component music-player-02 not found in examples.
Files
<script setup lang="ts">
import type { ButtonVariants } from '@/components/ui/button'
import type { HTMLAttributes } from 'vue'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
const props = withDefaults(defineProps<Props>(), {
variant: 'ghost',
size: 'sm',
})
interface Props {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
tooltip?: string
label?: string
class?: HTMLAttributes['class']
}
const delegatedProps = reactiveOmit(props, 'tooltip', 'label', 'class')
</script>
<template>
<TooltipProvider v-if="props.tooltip">
<Tooltip>
<TooltipTrigger as-child>
<Button
v-bind="delegatedProps"
:class="cn('text-muted-foreground hover:text-foreground relative size-9 p-1.5', props.class)"
>
<slot />
<span class="sr-only">{{ props.label || props.tooltip }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ props.tooltip }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
v-else
v-bind="delegatedProps"
:class="cn('text-muted-foreground hover:text-foreground relative size-9 p-1.5', props.class)"
>
<slot />
<span class="sr-only">{{ props.label }}</span>
</Button>
</template>
Voice chat 1
voice-chat-01
Component voice-chat-01 not found in examples.
Files
<!-- eslint-disable no-console -->
<script setup lang="ts">
import type { Status } from '@elevenlabs/client'
import type { HTMLAttributes } from 'vue'
import { useConversation } from '@/components/elevenlabs-ui/conversation-bar'
import { Orb } from '@/components/elevenlabs-ui/orb'
import { ShimmeringText } from '@/components/elevenlabs-ui/shimmering-text'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { Loader2Icon, PhoneIcon, PhoneOffIcon } from 'lucide-vue-next'
import { AnimatePresence, Motion } from 'motion-v'
import { computed, onUnmounted, ref } from 'vue'
type AgentState
= | 'disconnected'
| 'connecting'
| 'connected'
| 'disconnecting'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const DEFAULT_AGENT = {
agentId: import.meta.env.VITE_ELEVENLABS_AGENT_ID ?? '',
name: 'Customer Support',
description: 'Tap to start voice chat',
}
const agentState = ref<AgentState | null>('disconnected')
const errorMessage = ref<string | null>(null)
const mediaStreamRef = ref<MediaStream | null>(null)
const {
startSession,
endSession,
getInputVolume: getConversationInputVolume,
getOutputVolume: getConversationOutputVolume,
} = useConversation({
onConnect: () => console.log('Connected'),
onDisconnect: () => console.log('Disconnected'),
onMessage: message => console.log('Message:', message),
onError: (error: unknown) => {
console.error('Error:', error)
agentState.value = 'disconnected'
},
})
async function getMicStream() {
if (mediaStreamRef.value)
return mediaStreamRef.value
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
mediaStreamRef.value = stream
errorMessage.value = null
return stream
}
catch (error) {
if (error instanceof DOMException && error.name === 'NotAllowedError') {
errorMessage.value = 'Please enable microphone permissions in your browser.'
}
throw error
}
}
async function startConversation() {
try {
errorMessage.value = null
await getMicStream()
await startSession({
agentId: DEFAULT_AGENT.agentId,
connectionType: 'webrtc',
onStatusChange: ({ status }: { status: Status }) => {
agentState.value = status as AgentState
},
})
}
catch (error) {
console.error('Error starting conversation:', error)
agentState.value = 'disconnected'
}
}
async function handleCall() {
if (agentState.value === 'disconnected' || agentState.value === null) {
agentState.value = 'connecting'
await startConversation()
return
}
if (agentState.value === 'connected') {
await endSession()
agentState.value = 'disconnected'
if (mediaStreamRef.value) {
mediaStreamRef.value.getTracks().forEach(track => track.stop())
mediaStreamRef.value = null
}
}
}
onUnmounted(() => {
if (mediaStreamRef.value) {
mediaStreamRef.value.getTracks().forEach(track => track.stop())
}
})
const isCallActive = computed(() => agentState.value === 'connected')
const isTransitioning = computed(() => {
return agentState.value === 'connecting' || agentState.value === 'disconnecting'
})
function getInputVolume() {
const rawValue = getConversationInputVolume?.() ?? 0
return Math.min(1.0, rawValue ** 0.5 * 2.5)
}
function getOutputVolume() {
const rawValue = getConversationOutputVolume?.() ?? 0
return Math.min(1.0, rawValue ** 0.5 * 2.5)
}
</script>
<template>
<Card
:class="cn('flex h-[400px] w-full flex-col items-center justify-center overflow-hidden p-6', props.class)"
>
<div class="flex flex-col items-center gap-6">
<div
class="bg-muted relative h-32 w-32 rounded-full p-1 shadow-[inset_0_2px_8px_rgba(0,0,0,0.1)] dark:shadow-[inset_0_2px_8px_rgba(0,0,0,0.5)]"
>
<div
class="bg-background h-full w-full overflow-hidden rounded-full shadow-[inset_0_0_12px_rgba(0,0,0,0.05)] dark:shadow-[inset_0_0_12px_rgba(0,0,0,0.3)]"
>
<Orb
volume-mode="manual"
:get-input-volume="getInputVolume"
:get-output-volume="getOutputVolume"
/>
</div>
</div>
<div class="flex flex-col items-center gap-2">
<h2 class="text-xl font-semibold">
{{ DEFAULT_AGENT.name }}
</h2>
<AnimatePresence mode="wait">
<Motion
v-if="errorMessage"
key="error"
:initial="{ opacity: 0, y: -10 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: 10 }"
class="text-destructive text-center text-sm"
>
{{ errorMessage }}
</Motion>
<Motion
v-else-if="agentState === 'disconnected' || agentState === null"
key="disconnected"
:initial="{ opacity: 0, y: -10 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: 10 }"
class="text-muted-foreground text-sm"
>
{{ DEFAULT_AGENT.description }}
</Motion>
<Motion
v-else
key="status"
:initial="{ opacity: 0, y: -10 }"
:animate="{ opacity: 1, y: 0 }"
:exit="{ opacity: 0, y: 10 }"
class="flex items-center gap-2"
>
<div
:class="cn(
'h-2 w-2 rounded-full transition-all duration-300',
agentState === 'connected' && 'bg-green-500',
isTransitioning && 'bg-primary/60 animate-pulse',
)"
/>
<span class="text-sm capitalize">
<ShimmeringText
v-if="isTransitioning"
:text="agentState ?? ''"
/>
<span v-else class="text-green-600">Connected</span>
</span>
</Motion>
</AnimatePresence>
</div>
<Button
size="icon"
:disabled="isTransitioning"
:variant="isCallActive ? 'secondary' : 'default'"
class="h-12 w-12 rounded-full"
@click="handleCall"
>
<AnimatePresence mode="wait">
<Motion
v-if="isTransitioning"
key="loading"
:initial="{ opacity: 0, rotate: 0 }"
:animate="{ opacity: 1, rotate: 360 }"
:exit="{ opacity: 0 }"
:transition="{
rotate: { duration: 1, repeat: Infinity, ease: 'linear' },
}"
>
<Loader2Icon class="h-5 w-5" />
</Motion>
<Motion
v-else-if="isCallActive"
key="end"
:initial="{ opacity: 0, scale: 0.5 }"
:animate="{ opacity: 1, scale: 1 }"
:exit="{ opacity: 0, scale: 0.5 }"
>
<PhoneOffIcon class="h-5 w-5" />
</Motion>
<Motion
v-else
key="start"
:initial="{ opacity: 0, scale: 0.5 }"
:animate="{ opacity: 1, scale: 1 }"
:exit="{ opacity: 0, scale: 0.5 }"
>
<PhoneIcon class="h-5 w-5" />
</Motion>
</AnimatePresence>
</Button>
</div>
</Card>
</template>
Voice chat 2
voice-chat-02
Component voice-chat-02 not found in examples.
Files
<script setup lang="ts">
import type { ButtonVariants } from '@/components/ui/button'
import type { HTMLAttributes } from 'vue'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
const props = withDefaults(defineProps<Props>(), {
variant: 'ghost',
size: 'sm',
})
interface Props {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
tooltip?: string
label?: string
class?: HTMLAttributes['class']
}
const delegatedProps = reactiveOmit(props, 'tooltip', 'label', 'class')
</script>
<template>
<TooltipProvider v-if="props.tooltip">
<Tooltip>
<TooltipTrigger as-child>
<Button
v-bind="delegatedProps"
:class="cn('text-muted-foreground hover:text-foreground relative size-9 p-1.5', props.class)"
>
<slot />
<span class="sr-only">{{ props.label || props.tooltip }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ props.tooltip }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
v-else
v-bind="delegatedProps"
:class="cn('text-muted-foreground hover:text-foreground relative size-9 p-1.5', props.class)"
>
<slot />
<span class="sr-only">{{ props.label }}</span>
</Button>
</template>
Voice chat 3
voice-chat-03
Component voice-chat-03 not found in examples.