Files
AI-VideoAssistant/web/services/backendApi.ts
2026-02-10 10:50:47 +08:00

717 lines
26 KiB
TypeScript

import { ASRModel, Assistant, CallLog, InteractionDetail, KnowledgeBase, KnowledgeDocument, LLMModel, Tool, Voice, Workflow, WorkflowEdge, WorkflowNode } from '../types';
import { apiRequest } from './apiClient';
type AnyRecord = Record<string, any>;
const readField = <T>(obj: AnyRecord, keys: string[], fallback: T): T => {
for (const key of keys) {
if (obj[key] !== undefined && obj[key] !== null) {
return obj[key] as T;
}
}
return fallback;
};
const normalizeDateLabel = (value: string): string => {
if (!value) return '';
return value.includes('T') ? value.replace('T', ' ').slice(0, 16) : value.slice(0, 16);
};
const formatDuration = (seconds?: number | null): string => {
if (!seconds || seconds <= 0) return '0s';
const m = Math.floor(seconds / 60);
const s = seconds % 60;
if (m === 0) return `${s}s`;
return `${m}m ${s}s`;
};
const mapAssistant = (raw: AnyRecord): Assistant => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
callCount: Number(readField(raw, ['callCount', 'call_count'], 0)),
opener: readField(raw, ['opener'], ''),
prompt: readField(raw, ['prompt'], ''),
knowledgeBaseId: readField(raw, ['knowledgeBaseId', 'knowledge_base_id'], ''),
language: readField(raw, ['language'], 'zh') as 'zh' | 'en',
voice: readField(raw, ['voice'], ''),
speed: Number(readField(raw, ['speed'], 1)),
hotwords: readField(raw, ['hotwords'], []),
tools: readField(raw, ['tools'], []),
interruptionSensitivity: Number(readField(raw, ['interruptionSensitivity', 'interruption_sensitivity'], 500)),
configMode: readField(raw, ['configMode', 'config_mode'], 'platform') as 'platform' | 'dify' | 'fastgpt' | 'none',
apiUrl: readField(raw, ['apiUrl', 'api_url'], ''),
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
llmModelId: readField(raw, ['llmModelId', 'llm_model_id'], ''),
asrModelId: readField(raw, ['asrModelId', 'asr_model_id'], ''),
embeddingModelId: readField(raw, ['embeddingModelId', 'embedding_model_id'], ''),
rerankModelId: readField(raw, ['rerankModelId', 'rerank_model_id'], ''),
});
const mapVoice = (raw: AnyRecord): Voice => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
vendor: ((): string => {
const vendor = String(readField(raw, ['vendor'], ''));
return vendor.toLowerCase() === 'siliconflow' ? '硅基流动' : vendor;
})(),
gender: readField(raw, ['gender'], ''),
language: readField(raw, ['language'], ''),
description: readField(raw, ['description'], ''),
model: readField(raw, ['model'], ''),
voiceKey: readField(raw, ['voiceKey', 'voice_key'], ''),
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
baseUrl: readField(raw, ['baseUrl', 'base_url'], ''),
speed: Number(readField(raw, ['speed'], 1)),
gain: Number(readField(raw, ['gain'], 0)),
pitch: Number(readField(raw, ['pitch'], 0)),
enabled: Boolean(readField(raw, ['enabled'], true)),
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
});
const mapASRModel = (raw: AnyRecord): ASRModel => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
vendor: readField(raw, ['vendor'], 'OpenAI Compatible'),
language: readField(raw, ['language'], 'zh'),
baseUrl: readField(raw, ['baseUrl', 'base_url'], ''),
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
modelName: readField(raw, ['modelName', 'model_name'], ''),
hotwords: readField(raw, ['hotwords'], []),
enablePunctuation: Boolean(readField(raw, ['enablePunctuation', 'enable_punctuation'], true)),
enableNormalization: Boolean(readField(raw, ['enableNormalization', 'enable_normalization'], true)),
enabled: Boolean(readField(raw, ['enabled'], true)),
});
const mapLLMModel = (raw: AnyRecord): LLMModel => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
vendor: readField(raw, ['vendor'], 'OpenAI Compatible'),
type: readField(raw, ['type'], 'text'),
baseUrl: readField(raw, ['baseUrl', 'base_url'], ''),
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
modelName: readField(raw, ['modelName', 'model_name'], ''),
temperature: Number(readField(raw, ['temperature'], 0.7)),
contextLength: Number(readField(raw, ['contextLength', 'context_length'], 0)),
enabled: Boolean(readField(raw, ['enabled'], true)),
});
const mapTool = (raw: AnyRecord): Tool => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
description: readField(raw, ['description'], ''),
category: readField(raw, ['category'], 'system') as 'system' | 'query',
icon: readField(raw, ['icon'], 'Wrench'),
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
enabled: Boolean(readField(raw, ['enabled'], true)),
isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)),
});
const mapWorkflowNode = (raw: AnyRecord): WorkflowNode => ({
id: readField(raw, ['id'], ''),
name: readField(raw, ['name'], String(readField(raw, ['id'], ''))),
type: readField(raw, ['type'], 'assistant') as WorkflowNode['type'],
isStart: readField(raw, ['isStart', 'is_start'], undefined),
metadata: (() => {
const metadata = readField(raw, ['metadata'], null);
if (metadata && typeof metadata === 'object') return metadata;
const position = readField(raw, ['position'], null);
if (position && typeof position === 'object') return { position };
return { position: { x: 200, y: 200 } };
})(),
assistantId: readField(raw, ['assistantId', 'assistant_id'], undefined),
assistant: readField(raw, ['assistant'], undefined),
prompt: readField(raw, ['prompt'], ''),
messagePlan: readField(raw, ['messagePlan', 'message_plan'], undefined),
variableExtractionPlan: readField(raw, ['variableExtractionPlan', 'variable_extraction_plan'], undefined),
tool: readField(raw, ['tool'], undefined),
globalNodePlan: readField(raw, ['globalNodePlan', 'global_node_plan'], undefined),
});
const mapWorkflowEdge = (raw: AnyRecord): WorkflowEdge => ({
id: readField(raw, ['id'], undefined),
fromNodeId: readField(raw, ['fromNodeId', 'from', 'from_', 'source'], ''),
toNodeId: readField(raw, ['toNodeId', 'to', 'target'], ''),
from: readField(raw, ['fromNodeId', 'from', 'from_', 'source'], ''),
to: readField(raw, ['toNodeId', 'to', 'target'], ''),
label: readField(raw, ['label'], undefined),
condition: readField(raw, ['condition'], undefined),
priority: Number(readField(raw, ['priority'], 100)),
});
const mapWorkflow = (raw: AnyRecord): Workflow => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
nodeCount: Number(readField(raw, ['nodeCount', 'node_count'], Array.isArray(raw.nodes) ? raw.nodes.length : 0)),
createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')),
updatedAt: normalizeDateLabel(readField(raw, ['updatedAt', 'updated_at'], '')),
nodes: readField(raw, ['nodes'], []).map((node: AnyRecord) => mapWorkflowNode(node)),
edges: readField(raw, ['edges'], []).map((edge: AnyRecord) => mapWorkflowEdge(edge)),
globalPrompt: readField(raw, ['globalPrompt', 'global_prompt'], ''),
});
const mapKnowledgeDocument = (raw: AnyRecord): KnowledgeDocument => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
size: readField(raw, ['size'], ''),
uploadDate: normalizeDateLabel(readField(raw, ['uploadDate', 'upload_date', 'created_at'], '')),
status: readField(raw, ['status'], 'pending'),
chunkCount: Number(readField(raw, ['chunkCount', 'chunk_count'], 0)),
});
const mapKnowledgeBase = (raw: AnyRecord): KnowledgeBase => ({
id: String(readField(raw, ['id'], '')),
name: readField(raw, ['name'], ''),
creator: 'Admin',
createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')),
description: readField(raw, ['description'], ''),
embeddingModel: readField(raw, ['embeddingModel', 'embedding_model'], ''),
chunkSize: Number(readField(raw, ['chunkSize', 'chunk_size'], 500)),
chunkOverlap: Number(readField(raw, ['chunkOverlap', 'chunk_overlap'], 50)),
status: readField(raw, ['status'], 'active'),
documents: readField(raw, ['documents'], []).map((doc: AnyRecord) => mapKnowledgeDocument(doc)),
});
const toHistoryRow = (raw: AnyRecord, assistantNameMap: Map<string, string>): CallLog => {
const assistantId = readField(raw, ['assistant_id', 'assistantId'], '');
const startTime = normalizeDateLabel(readField(raw, ['started_at', 'startTime'], ''));
const type = readField(raw, ['type'], 'text');
return {
id: String(readField(raw, ['id'], '')),
source: readField(raw, ['source'], 'debug') as 'debug' | 'external',
status: readField(raw, ['status'], 'connected') as 'connected' | 'missed',
startTime,
duration: formatDuration(readField(raw, ['duration_seconds', 'durationSeconds'], 0)),
agentName: assistantNameMap.get(String(assistantId)) || String(assistantId || 'Unknown Assistant'),
type: type === 'audio' || type === 'video' ? type : 'text',
details: [],
};
};
const toHistoryDetails = (raw: AnyRecord): InteractionDetail[] => {
const transcripts = readField(raw, ['transcripts'], []);
return transcripts.map((t: AnyRecord) => ({
role: readField(t, ['speaker'], 'human') === 'human' ? 'user' : 'assistant',
content: readField(t, ['content'], ''),
timestamp: `${Math.floor(Number(readField(t, ['startMs', 'start_ms'], 0)) / 1000)}s`,
audioUrl: readField(t, ['audioUrl', 'audio_url'], undefined),
}));
};
export const fetchAssistants = async (): Promise<Assistant[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/assistants');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapAssistant(item));
};
export const createAssistant = async (data: Partial<Assistant>): Promise<Assistant> => {
const payload = {
name: data.name || 'New Assistant',
opener: data.opener || '',
prompt: data.prompt || '',
knowledgeBaseId: data.knowledgeBaseId || '',
language: data.language || 'zh',
voice: data.voice || '',
speed: data.speed ?? 1,
hotwords: data.hotwords || [],
tools: data.tools || [],
interruptionSensitivity: data.interruptionSensitivity ?? 500,
configMode: data.configMode || 'platform',
apiUrl: data.apiUrl || '',
apiKey: data.apiKey || '',
llmModelId: data.llmModelId || '',
asrModelId: data.asrModelId || '',
embeddingModelId: data.embeddingModelId || '',
rerankModelId: data.rerankModelId || '',
};
const response = await apiRequest<AnyRecord>('/assistants', { method: 'POST', body: payload });
return mapAssistant(response);
};
export const updateAssistant = async (id: string, data: Partial<Assistant>): Promise<Assistant> => {
const payload = {
name: data.name,
opener: data.opener,
prompt: data.prompt,
knowledgeBaseId: data.knowledgeBaseId,
language: data.language,
voice: data.voice,
speed: data.speed,
hotwords: data.hotwords,
tools: data.tools,
interruptionSensitivity: data.interruptionSensitivity,
configMode: data.configMode,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
llmModelId: data.llmModelId,
asrModelId: data.asrModelId,
embeddingModelId: data.embeddingModelId,
rerankModelId: data.rerankModelId,
};
const response = await apiRequest<AnyRecord>(`/assistants/${id}`, { method: 'PUT', body: payload });
return mapAssistant(response);
};
export const deleteAssistant = async (id: string): Promise<void> => {
await apiRequest(`/assistants/${id}`, { method: 'DELETE' });
};
export interface AssistantRuntimeConfigResponse {
assistantId: string;
sessionStartMetadata: Record<string, any>;
sources?: {
llmModelId?: string;
asrModelId?: string;
voiceId?: string;
};
warnings?: string[];
}
export const fetchAssistantRuntimeConfig = async (assistantId: string): Promise<AssistantRuntimeConfigResponse> => {
return apiRequest<AssistantRuntimeConfigResponse>(`/assistants/${assistantId}/runtime-config`);
};
export const fetchVoices = async (): Promise<Voice[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/voices');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapVoice(item));
};
export const createVoice = async (data: Partial<Voice>): Promise<Voice> => {
const payload = {
id: data.id || undefined,
name: data.name || 'New Voice',
vendor: data.vendor === '硅基流动' ? 'SiliconFlow' : (data.vendor || 'SiliconFlow'),
gender: data.gender || 'Female',
language: data.language || 'zh',
description: data.description || '',
model: data.model || undefined,
voice_key: data.voiceKey || undefined,
api_key: data.apiKey || undefined,
base_url: data.baseUrl || undefined,
speed: data.speed ?? 1,
gain: data.gain ?? 0,
pitch: data.pitch ?? 0,
enabled: data.enabled ?? true,
};
const response = await apiRequest<AnyRecord>('/voices', { method: 'POST', body: payload });
return mapVoice(response);
};
export const updateVoice = async (id: string, data: Partial<Voice>): Promise<Voice> => {
const payload = {
name: data.name,
vendor: data.vendor === '硅基流动' ? 'SiliconFlow' : data.vendor,
gender: data.gender,
language: data.language,
description: data.description,
model: data.model,
voice_key: data.voiceKey,
api_key: data.apiKey,
base_url: data.baseUrl,
speed: data.speed,
gain: data.gain,
pitch: data.pitch,
enabled: data.enabled,
};
const response = await apiRequest<AnyRecord>(`/voices/${id}`, { method: 'PUT', body: payload });
return mapVoice(response);
};
export const deleteVoice = async (id: string): Promise<void> => {
await apiRequest(`/voices/${id}`, { method: 'DELETE' });
};
export const previewVoice = async (id: string, text: string, speed?: number, apiKey?: string): Promise<string> => {
const response = await apiRequest<{ success: boolean; audio_url?: string; error?: string }>(`/voices/${id}/preview`, {
method: 'POST',
body: { text, speed, api_key: apiKey },
});
if (!response.success || !response.audio_url) {
throw new Error(response.error || 'Preview failed');
}
return response.audio_url;
};
export const fetchASRModels = async (): Promise<ASRModel[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/asr');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapASRModel(item));
};
export const createASRModel = async (data: Partial<ASRModel>): Promise<ASRModel> => {
const payload = {
id: data.id || undefined,
name: data.name || 'New ASR Model',
vendor: data.vendor || 'OpenAI Compatible',
language: data.language || 'zh',
base_url: data.baseUrl || 'https://api.siliconflow.cn/v1',
api_key: data.apiKey || '',
model_name: data.modelName || undefined,
hotwords: data.hotwords || [],
enable_punctuation: data.enablePunctuation ?? true,
enable_normalization: data.enableNormalization ?? true,
enabled: data.enabled ?? true,
};
const response = await apiRequest<AnyRecord>('/asr', { method: 'POST', body: payload });
return mapASRModel(response);
};
export const updateASRModel = async (id: string, data: Partial<ASRModel>): Promise<ASRModel> => {
const payload = {
name: data.name,
vendor: data.vendor,
language: data.language,
base_url: data.baseUrl,
api_key: data.apiKey,
model_name: data.modelName,
hotwords: data.hotwords,
enable_punctuation: data.enablePunctuation,
enable_normalization: data.enableNormalization,
enabled: data.enabled,
};
const response = await apiRequest<AnyRecord>(`/asr/${id}`, { method: 'PUT', body: payload });
return mapASRModel(response);
};
export const deleteASRModel = async (id: string): Promise<void> => {
await apiRequest(`/asr/${id}`, { method: 'DELETE' });
};
export type ASRPreviewResult = {
success: boolean;
transcript?: string;
language?: string;
confidence?: number;
latency_ms?: number;
message?: string;
error?: string;
};
export const previewASRModel = async (
id: string,
file: File,
options?: { language?: string; apiKey?: string }
): Promise<ASRPreviewResult> => {
const formData = new FormData();
formData.append('file', file);
if (options?.language) {
formData.append('language', options.language);
}
if (options?.apiKey) {
formData.append('api_key', options.apiKey);
}
const base = (import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8100/api').replace(/\/+$/, '');
const url = `${base}/asr/${id}/preview`;
const response = await fetch(url, {
method: 'POST',
body: formData,
});
let data: ASRPreviewResult | null = null;
try {
data = await response.json();
} catch {
data = null;
}
if (!response.ok) {
const detail = (data as AnyRecord | null)?.error || (data as AnyRecord | null)?.detail || `Request failed: ${response.status}`;
throw new Error(typeof detail === 'string' ? detail : `Request failed: ${response.status}`);
}
return data || { success: false, error: 'Invalid preview response' };
};
export const fetchLLMModels = async (): Promise<LLMModel[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/llm');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapLLMModel(item));
};
export const createLLMModel = async (data: Partial<LLMModel>): Promise<LLMModel> => {
const payload = {
id: data.id || undefined,
name: data.name || 'New LLM Model',
vendor: data.vendor || 'OpenAI Compatible',
type: data.type || 'text',
base_url: data.baseUrl || '',
api_key: data.apiKey || '',
model_name: data.modelName || undefined,
temperature: data.temperature,
context_length: data.contextLength,
enabled: data.enabled ?? true,
};
const response = await apiRequest<AnyRecord>('/llm', { method: 'POST', body: payload });
return mapLLMModel(response);
};
export const updateLLMModel = async (id: string, data: Partial<LLMModel>): Promise<LLMModel> => {
const payload = {
name: data.name,
vendor: data.vendor,
type: data.type,
base_url: data.baseUrl,
api_key: data.apiKey,
model_name: data.modelName,
temperature: data.temperature,
context_length: data.contextLength,
enabled: data.enabled,
};
const response = await apiRequest<AnyRecord>(`/llm/${id}`, { method: 'PUT', body: payload });
return mapLLMModel(response);
};
export const deleteLLMModel = async (id: string): Promise<void> => {
await apiRequest(`/llm/${id}`, { method: 'DELETE' });
};
export type LLMPreviewResult = {
success: boolean;
reply?: string;
usage?: Record<string, number>;
latency_ms?: number;
error?: string;
};
export const previewLLMModel = async (
id: string,
payload: { message: string; system_prompt?: string; max_tokens?: number; temperature?: number; api_key?: string }
): Promise<LLMPreviewResult> => {
return apiRequest<LLMPreviewResult>(`/llm/${id}/preview`, {
method: 'POST',
body: payload,
});
};
export const fetchTools = async (): Promise<Tool[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/tools/resources');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapTool(item));
};
export const createTool = async (data: Partial<Tool>): Promise<Tool> => {
const payload = {
id: data.id || undefined,
name: data.name || 'New Tool',
description: data.description || '',
category: data.category || 'system',
icon: data.icon || (data.category === 'query' ? 'Globe' : 'Terminal'),
enabled: data.enabled ?? true,
};
const response = await apiRequest<AnyRecord>('/tools/resources', { method: 'POST', body: payload });
return mapTool(response);
};
export const updateTool = async (id: string, data: Partial<Tool>): Promise<Tool> => {
const payload = {
name: data.name,
description: data.description,
category: data.category,
icon: data.icon,
enabled: data.enabled,
};
const response = await apiRequest<AnyRecord>(`/tools/resources/${id}`, { method: 'PUT', body: payload });
return mapTool(response);
};
export const deleteTool = async (id: string): Promise<void> => {
await apiRequest(`/tools/resources/${id}`, { method: 'DELETE' });
};
export const fetchWorkflows = async (): Promise<Workflow[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/workflows');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapWorkflow(item));
};
export const fetchWorkflowById = async (id: string): Promise<Workflow> => {
const list = await fetchWorkflows();
const workflow = list.find((item) => item.id === id);
if (!workflow) {
throw new Error('Workflow not found');
}
return workflow;
};
export const createWorkflow = async (data: Partial<Workflow>): Promise<Workflow> => {
const payload = {
name: data.name || '新工作流',
nodeCount: data.nodeCount ?? data.nodes?.length ?? 0,
createdAt: data.createdAt || '',
updatedAt: data.updatedAt || '',
globalPrompt: data.globalPrompt || '',
nodes: data.nodes || [],
edges: data.edges || [],
};
const response = await apiRequest<AnyRecord>('/workflows', { method: 'POST', body: payload });
return mapWorkflow(response);
};
export const updateWorkflow = async (id: string, data: Partial<Workflow>): Promise<Workflow> => {
const payload = {
name: data.name,
nodeCount: data.nodeCount ?? data.nodes?.length,
nodes: data.nodes,
edges: data.edges,
globalPrompt: data.globalPrompt,
};
const response = await apiRequest<AnyRecord>(`/workflows/${id}`, { method: 'PUT', body: payload });
return mapWorkflow(response);
};
export const deleteWorkflow = async (id: string): Promise<void> => {
await apiRequest(`/workflows/${id}`, { method: 'DELETE' });
};
export const fetchKnowledgeBases = async (): Promise<KnowledgeBase[]> => {
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/knowledge/bases');
const list = Array.isArray(response) ? response : (response.list || []);
return list.map((item) => mapKnowledgeBase(item));
};
export const fetchKnowledgeBaseById = async (kbId: string): Promise<KnowledgeBase> => {
const response = await apiRequest<AnyRecord>(`/knowledge/bases/${kbId}`);
return mapKnowledgeBase(response);
};
export const createKnowledgeBase = async (data: {
name: string;
description?: string;
embeddingModel?: string;
chunkSize?: number;
chunkOverlap?: number;
}): Promise<KnowledgeBase> => {
const payload = {
name: data.name,
description: data.description || '',
embeddingModel: data.embeddingModel || 'text-embedding-3-small',
chunkSize: data.chunkSize ?? 500,
chunkOverlap: data.chunkOverlap ?? 50,
};
const response = await apiRequest<AnyRecord>('/knowledge/bases', { method: 'POST', body: payload });
return mapKnowledgeBase(response);
};
export const updateKnowledgeBase = async (kbId: string, data: Partial<KnowledgeBase>): Promise<KnowledgeBase> => {
const payload = {
name: data.name,
description: data.description,
embeddingModel: data.embeddingModel,
chunkSize: data.chunkSize,
chunkOverlap: data.chunkOverlap,
status: data.status,
};
const response = await apiRequest<AnyRecord>(`/knowledge/bases/${kbId}`, { method: 'PUT', body: payload });
return mapKnowledgeBase(response);
};
export const deleteKnowledgeBase = async (kbId: string): Promise<void> => {
await apiRequest(`/knowledge/bases/${kbId}`, { method: 'DELETE' });
};
export const uploadKnowledgeDocument = async (kbId: string, file: File): Promise<void> => {
const formData = new FormData();
formData.append('file', file);
formData.append('name', file.name);
formData.append('size', `${file.size} bytes`);
formData.append('file_type', file.type || 'application/octet-stream');
const base = (import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8100/api').replace(/\/+$/, '');
const url = `${base}/knowledge/bases/${kbId}/documents`;
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
let message = `Upload failed: ${response.status}`;
try {
const data = await response.json();
if (data?.detail) {
message = typeof data.detail === 'string' ? data.detail : message;
}
} catch {
// ignore parse error
}
throw new Error(message);
}
};
export const deleteKnowledgeDocument = async (kbId: string, docId: string): Promise<void> => {
await apiRequest(`/knowledge/bases/${kbId}/documents/${docId}`, { method: 'DELETE' });
};
export const indexKnowledgeDocument = async (
kbId: string,
docId: string,
content: string,
): Promise<{ message: string; chunkCount: number }> => {
return apiRequest<{ message: string; chunkCount: number }>(
`/knowledge/bases/${kbId}/documents/${docId}/index`,
{
method: 'POST',
body: {
document_id: docId,
content,
},
}
);
};
export type KnowledgeSearchResultItem = {
content: string;
metadata?: {
document_id?: string;
chunk_index?: number;
kb_id?: string;
[key: string]: any;
};
distance?: number;
};
export type KnowledgeSearchResponse = {
query: string;
results: KnowledgeSearchResultItem[];
};
export const searchKnowledgeBase = async (
kbId: string,
query: string,
nResults: number = 5
): Promise<KnowledgeSearchResponse> => {
return apiRequest<KnowledgeSearchResponse>('/knowledge/search', {
method: 'POST',
body: {
kb_id: kbId,
query,
nResults,
},
});
};
export const fetchHistory = async (): Promise<CallLog[]> => {
const [historyResp, assistantsResp] = await Promise.all([
apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/history'),
apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/assistants'),
]);
const assistantList = Array.isArray(assistantsResp) ? assistantsResp : (assistantsResp.list || []);
const assistantNameMap = new Map<string, string>(assistantList.map((a: AnyRecord) => [String(readField(a, ['id'], '')), readField(a, ['name'], '')]));
const historyList = Array.isArray(historyResp) ? historyResp : (historyResp.list || []);
return historyList.map((item) => toHistoryRow(item, assistantNameMap));
};
export const fetchHistoryDetail = async (id: string, base: CallLog): Promise<CallLog> => {
const response = await apiRequest<AnyRecord>(`/history/${id}`);
const details = toHistoryDetails(response);
const duration = formatDuration(readField(response, ['duration_seconds', 'durationSeconds'], 0));
const startTime = normalizeDateLabel(readField(response, ['started_at'], base.startTime));
return {
...base,
startTime,
duration,
details,
};
};