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 {
// Token al
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();
// Ses oturumu oluştur
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Bağlantı kurulduğunda
this.room.on(RoomEvent.Connected, () => {
console.log('Asistana bağlandı');
});
// Asistan sesi geldiğinde
this.room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'audio') {
const audioElement = track.attach();
document.body.appendChild(audioElement);
}
});
// Bağlan ve mikrofonu aç
await this.room.connect(data.url, data.token);
await this.room.localParticipant.setMicrophoneEnabled(true);
} catch (error) {
console.error('Bağlantı hatası:', error);
throw error;
}
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Kullanım
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> {
// Token al
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();
// Ses oturumu
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Bağlantı durumu değişikliklerini dinle
this.room.on(RoomEvent.ConnectionStateChanged, (state) => {
this.onStateChange?.(state);
});
// Asistan sesi
this.room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'audio') {
const audioElement = track.attach();
document.body.appendChild(audioElement);
// Ses seviyesini izle
this.monitorVolume(track);
}
});
// Bağlan
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('Mikrofon toggle hatası:', error);
throw error;
}
}
isMicrophoneEnabled(): boolean {
return this.room?.localParticipant.isMicrophoneEnabled ?? false;
}
getConnectionState(): ConnectionState | null {
return this.room?.state ?? null;
}
private monitorVolume(track: any): void {
// Ses seviyesini periyodik olarak kontrol et
const interval = setInterval(() => {
if (!this.room || this.room.state !== 'connected') {
clearInterval(interval);
return;
}
// Ses seviyesi ölçümü (0-100 arası)
// Not: Gerçek implementasyon için Web Audio API kullanın
const volume = Math.random() * 100; // Placeholder
this.onVolumeChange?.(volume);
}, 100);
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Kullanım
const assistant = new VoiceAssistantWithUI(
'pk_live_abc123...',
'asst_xyz789',
{
onStateChange: (state) => {
console.log('Durum değişti:', state);
// UI güncelle
},
onVolumeChange: (volume) => {
// Ses seviyesi göstergesi güncelle
updateVolumeIndicator(volume);
}
}
);
// Bağlan
await assistant.connect({
userId: 'user-123',
source: 'website'
});
// Mikrofonu aç/kapa
document.getElementById('mic-toggle')?.addEventListener('click', async () => {
const enabled = await assistant.toggleMicrophone();
updateMicIcon(enabled);
});
// Bağlantıyı kes
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 {
// Token al
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();
// Ses oturumu
this.room = new Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
}
});
// Bağlantı hataları
this.room.on(RoomEvent.Disconnected, (reason) => {
if (reason) {
this.handleDisconnection(reason);
}
});
// Bağlan
await this.room.connect(data.url, data.token);
// Mikrofon iznini kontrol et
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 || 'Geçersiz istek',
userMessage: 'İstek parametreleri hatalı. Lütfen destek ekibiyle iletişime geçin.'
},
401: {
code: 'UNAUTHORIZED',
message: 'Invalid API key',
userMessage: 'Kimlik doğrulama hatası. Lütfen sayfayı yenileyin.'
},
403: {
code: 'DOMAIN_NOT_ALLOWED',
message: 'Domain not in allowed list',
userMessage: 'Bu site ses asistanı kullanımına izinli değil.'
},
404: {
code: 'ASSISTANT_NOT_FOUND',
message: 'Assistant not found',
userMessage: 'Asistan bulunamadı. Lütfen destek ekibiyle iletişime geçin.'
},
500: {
code: 'SERVER_ERROR',
message: 'Internal server error',
userMessage: 'Sunucu hatası. Lütfen daha sonra tekrar deneyin.'
}
};
const error = errorMap[status] || {
code: 'UNKNOWN_ERROR',
message: `HTTP ${status}`,
userMessage: 'Beklenmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin.'
};
this.onError?.(error);
}
private handleConnectionError(error: any): void {
const callError: CallError = {
code: 'CONNECTION_ERROR',
message: error.message || 'Connection failed',
userMessage: 'Bağlantı kurulamadı. İnternet bağlantınızı kontrol edin.'
};
this.onError?.(callError);
}
private handleMicrophoneError(error: any): void {
const callError: CallError = {
code: 'MICROPHONE_ERROR',
message: error.message || 'Microphone access denied',
userMessage: 'Mikrofon erişimi reddedildi. Lütfen tarayıcı ayarlarından mikrofon iznini kontrol edin.'
};
this.onError?.(callError);
}
private handleDisconnection(reason?: string): void {
const callError: CallError = {
code: 'DISCONNECTED',
message: reason || 'Disconnected',
userMessage: 'Bağlantı kesildi.'
};
this.onError?.(callError);
}
async disconnect(): Promise<void> {
if (this.room) {
await this.room.disconnect();
this.room = null;
}
}
}
// Kullanım
const assistant = new RobustVoiceAssistant(
'pk_live_abc123...',
'asst_xyz789',
(error) => {
// Kullanıcıya hata mesajı göster
showErrorNotification(error.userMessage);
// Hata loglama
console.error(`[${error.code}] ${error.message}`);
// Hata tracking (örn: 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';
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 {
// Token al
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('Token alınamadı');
}
const { data } = await response.json();
// Ses oturumu
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);
});
// Bağlan
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 ? 'Bağlanıyor...' : 'Asistanı Ara'}
</button>
) : (
<>
<button
onClick={toggleMicrophone}
className="px-4 py-2 bg-secondary rounded"
>
{isMicEnabled ? '🎤 Mikrofon Açık' : '🔇 Mikrofon Kapalı'}
</button>
<button
onClick={disconnect}
className="px-4 py-2 bg-destructive text-white rounded"
>
Aramayı Bitir
</button>
</>
)}
</div>
);
}Aramayı sonlandırmanın farklı yolları ve en iyi pratikler:
import { Room } from 'livekit-client';
// Basit sonlandırma - sadece disconnect çağırın
async function endCall(room: Room) {
// Bu tek satır yeterli!
await room.disconnect();
// Backend otomatik olarak:
// ✅ Arama durumunu 'completed' yapar
// ✅ Bitiş zamanını ve süresini kaydeder
// ✅ Kaydı finalize eder
// ✅ Maliyeti hesaplar ve kredileri düşer
// ✅ Webhook'ları gönderir
}import { Room, RoomEvent } from 'livekit-client';
class CallManager {
private room: Room | null = null;
setupEventListeners() {
if (!this.room) return;
// Bağlantı kesildiğinde tetiklenir
this.room.on(RoomEvent.Disconnected, (reason) => {
console.log('Arama sonlandı:', reason);
// UI güncelle
this.updateUIAfterDisconnect();
// Temizlik
this.cleanup();
});
}
async endCall() {
if (!this.room) {
console.warn('Zaten bağlı değil');
return;
}
console.log('Arama sonlandırılıyor...');
// Disconnect - RoomEvent.Disconnected tetiklenecek
await this.room.disconnect();
// room referansını temizle
this.room = null;
}
private updateUIAfterDisconnect() {
// Butonları güncelle
document.getElementById('call-btn')?.classList.remove('hidden');
document.getElementById('end-btn')?.classList.add('hidden');
// Durum göstergesini güncelle
document.getElementById('status')!.textContent = 'Bağlantı kesildi';
}
private cleanup() {
// Audio elementlerini temizle
document.querySelectorAll('audio[data-call-audio]').forEach(el => el.remove());
}
}import { Room } from 'livekit-client';
async function endCallWithConfirmation(room: Room) {
// Kullanıcıya onay sor
const confirmed = confirm(
'Aramayı sonlandırmak istediğinizden emin misiniz?'
);
if (!confirmed) {
return; // İptal
}
try {
// Kullanıcıyı bilgilendir
showNotification('Arama sonlandırılıyor...', 'info');
// Aramayı sonlandır
await room.disconnect();
// Başarı mesajı
showNotification('Arama başarıyla sonlandırıldı', 'success');
} catch (error) {
console.error('Arama sonlandırma hatası:', error);
showNotification('Arama sonlandırılamadı', 'error');
}
}
// Modern alternatif (daha iyi UX)
async function endCallWithModal(room: Room) {
const modal = document.getElementById('end-call-modal');
modal?.classList.remove('hidden');
// Promise ile kullanıcı seçimini bekle
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;
// Bağlantı durumu değişikliklerini izle
this.room.on(RoomEvent.ConnectionStateChanged, (state: ConnectionState) => {
console.log('Bağlantı durumu:', state);
if (state === 'disconnected' && !this.isIntentionalDisconnect) {
// İstemeden bağlantı kesildi - yeniden bağlan
this.attemptReconnect();
}
});
// Bağlantı kesildiğinde
this.room.on(RoomEvent.Disconnected, (reason) => {
if (this.isIntentionalDisconnect) {
console.log('Arama kullanıcı tarafından sonlandırıldı');
this.cleanup();
} else {
console.warn('Beklenmeyen bağlantı kesintisi:', reason);
}
});
// Yeniden bağlanma olayları
this.room.on(RoomEvent.Reconnecting, () => {
showNotification('Yeniden bağlanılıyor...', 'warning');
});
this.room.on(RoomEvent.Reconnected, () => {
this.reconnectAttempts = 0;
showNotification('Yeniden bağlandı!', 'success');
});
}
async endCall() {
if (!this.room) return;
// Kasıtlı bağlantı kesme bayrağını ayarla
this.isIntentionalDisconnect = true;
try {
await this.room.disconnect();
this.room = null;
console.log('Arama başarıyla sonlandırıldı');
} catch (error) {
console.error('Sonlandırma hatası:', error);
} finally {
this.isIntentionalDisconnect = false;
this.reconnectAttempts = 0;
}
}
private async attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
showNotification('Yeniden bağlanılamadı. Lütfen sayfayı yenileyin.', 'error');
this.cleanup();
return;
}
this.reconnectAttempts++;
console.log(`Yeniden bağlanma denemesi ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
// Bağlantı otomatik olarak yeniden kurulacak (LiveKit SDK)
}
private cleanup() {
this.room = null;
this.isIntentionalDisconnect = false;
this.reconnectAttempts = 0;
}
}❌ DELETE endpoint kullanmayın:
// ❌ YANLIŞ - DELETE endpoint aramayı sonlandırmaz, sadece kaydı siler
async function wrongWayToEndCall(callId: string, apiKey: string) {
await fetch(`https://api.wespoke.ai/api/v1/calls/${callId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiKey}` }
});
// Bu SADECE call kaydını siler, aktif aramayı sonlandırmaz!
}
// ✅ DOĞRU - Önce disconnect, sonra isteğe bağlı olarak DELETE
async function correctWayToEndCall(room: Room, callId: string, apiKey: string) {
// 1. Önce WebRTC bağlantısını kes
await room.disconnect();
// 2. (Opsiyonel) Geçmişten silmek istersen DELETE kullan
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ızda backend otomatik olarak aramayı sonlandırır, fatura keser ve kayıt yapar.DELETE /calls/:id endpoint sadece arama kaydını silmek içindir, aramayı sonlandırmaz.disconnect() çağırın.disconnect() çağrısı yapın.track.attach() ile dönen audio element'i DOM'a eklemeyi unutmayın.