const BASE_URL = '/api'; // Flat settings schema - must mirror backend/app/config.py's DEFAULT_SETTINGS // exactly. This is the single source of truth; don't reintroduce a nested // shape here without updating config.py, routers/settings.py, Setup.tsx and // Settings.tsx together. export interface AppSettings { onboarding_completed: boolean; whisper_model: string; whisper_compute_type: string; hf_token: string; llm_mode: 'ollama' | 'api'; ollama_host: string; ollama_model: string; api_base_url: string; api_key: string; api_model: string; chunk_word_target: string; world_context: string; world_context_path: string; } export type JobStatus = 'queued' | 'running' | 'done' | 'error'; export interface Job { id: string; session_id: string; job_type: 'transcribe' | 'summarize'; status: JobStatus; progress: string | null; error: string | null; error_stage: string | null; created_at: number; updated_at: number; } export type DeleteStrategy = 'all' | 'artifacts_only' | 'none'; export const jobApi = { getJob: async (jobId: string): Promise => { const res = await fetch(`${BASE_URL}/jobs/${jobId}`); if (!res.ok) throw new Error('Failed to fetch job'); return res.json(); }, cancelJob: async (jobId: string): Promise => { const res = await fetch(`${BASE_URL}/jobs/${jobId}/cancel`, { method: 'POST' }); return res.json(); }, deleteJob: async (jobId: string, strategy: DeleteStrategy): Promise => { const res = await fetch(`${BASE_URL}/jobs/${jobId}?strategy=${strategy}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete job'); return res.json(); }, }; export const api = { // Settings getSettings: async (): Promise => { const res = await fetch(`${BASE_URL}/settings`); return res.json(); }, updateSettings: async (settings: Partial): Promise => { const res = await fetch(`${BASE_URL}/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }); return res.json(); }, // kept as an alias since some pages call it by this name saveSettings: async (settings: Partial): Promise => { return api.updateSettings(settings); }, // Sessions listSessions: async (): Promise => { const res = await fetch(`${BASE_URL}/sessions`); return res.json(); }, createSession: async (name: string, file: File, onProgress?: (pct: number) => void): Promise<{ session_id: string; job_id: string }> => { const CHUNK_THRESHOLD = 90 * 1024 * 1024; if (file.size > CHUNK_THRESHOLD) { const result = await api.uploadFileInChunks(file, onProgress || (() => {})); const formData = new FormData(); formData.append('name', name); formData.append('upload_path', result.filepath); const res = await fetch(`${BASE_URL}/sessions`, { method: 'POST', body: formData }); if (!res.ok) throw new Error('Failed to create session'); return res.json(); } const formData = new FormData(); formData.append('name', name); formData.append('file', file); const res = await fetch(`${BASE_URL}/sessions`, { method: 'POST', body: formData }); if (!res.ok) throw new Error('Failed to create session'); return res.json(); }, getSession: async (sessionId: string): Promise => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}`); if (!res.ok) throw new Error('Failed to fetch session'); return res.json(); }, retryTranscription: async (sessionId: string): Promise<{ job_id: string }> => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/transcribe`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to start transcription'); return res.json(); }, audioUrl: (sessionId: string): string => `${BASE_URL}/sessions/${sessionId}/audio`, // Speakers getSpeakers: async (sessionId: string): Promise => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers`); return res.json(); }, getTurns: async (sessionId: string): Promise => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers/turns`); return res.json(); }, setSpeakerName: async (sessionId: string, raw_label: string, display_name: string): Promise => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ raw_label, display_name }), }); if (!res.ok) throw new Error('Failed to save speaker name'); }, // Notes getNotes: async (sessionId: string): Promise => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/notes`); if (!res.ok) throw new Error('Notes not generated yet'); return res.json(); }, generateNotes: async (sessionId: string): Promise<{ job_id: string }> => { const res = await fetch(`${BASE_URL}/sessions/${sessionId}/notes/generate`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to start note generation'); return res.json(); }, // Diagnostics getDiagnostics: async (): Promise<{ ok: boolean; checks: any[] }> => { const res = await fetch(`${BASE_URL}/diagnostics`); if (!res.ok) throw new Error('Failed to reach backend'); return res.json(); }, // Jobs (re-exported here so pages that only import `api` still work) deleteJob: jobApi.deleteJob, cancelJob: jobApi.cancelJob, getJob: jobApi.getJob, // Large-file (chunked) upload, used automatically by createSession() // when the file exceeds 90MB. Sends up to 3 chunks concurrently. uploadFileInChunks: async ( file: File, onProgress: (percent: number) => void ): Promise => { const CHUNK_SIZE = 1024 * 1024 * 15; const CONCURRENCY = 3; const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const uploadId = crypto.randomUUID(); const uploadOne = async (chunkIndex: number): Promise => { const start = chunkIndex * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('file', chunk, file.name); formData.append('chunk_index', chunkIndex.toString()); formData.append('total_chunks', totalChunks.toString()); formData.append('filename', file.name); formData.append('upload_id', uploadId); const res = await fetch(`${BASE_URL}/sessions/upload-chunk`, { method: 'POST', body: formData }); if (!res.ok) throw new Error(`Failed to upload chunk ${chunkIndex}`); return res.json(); }; let lastResult: any = null; for (let i = 0; i < totalChunks; i += CONCURRENCY) { const batch = []; for (let j = i; j < Math.min(i + CONCURRENCY, totalChunks); j++) { batch.push(uploadOne(j)); } const results = await Promise.all(batch); lastResult = results[results.length - 1]; onProgress(Math.round((Math.min(i + CONCURRENCY, totalChunks) / totalChunks) * 100)); } return lastResult; }, };