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 {
      // 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();

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> {
    // 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();
});

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 {
      // 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' });

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';
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ı:

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 ? '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>
  );
}

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';

// 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
}

🎯 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;

    // 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());
  }
}

⚡ Kullanıcı Onayı ile Sonlandırma

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

🔄 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;

    // 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;
  }
}

🚫 YANLIŞ Yaklaşımlar

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

Dikkat Edilmesi Gerekenler

  • HTTPS Zorunluluğu: WebRTC mikrofon erişimiiçin siteniz HTTPS üzerinden sunulmalıdır. Localhost'ta geliştirme yaparken HTTP kullanabilirsiniz.
  • Tarayıcı İzinleri: Kullanıcıdan mikrofonizni istenmeden önce açıklayıcı bir mesaj gösterin.
  • Temizlik: Bileşen unmount olduğundamutlaka <code className="text-xs">disconnect()</code> çağrısı yapın.
  • Ses Çıkışı: WebRTC kütüphanesi otomatik olarakasistan sesini çalar. <code className="text-xs">track.attach()</code> ile dönen audioelement'i DOM'a eklemeyi unutmayın.
  • Mobil Destek: Mobil tarayıcılardakullanıcı etkileşimi (button click) sonrasında bağlantı kurun.