Files
Nat20-Notes/frontend/src/api.ts

191 lines
7.0 KiB
TypeScript

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<Job> => {
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<any> => {
const res = await fetch(`${BASE_URL}/jobs/${jobId}/cancel`, { method: 'POST' });
return res.json();
},
deleteJob: async (jobId: string, strategy: DeleteStrategy): Promise<any> => {
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<AppSettings> => {
const res = await fetch(`${BASE_URL}/settings`);
return res.json();
},
updateSettings: async (settings: Partial<AppSettings>): Promise<AppSettings> => {
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<AppSettings>): Promise<AppSettings> => {
return api.updateSettings(settings);
},
// Sessions
listSessions: async (): Promise<any[]> => {
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<any> => {
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<any[]> => {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers`);
return res.json();
},
getTurns: async (sessionId: string): Promise<any[]> => {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers/turns`);
return res.json();
},
setSpeakerName: async (sessionId: string, raw_label: string, display_name: string): Promise<void> => {
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<any> => {
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<any> => {
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<any> => {
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;
},
};