165

Blocks

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

Files
components/PlayButton.vue
<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
components/MusicPlayer.vue
<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
components/MusicPlayerDemo.vue
<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
components/ChatAction.vue
<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
components/VoiceChat.vue
<!-- 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
components/ChatAction.vue
<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.