frontend_nyla/src/api/chatbotV2Api.ts

240 lines
6.6 KiB
TypeScript

/**
* 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<any>) => Promise<any>;
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<UploadChatbotV2Response> {
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<void> {
try {
const url = `/api/chatbotv2/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`;
const baseURL = api.defaults.baseURL || '';
const fullURL = baseURL + url;
const headers: Record<string, string> = { '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<ChatbotV2Workflow> {
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<void> {
await request({
url: `/api/chatbotv2/${instanceId}/conversations/${workflowId}`,
method: 'delete'
});
}