Kod Örnekleri

Web embedding için kapsamlı kod örnekleri ve kullanım senaryoları

1. Temel Uygulama

En basit haliyle bir ses asistanı bağlantısı kurmak için:

basic-call.ts
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();

2. UI Kontrolleri ile

Mikrofon açma/kapama, ses seviyesi ve bağlantı durumu gösterimi:

assistant-with-controls.ts
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();
});

3. Hata Yönetimi

Kapsamlı hata yönetimi ve kullanıcı bildirimleri:

error-handling.ts
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' });

4. React Hook Örneği

React uygulamalarında kullanım için özel hook:

use-voice-assistant.ts
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ı:

VoiceAssistantButton.tsx
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>
  );
}

5. Aramayı Sonlandırma

Aramayı sonlandırmanın farklı yolları ve en iyi pratikler:

✅ Basit Sonlandırma (Önerilen)

simple-disconnect.ts
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
}

🎯 Olay Dinleyicili Sonlandırma

disconnect-with-events.ts
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());
  }
}

⚡ Kullanıcı Onayı ile Sonlandırma

disconnect-with-confirmation.ts
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();
  }
}

🔄 Otomatik Yeniden Bağlanma ile Sonlandırma

disconnect-with-reconnection.ts
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;
  }
}

🚫 YANLIŞ Yaklaşımlar

❌ 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.
  • Hiçbir HTTP endpoint çağırmanıza gerek yok - sadece WebRTC bağlantısını kesin.
  • DELETE /calls/:id endpoint sadece arama kaydını silmek içindir, aramayı sonlandırmaz.
  • Bileşen unmount olduğunda her zaman disconnect() çağırın.

Dikkat Edilmesi Gerekenler

  • HTTPS Zorunluluğu: WebRTC mikrofon erişimi için siteniz HTTPS üzerinden sunulmalıdır. Localhost'ta geliştirme yaparken HTTP kullanabilirsiniz.
  • Tarayıcı İzinleri: Kullanıcıdan mikrofon izni istenmeden önce açıklayıcı bir mesaj gösterin.
  • Temizlik: Bileşen unmount olduğunda mutlaka disconnect() çağrısı yapın.
  • Ses Çıkışı: WebRTC kütüphanesi otomatik olarak asistan sesini çalar. track.attach() ile dönen audio element'i DOM'a eklemeyi unutmayın.
  • Mobil Destek: Mobil tarayıcılarda kullanıcı etkileşimi (button click) sonrasında bağlantı kurun.