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 contextuseVoiceSession— Controls connect/disconnectuseConnectionState— Shows connection statususeTranscript— Displays conversationuseNetworkStatus— 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