Skip to Content
ExamplesCustom UI

Custom UI Example

Build a completely custom voice interface using the headless hooks.

Overview

Instead of using the pre-styled <VoiceAgent /> component, we’ll use:

  • VoiceSessionProvider — Provides context
  • useVoiceSession — Controls connect/disconnect
  • useConnectionState — Shows connection status
  • useTranscript — Displays conversation
  • useNetworkStatus — Shows offline indicator

Complete Example

components/VoiceChat.tsx
'use client' import { useRef, useEffect } from 'react' import { VoiceSessionProvider, useVoiceSession, useConnectionState, useTranscript, useNetworkStatus, VoiceVisualizer, TranscriptEntry } from '@vocobase/voice-client-sdk' import '@vocobase/voice-client-sdk/styles.css' export function VoiceChat({ apiKey, agentName }: { apiKey: string; agentName: string }) { return ( <VoiceSessionProvider apiKey={apiKey} agentName={agentName}> <ChatInterface /> </VoiceSessionProvider> ) } function ChatInterface() { const { connect, disconnect, error } = useVoiceSession() const { isConnected, isConnecting, isReconnecting } = useConnectionState() const { entries, isEmpty } = useTranscript() const { isOnline } = useNetworkStatus() const scrollRef = useRef<HTMLDivElement>(null) useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [entries.length]) return ( <div className="voice-chat"> {/* Offline Banner */} {!isOnline && ( <div className="offline-banner"> You're offline. Voice calls require an internet connection. </div> )} {/* Error Banner */} {error && ( <div className="error-banner">{error.message}</div> )} {/* Reconnecting Banner */} {isReconnecting && ( <div className="reconnecting-banner"> Connection lost. Reconnecting... </div> )} {/* Messages */} <div className="messages-container"> {isEmpty ? ( <div className="empty-state"> <h3>{isConnected ? 'Listening...' : 'Ready to Talk'}</h3> </div> ) : ( <> {entries.map((entry, index) => ( <div key={index} className={`message ${entry.role}`}> <div className="content">{entry.content}</div> <div className="meta"> {new Date(entry.timestamp).toLocaleTimeString()} {entry.latency_ms && <span>{entry.latency_ms}ms</span>} </div> </div> ))} <div ref={scrollRef} /> </> )} </div> {/* Visualizer */} {isConnected && ( <div className="visualizer-container"> <VoiceVisualizer options={{ barCount: 30, barColor: '#6366f1' }} /> </div> )} {/* Controls */} <div className="controls"> {isConnected ? ( <button onClick={disconnect} className="btn-disconnect"> End Call </button> ) : ( <button onClick={connect} disabled={!isOnline || isConnecting} className="btn-connect" > {isConnecting ? 'Connecting...' : 'Start Call'} </button> )} </div> </div> ) }

Styles

styles/voice-chat.css
.voice-chat { display: flex; flex-direction: column; height: 600px; max-width: 400px; border-radius: 16px; background: #ffffff; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); overflow: hidden; } .messages-container { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } .message { max-width: 80%; padding: 12px 16px; border-radius: 16px; } .message.user { align-self: flex-end; background: #6366f1; color: white; } .message.bot { align-self: flex-start; background: #f1f5f9; color: #1e293b; } .controls { padding: 16px; background: #f8fafc; } .btn-connect, .btn-disconnect { width: 100%; padding: 14px; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; } .btn-connect { background: #6366f1; color: white; } .btn-disconnect { background: #ef4444; color: white; }

Usage

app/page.tsx
import { VoiceChat } from '@/components/VoiceChat' export default function Page() { return ( <VoiceChat apiKey={process.env.NEXT_PUBLIC_VOCOBASE_API_KEY!} agentName="customer-support" /> ) }
Last updated on