Web embedding için kapsamlı kod örnekleri ve kullanım senaryoları
En basit haliyle bir ses asistanı bağlantısı kurmak için:
import { Room, RoomEvent } from 'livekit-client';
class VoiceAssistant {
private room: Room | null = null;
private apiKey: string;
private assistantId: string;
constructor(apiKey: string, assistantId: string) {
this.apiKey = apiKey;
this.assistantId = assistantId;
}
async connect(userId?: string): Promise<void> {
try {
// Get token
const response = await fetch('https://api.wespoke.ai/api/v1/embed/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
assistantId: this.assistantId,
metadata: { userId }
})
});
const { data } = await response.json();
// Create audio session
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// When connected
this.room.on(RoomEvent.Connected, () => {
console.log('Connected to assistant');
});
// When assistant audio arrives
this.room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'audio') {
const audioElement = track.attach();
document.body.appendChild(audioElement);
}
});
// Connect and enable microphone
await this.room.connect(data.url, data.token);
await this.room.localParticipant.setMicrophoneEnabled(true);
} catch (error) {
console.error('Connection error:', error);
throw error;
}
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Usage
const assistant = new VoiceAssistant(
'pk_live_abc123...',
'asst_xyz789'
);
await assistant.connect('user-123');
// await assistant.disconnect();Mikrofon açma/kapama, ses seviyesi ve bağlantı durumu gösterimi:
import { Room, RoomEvent, ConnectionState } from 'livekit-client';
class VoiceAssistantWithUI {
private room: Room | null = null;
private apiKey: string;
private assistantId: string;
private onStateChange?: (state: ConnectionState) => void;
private onVolumeChange?: (volume: number) => void;
constructor(
apiKey: string,
assistantId: string,
callbacks?: {
onStateChange?: (state: ConnectionState) => void;
onVolumeChange?: (volume: number) => void;
}
) {
this.apiKey = apiKey;
this.assistantId = assistantId;
this.onStateChange = callbacks?.onStateChange;
this.onVolumeChange = callbacks?.onVolumeChange;
}
async connect(metadata?: Record<string, any>): Promise<void> {
// Get token
const response = await fetch('https://api.wespoke.ai/api/v1/embed/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
assistantId: this.assistantId,
metadata
})
});
const { data } = await response.json();
// Audio session
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Listen for connection state changes
this.room.on(RoomEvent.ConnectionStateChanged, (state) => {
this.onStateChange?.(state);
});
// Assistant audio
this.room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'audio') {
const audioElement = track.attach();
document.body.appendChild(audioElement);
// Monitor volume level
this.monitorVolume(track);
}
});
// Connect
await this.room.connect(data.url, data.token);
await this.room.localParticipant.setMicrophoneEnabled(true);
}
async toggleMicrophone(): Promise<boolean> {
if (!this.room) return false;
const currentlyEnabled = this.room.localParticipant.isMicrophoneEnabled;
const newState = !currentlyEnabled;
try {
await this.room.localParticipant.setMicrophoneEnabled(newState);
return newState;
} catch (error) {
console.error('Microphone toggle error:', error);
throw error;
}
}
isMicrophoneEnabled(): boolean {
return this.room?.localParticipant.isMicrophoneEnabled ?? false;
}
getConnectionState(): ConnectionState | null {
return this.room?.state ?? null;
}
private monitorVolume(track: any): void {
// Periodically check volume level
const interval = setInterval(() => {
if (!this.room || this.room.state !== 'connected') {
clearInterval(interval);
return;
}
// Volume measurement (0-100 range)
// Note: Use Web Audio API for real implementation
const volume = Math.random() * 100; // Placeholder
this.onVolumeChange?.(volume);
}, 100);
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Usage
const assistant = new VoiceAssistantWithUI(
'pk_live_abc123...',
'asst_xyz789',
{
onStateChange: (state) => {
console.log('State changed:', state);
// Update UI
},
onVolumeChange: (volume) => {
// Update volume indicator
updateVolumeIndicator(volume);
}
}
);
// Connect
await assistant.connect({
userId: 'user-123',
source: 'website'
});
// Toggle microphone
document.getElementById('mic-toggle')?.addEventListener('click', async () => {
const enabled = await assistant.toggleMicrophone();
updateMicIcon(enabled);
});
// Disconnect
document.getElementById('hang-up')?.addEventListener('click', async () => {
await assistant.disconnect();
});Kapsamlı hata yönetimi ve kullanıcı bildirimleri:
import { Room, RoomEvent } from 'livekit-client';
interface CallError {
code: string;
message: string;
userMessage: string;
}
class RobustVoiceAssistant {
private room: Room | null = null;
private apiKey: string;
private assistantId: string;
private onError?: (error: CallError) => void;
constructor(
apiKey: string,
assistantId: string,
onError?: (error: CallError) => void
) {
this.apiKey = apiKey;
this.assistantId = assistantId;
this.onError = onError;
}
async connect(metadata?: Record<string, any>): Promise<void> {
try {
// Get token
const response = await fetch('https://api.wespoke.ai/api/v1/embed/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
assistantId: this.assistantId,
metadata
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
this.handleApiError(response.status, errorData);
return;
}
const { data } = await response.json();
// Audio session
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Connection errors
this.room.on(RoomEvent.Disconnected, (reason) => {
if (reason) {
this.handleDisconnection(reason);
}
});
// Connect
await this.room.connect(data.url, data.token);
// Check microphone permission
try {
await this.room.localParticipant.setMicrophoneEnabled(true);
} catch (error) {
this.handleMicrophoneError(error);
}
} catch (error: any) {
this.handleConnectionError(error);
}
}
private handleApiError(status: number, errorData: any): void {
const errorMap: Record<number, CallError> = {
400: {
code: 'INVALID_REQUEST',
message: errorData?.error?.message || 'Invalid request',
userMessage: 'Request parameters are invalid. Please contact support.'
},
401: {
code: 'UNAUTHORIZED',
message: 'Invalid API key',
userMessage: 'Authentication error. Please refresh the page.'
},
403: {
code: 'DOMAIN_NOT_ALLOWED',
message: 'Domain not in allowed list',
userMessage: 'This site is not authorized to use the voice assistant.'
},
404: {
code: 'ASSISTANT_NOT_FOUND',
message: 'Assistant not found',
userMessage: 'Assistant not found. Please contact support.'
},
500: {
code: 'SERVER_ERROR',
message: 'Internal server error',
userMessage: 'Server error. Please try again later.'
}
};
const error = errorMap[status] || {
code: 'UNKNOWN_ERROR',
message: `HTTP ${status}`,
userMessage: 'An unexpected error occurred. Please try again later.'
};
this.onError?.(error);
}
private handleConnectionError(error: any): void {
const callError: CallError = {
code: 'CONNECTION_ERROR',
message: error.message || 'Connection failed',
userMessage: 'Could not connect. Please check your internet connection.'
};
this.onError?.(callError);
}
private handleMicrophoneError(error: any): void {
const callError: CallError = {
code: 'MICROPHONE_ERROR',
message: error.message || 'Microphone access denied',
userMessage: 'Microphone access denied. Please check microphone permissions in your browser settings.'
};
this.onError?.(callError);
}
private handleDisconnection(reason?: string): void {
const callError: CallError = {
code: 'DISCONNECTED',
message: reason || 'Disconnected',
userMessage: 'Disconnected.'
};
this.onError?.(callError);
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Usage
const assistant = new RobustVoiceAssistant(
'pk_live_abc123...',
'asst_xyz789',
(error) => {
// Show error message to user
showErrorNotification(error.userMessage);
// Error logging
console.error(`[${error.code}] ${error.message}`);
// Error tracking (e.g., Sentry)
trackError({
code: error.code,
message: error.message
});
}
);
await assistant.connect({ userId: 'user-123' });React uygulamalarında kullanım için özel hook:
import { useState, useEffect, useCallback, useRef } from 'react';
import { Room, RoomEvent, ConnectionState } from 'livekit-client';
import { getTranslations } from 'next-intl/server';
interface UseVoiceAssistantOptions {
apiKey: string;
assistantId: string;
autoConnect?: boolean;
metadata?: Record<string, any>;
}
export function useVoiceAssistant({
apiKey,
assistantId,
autoConnect = false,
metadata
}: UseVoiceAssistantOptions) {
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [isMicEnabled, setIsMicEnabled] = useState(false);
const [error, setError] = useState<string | null>(null);
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
const roomRef = useRef<Room | null>(null);
const connect = useCallback(async () => {
if (roomRef.current) return;
setIsConnecting(true);
setError(null);
try {
// Get token
const response = await fetch('https://api.wespoke.ai/api/v1/embed/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
assistantId,
metadata
})
});
if (!response.ok) {
throw new Error('Failed to get token');
}
const { data } = await response.json();
// Audio session
const room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Event listeners
room.on(RoomEvent.ConnectionStateChanged, (state) => {
setConnectionState(state);
setIsConnected(state === 'connected');
});
room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'audio') {
const audioElement = track.attach();
document.body.appendChild(audioElement);
}
});
room.on(RoomEvent.Disconnected, () => {
setIsConnected(false);
setIsMicEnabled(false);
});
// Connect
await room.connect(data.url, data.token);
await room.localParticipant.setMicrophoneEnabled(true);
setIsMicEnabled(true);
roomRef.current = room;
} catch (err: any) {
setError(err.message);
} finally {
setIsConnecting(false);
}
}, [apiKey, assistantId, metadata]);
const disconnect = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.disconnect();
roomRef.current = null;
setIsConnected(false);
setIsMicEnabled(false);
}
}, []);
const toggleMicrophone = useCallback(async () => {
if (!roomRef.current) return;
const enabled = !isMicEnabled;
await roomRef.current.localParticipant.setMicrophoneEnabled(enabled);
setIsMicEnabled(enabled);
}, [isMicEnabled]);
// Auto-connect
useEffect(() => {
if (autoConnect) {
connect();
}
return () => {
disconnect();
};
}, [autoConnect, connect, disconnect]);
return {
isConnected,
isConnecting,
isMicEnabled,
error,
connectionState,
connect,
disconnect,
toggleMicrophone
};
}Hook kullanımı:
import { useVoiceAssistant } from './use-voice-assistant';
function VoiceAssistantButton() {
const {
isConnected,
isConnecting,
isMicEnabled,
error,
connect,
disconnect,
toggleMicrophone
} = useVoiceAssistant({
apiKey: process.env.NEXT_PUBLIC_API_KEY!,
assistantId: 'asst_xyz789',
metadata: {
userId: 'user-123',
source: 'homepage'
}
});
if (error) {
return <div className="text-red-500">Hata: {error}</div>;
}
return (
<div className="flex gap-2">
{!isConnected ? (
<button
onClick={connect}
disabled={isConnecting}
className="px-4 py-2 bg-primary text-white rounded"
>
{isConnecting ? 'Connecting...' : 'Call Assistant'}
</button>
) : (
<>
<button
onClick={toggleMicrophone}
className="px-4 py-2 bg-secondary rounded"
>
{isMicEnabled ? '🎤 Mic On' : '🔇 Mic Off'}
</button>
<button
onClick={disconnect}
className="px-4 py-2 bg-destructive text-white rounded"
>
{t('aramayiBitir')}
</button>
</>
)}
</div>
);
}Aramayı sonlandırmanın farklı yolları ve en iyi pratikler:
import { Room } from 'livekit-client';
// Simple disconnect - just call disconnect
async function endCall(room: Room) {
// This single line is sufficient!
await room.disconnect();
// Backend automatically:
// ✅ Sets call status to 'completed'
// ✅ Records end time and duration
// ✅ Finalizes the record
// ✅ Calculates cost and deducts credits
// ✅ Sends webhooks
}import { Room, RoomEvent } from 'livekit-client';
class CallManager {
private room: Room | null = null;
setupEventListeners() {
if (!this.room) return;
// Triggered when disconnected
this.room.on(RoomEvent.Disconnected, (reason) => {
console.log('Call ended:', reason);
// Update UI
this.updateUIAfterDisconnect();
// Cleanup
this.cleanup();
});
}
async endCall() {
if (!this.room) {
console.warn('Already disconnected');
return;
}
console.log('Ending call...');
// Disconnect - RoomEvent.Disconnected will be triggered
await this.room.disconnect();
// Clean up room reference
this.room = null;
}
private updateUIAfterDisconnect() {
// Update buttons
document.getElementById('call-btn')?.classList.remove('hidden');
document.getElementById('end-btn')?.classList.add('hidden');
// Update status indicator
document.getElementById('status')!.textContent = 'Disconnected';
}
private cleanup() {
// Clean up audio elements
document.querySelectorAll('audio[data-call-audio]').forEach(el => el.remove());
}
}import { Room } from 'livekit-client';
async function endCallWithConfirmation(room: Room) {
// Ask user for confirmation
const confirmed = confirm(
'Are you sure you want to end the call?'
);
if (!confirmed) {
return; // Cancel
}
try {
// Notify user
showNotification('Ending call...', 'info');
// End call
await room.disconnect();
// Success message
showNotification('Call ended successfully', 'success');
} catch (error) {
console.error('Error ending call:', error);
showNotification('Failed to end call', 'error');
}
}
// Modern alternatif (daha iyi UX)
async function endCallWithModal(room: Room) {
const modal = document.getElementById('end-call-modal');
modal?.classList.remove('hidden');
// Wait for user choice with Promise
const shouldEnd = await new Promise<boolean>((resolve) => {
document.getElementById('confirm-end')?.addEventListener('click', () => {
modal?.classList.add('hidden');
resolve(true);
}, { once: true });
document.getElementById('cancel-end')?.addEventListener('click', () => {
modal?.classList.add('hidden');
resolve(false);
}, { once: true });
});
if (shouldEnd) {
await room.disconnect();
}
}import { Room, RoomEvent, ConnectionState } from 'livekit-client';
class ResilientCallManager {
private room: Room | null = null;
private isIntentionalDisconnect = false;
private reconnectAttempts = 0;
private maxReconnectAttempts = 3;
setupEventListeners() {
if (!this.room) return;
// Monitor connection state changes
this.room.on(RoomEvent.ConnectionStateChanged, (state: ConnectionState) => {
console.log('Connection state:', state);
if (state === 'disconnected' && !this.isIntentionalDisconnect) {
// Unintentional disconnect - reconnect
this.attemptReconnect();
}
});
// When disconnected
this.room.on(RoomEvent.Disconnected, (reason) => {
if (this.isIntentionalDisconnect) {
console.log('Call ended by user');
this.cleanup();
} else {
console.warn('Unexpected disconnection:', reason);
}
});
// Reconnection events
this.room.on(RoomEvent.Reconnecting, () => {
showNotification('Reconnecting...', 'warning');
});
this.room.on(RoomEvent.Reconnected, () => {
this.reconnectAttempts = 0;
showNotification('Reconnected!', 'success');
});
}
async endCall() {
if (!this.room) return;
// Set intentional disconnect flag
this.isIntentionalDisconnect = true;
try {
await this.room.disconnect();
this.room = null;
console.log('Call ended successfully');
} catch (error) {
console.error('Disconnect error:', error);
} finally {
this.isIntentionalDisconnect = false;
this.reconnectAttempts = 0;
}
}
private async attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
showNotification('Could not reconnect. Please refresh the page.', 'error');
this.cleanup();
return;
}
this.reconnectAttempts++;
console.log(`Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
// Connection will be automatically re-established (LiveKit SDK)
}
private cleanup() {
this.room = null;
this.isIntentionalDisconnect = false;
this.reconnectAttempts = 0;
}
}❌ DELETE endpoint kullanmayın:
// ❌ WRONG - DELETE endpoint does not end the call, it only deletes the record
async function wrongWayToEndCall(callId: string, apiKey: string) {
await fetch(`https://api.wespoke.ai/api/v1/calls/${callId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiKey}` }
});
// This ONLY deletes the call record, it does NOT end the active call!
}
// ✅ CORRECT - First disconnect, then optionally DELETE
async function correctWayToEndCall(room: Room, callId: string, apiKey: string) {
// 1. First disconnect the WebRTC connection
await room.disconnect();
// 2. (Optional) Use DELETE if you want to remove from history
if (shouldDeleteFromHistory) {
await fetch(`https://api.wespoke.ai/api/v1/calls/${callId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiKey}` }
});
}
}💡 Önemli Notlar:
room.disconnect() çağırdığınızdabackend otomatik olarak aramayı sonlandırır, fatura keser ve kayıt yapar.DELETE /calls/:id endpointsadece arama kaydını silmek içindir, aramayı sonlandırmaz.