import { useState, useCallback } from 'react'; import apiClient from '@/lib/api/client'; export interface StreamChunk { type: 'token' | 'metadata' | 'done' | 'error'; content?: string; metadata?: any; error?: string; } export interface ChatMessageDto { message: string; conversationId?: string; language?: string; } /** * Hook for streaming AI chat responses * Uses Server-Sent Events (SSE) for real-time token streaming */ export function useStreamingChat() { const [isStreaming, setIsStreaming] = useState(false); const [error, setError] = useState(null); const streamMessage = useCallback( async ( chatDto: ChatMessageDto, onChunk: (chunk: StreamChunk) => void, onComplete?: () => void, onError?: (error: string) => void ) => { setIsStreaming(true); setError(null); try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/ai/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', // Add auth token if available ...(typeof window !== 'undefined' && localStorage.getItem('accessToken') ? { Authorization: `Bearer ${localStorage.getItem('accessToken')}` } : {}), }, body: JSON.stringify(chatDto), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('Response body is not readable'); } const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } // Decode the chunk and add to buffer buffer += decoder.decode(value, { stream: true }); // Split by newlines to process complete SSE events const lines = buffer.split('\n'); // Keep the last incomplete line in the buffer buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines if (!trimmed) { continue; } // Parse SSE data format if (trimmed.startsWith('data: ')) { const data = trimmed.substring(6); try { const chunk: StreamChunk = JSON.parse(data); // Emit the chunk onChunk(chunk); // Check for completion if (chunk.type === 'done') { setIsStreaming(false); if (onComplete) { onComplete(); } return; } // Check for errors if (chunk.type === 'error') { setIsStreaming(false); const errorMsg = chunk.error || 'Streaming error occurred'; setError(errorMsg); if (onError) { onError(errorMsg); } return; } } catch (parseError) { console.error('Failed to parse SSE chunk:', parseError); } } } } } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Streaming failed'; setError(errorMsg); setIsStreaming(false); if (onError) { onError(errorMsg); } } }, [] ); return { streamMessage, isStreaming, error, }; }