/** * Chatbot V2 API * * Context-aware chat: upload files for extraction first, then chat. * Endpoints: /api/chatbotv2/{instanceId}/... */ import { ApiRequestOptions } from '../hooks/useApi'; import api from '../api'; import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils'; import { Message } from '../components/UiComponents/Messages/MessagesTypes'; // ============================================================================ // TYPES // ============================================================================ export interface ChatbotV2Workflow { id: string; mandateId?: string; featureInstanceId?: string; status: string; // extracting | ready | running | stopped name?: string; currentRound?: number; lastActivity?: number; startedAt?: number; extractedContextId?: string; contextFiles?: Array<{ fileId: string; fileName: string; mimeType?: string }>; [key: string]: any; } export interface UploadChatbotV2Request { listFileId: string[]; } export interface UploadChatbotV2Response { conversationId: string; status: string; message?: string; } export interface StartChatbotV2Request { prompt: string; workflowId: string; // conversationId - required for V2 userLanguage?: string; } export interface ChatDataItem { type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status'; createdAt?: number; item: Message | any; label?: string; } export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; export type SSEEventHandler = (item: ChatDataItem) => void; // ============================================================================ // API FUNCTIONS // ============================================================================ /** * Upload files as context and start extraction. * Files must be uploaded to central storage first via /api/files/upload. */ export async function uploadChatbotV2Api( request: ApiRequestFunction, instanceId: string, listFileId: string[] ): Promise { const data = await request({ url: `/api/chatbotv2/${instanceId}/upload`, method: 'post', data: { listFileId } }); return data as UploadChatbotV2Response; } /** * Get chatbot V2 threads (conversations) */ export async function getChatbotV2ThreadsApi( request: ApiRequestFunction, instanceId: string, pagination?: { page?: number; pageSize?: number } ): Promise<{ items: ChatbotV2Workflow[]; pagination?: any }> { const params = pagination ? { pagination: JSON.stringify(pagination) } : undefined; const data = await request({ url: `/api/chatbotv2/${instanceId}/threads`, method: 'get', params }) as any; return { items: Array.isArray(data.items) ? data.items : [], pagination: data.pagination ?? data.metadata }; } /** * Get a specific thread with chat data */ export async function getChatbotV2ThreadApi( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise<{ workflow: ChatbotV2Workflow; chatData: { items: ChatDataItem[] } }> { const data = await request({ url: `/api/chatbotv2/${instanceId}/threads`, method: 'get', params: { workflowId } }) as { workflow: ChatbotV2Workflow; chatData: { items: ChatDataItem[] } }; return { workflow: data.workflow, chatData: data.chatData || { items: [] } }; } /** * Start or continue chat with SSE streaming. * Requires conversationId (workflowId) - must have completed extraction first. */ export async function startChatbotV2StreamApi( instanceId: string, requestBody: StartChatbotV2Request, onEvent: SSEEventHandler, onError?: (error: Error) => void, onComplete?: () => void ): Promise { try { const url = `/api/chatbotv2/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`; const baseURL = api.defaults.baseURL || ''; const fullURL = baseURL + url; const headers: Record = { 'Content-Type': 'application/json' }; const authToken = localStorage.getItem('authToken'); if (authToken) headers['Authorization'] = `Bearer ${authToken}`; if (!getCSRFToken()) generateAndStoreCSRFToken(); addCSRFTokenToHeaders(headers); const response = await fetch(fullURL, { method: 'POST', headers, body: JSON.stringify({ prompt: requestBody.prompt, userLanguage: requestBody.userLanguage || navigator.language || 'de' }), credentials: 'include' }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } if (!response.body) throw new Error('Response body is null'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { 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: ')) { try { const jsonStr = line.slice(6); if (jsonStr.trim()) { const item: ChatDataItem = JSON.parse(jsonStr); onEvent(item); } } catch { // ignore parse errors } } } } if (buffer.trim()) { const lines = buffer.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const jsonStr = line.slice(6); if (jsonStr.trim()) { const item: ChatDataItem = JSON.parse(jsonStr); onEvent(item); } } catch { // ignore } } } } onComplete?.(); } finally { reader.releaseLock(); } } catch (error: any) { if (onError) { onError(error instanceof Error ? error : new Error(String(error))); } else { throw error; } } } /** * Stop a running chat */ export async function stopChatbotV2Api( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { const data = await request({ url: `/api/chatbotv2/${instanceId}/stop/${workflowId}`, method: 'post' }); return data as ChatbotV2Workflow; } /** * Delete a conversation */ export async function deleteChatbotV2Api( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { await request({ url: `/api/chatbotv2/${instanceId}/conversations/${workflowId}`, method: 'delete' }); }