import apiClient from './client' import type { AISessionCreateRequest, AISessionCreateResponse, StepResponseRequest, StepResponseResponse, ResolveSessionRequest, EscalateSessionRequest, SessionCloseResponse, SessionDocumentation, AISessionSummary, AISessionDetail, AISessionSearchResult, SimilarSession, PickupSessionRequest, StatusUpdateRequest, StatusUpdateResponse, ChatSessionCreateResponse, ChatMessageRequest, ChatMessageResponse, HandoffCreatedEvent, HandoffAssessmentReadyEvent, EscalationStreamHandlers, } from '@/types/ai-session' export const aiSessionsApi = { async createSession(data: AISessionCreateRequest): Promise { const response = await apiClient.post('/ai-sessions', data) return response.data }, async createChatSession(data: AISessionCreateRequest): Promise { const response = await apiClient.post('/ai-sessions', { ...data, session_type: 'chat', }) return response.data }, async sendChatMessage(sessionId: string, data: ChatMessageRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/chat`, data ) return response.data }, async respondToStep(sessionId: string, data: StepResponseRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/respond`, data ) return response.data }, async resolveSession(sessionId: string, data: ResolveSessionRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/resolve`, data ) return response.data }, async escalateSession(sessionId: string, data: EscalateSessionRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/escalate`, data ) return response.data }, async listSessions(params?: { status?: string session_type?: string skip?: number limit?: number problem_domain?: string matched_flow_id?: string confidence_tier?: string ticket_id?: string date_from?: string date_to?: string q?: string }): Promise { // Strip empty string values so they aren't sent as empty query params const cleanParams = params ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== '' && v !== undefined)) : undefined const response = await apiClient.get('/ai-sessions', { params: cleanParams }) return response.data }, async getSession(sessionId: string): Promise { const response = await apiClient.get(`/ai-sessions/${sessionId}`) return response.data }, async getDocumentation(sessionId: string): Promise { const response = await apiClient.get( `/ai-sessions/${sessionId}/documentation` ) return response.data }, async streamDocumentation( sessionId: string, onChunk: (text: string) => void, onDone: () => void, onError: (error: string) => void, ): Promise { const token = localStorage.getItem('access_token') const baseUrl = import.meta.env.VITE_API_URL || '' try { const response = await fetch( `${baseUrl}/api/v1/ai-sessions/${sessionId}/documentation/stream`, { headers: { Authorization: `Bearer ${token}`, }, }, ) if (!response.ok) { onError(`HTTP ${response.status}`) return } const reader = response.body?.getReader() if (!reader) { onError('No response body') return } const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6) if (data === '[DONE]') { onDone() return } if (data.startsWith('[ERROR]')) { onError(data.slice(8)) return } onChunk(data) } } } // Stream ended without [DONE] onDone() } catch (err) { onError(err instanceof Error ? err.message : 'Stream failed') } }, async rateSession(sessionId: string, data: { rating: number; feedback?: string }): Promise { await apiClient.post(`/ai-sessions/${sessionId}/rate`, data) }, async retryPsaPush(sessionId: string): Promise<{ psa_push_status: string; psa_push_error: string | null }> { const response = await apiClient.post<{ psa_push_status: string; psa_push_error: string | null }>( `/ai-sessions/${sessionId}/retry-psa-push` ) return response.data }, async saveTaskLane(sessionId: string, data: { questions: Array<{ text: string; context?: string }>; actions: Array<{ label: string; command?: string | null; description?: string }>; responses: Array>; }): Promise { await apiClient.put(`/ai-sessions/${sessionId}/task-lane`, data) }, async pauseSession(sessionId: string): Promise { await apiClient.post(`/ai-sessions/${sessionId}/pause`) }, async resumeSession(sessionId: string): Promise { await apiClient.post(`/ai-sessions/${sessionId}/resume`) }, async abandonSession(sessionId: string, reason?: string): Promise { await apiClient.post(`/ai-sessions/${sessionId}/abandon`, null, { params: reason ? { reason } : undefined, }) }, async deleteSession(sessionId: string): Promise { await apiClient.delete(`/ai-sessions/${sessionId}`) }, async pickupSession(sessionId: string, data: PickupSessionRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/pickup`, data ) return response.data }, async linkTicket(sessionId: string, data: { psa_ticket_id: string; psa_connection_id: string }): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/link-ticket`, data ) return response.data }, async getEscalationQueue(): Promise { const response = await apiClient.get('/ai-sessions/escalation-queue') return response.data }, // Native EventSource cannot send Authorization headers, so we use fetch + // ReadableStream and parse SSE frames manually (same pattern as // `streamDocumentation`). The returned promise resolves on clean stream // close (server hangs up) and rejects on network/HTTP error so the caller // can decide whether to reconnect with backoff. async streamEscalations( handlers: EscalationStreamHandlers, signal: AbortSignal, ): Promise { const token = localStorage.getItem('access_token') const baseUrl = import.meta.env.VITE_API_URL || '' const response = await fetch( `${baseUrl}/api/v1/ai-sessions/escalations/stream`, { headers: { Authorization: `Bearer ${token}` }, signal, }, ) if (!response.ok) { throw new Error(`Escalation stream failed: HTTP ${response.status}`) } const reader = response.body?.getReader() if (!reader) { throw new Error('Escalation stream: no response body') } const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) return buffer += decoder.decode(value, { stream: true }) // SSE frames are separated by blank lines. Hold the trailing partial // frame in the buffer until the next chunk completes it. const frames = buffer.split('\n\n') buffer = frames.pop() ?? '' for (const frame of frames) { if (!frame) continue let eventType = 'message' let data = '' for (const line of frame.split('\n')) { if (line.startsWith(':')) continue // comment / keepalive if (line.startsWith('event: ')) eventType = line.slice(7).trim() else if (line.startsWith('data: ')) data += line.slice(6) } if (!data) continue try { const parsed = JSON.parse(data) as Record if (eventType === 'handoff_created' && parsed.type === 'handoff_created') { handlers.onHandoffCreated?.(parsed as unknown as HandoffCreatedEvent) } else if ( eventType === 'handoff_assessment_ready' && parsed.type === 'handoff_assessment_ready' ) { handlers.onAssessmentReady?.( parsed as unknown as HandoffAssessmentReadyEvent, ) } else if (eventType === 'ready') { handlers.onReady?.() } } catch { // skip malformed frame } } } }, async search(q: string, limit: number = 5): Promise { const response = await apiClient.get('/ai-sessions/search', { params: { q, limit }, }) return response.data }, async getSimilar(sessionId: string, limit: number = 5): Promise { const response = await apiClient.get(`/ai-sessions/${sessionId}/similar`, { params: { limit }, }) return response.data }, async generateStatusUpdate(sessionId: string, data: StatusUpdateRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/status-update`, data ) return response.data }, } export default aiSessionsApi