import { api } from './client'; import type { SynthesisListItem, Synthesis, GenerateResponse, SendEmailRequest } from '~/types'; const API_BASE = '/api/v1'; /** * Trigger a file download from a fetch Response. * Reads the blob, creates a temporary object URL, clicks a hidden anchor, then cleans up. */ export async function triggerDownload(response: Response, fallbackFilename: string): Promise { const blob = await response.blob(); // Try to extract filename from Content-Disposition header const disposition = response.headers.get('Content-Disposition'); let filename = fallbackFilename; if (disposition) { const match = disposition.match(/filename="?([^";\n]+)"?/); if (match) { filename = match[1]; } } const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } /** * Perform an authenticated GET request that expects a binary/file response. * Throws an ApiError-shaped object on failure. */ export async function fetchFile(path: string): Promise { const response = await fetch(`${API_BASE}${path}`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', }); if (!response.ok) { if (response.status === 401) { window.location.href = '/login'; } const errorBody = await response.json().catch(() => ({ error: 'Unknown error' })); throw { status: response.status, message: errorBody.error || errorBody.message || `HTTP ${response.status}`, }; } return response; } /** Synthesis API endpoints (CRUD, generation, export, email). */ export const synthesesApi = { /** GET /syntheses -- paginated list of the user's syntheses. */ list: async (limit = 50, offset = 0): Promise => { const response = await api.get<{ items: SynthesisListItem[] }>(`/syntheses?limit=${limit}&offset=${offset}`); return response.items; }, /** GET /syntheses/:id -- fetch a single synthesis with full content. */ get: (id: string): Promise => api.get(`/syntheses/${id}`), /** DELETE /syntheses/:id -- permanently delete a synthesis. */ remove: (id: string): Promise => api.delete(`/syntheses/${id}`), /** POST /syntheses/generate -- kick off an async generation job, returns a job ID. */ generate: (): Promise => api.post('/syntheses/generate'), /** Build the SSE endpoint URL for streaming generation progress. */ progressUrl: (jobId: string): string => `${API_BASE}/syntheses/generate/${jobId}/progress`, /** POST /syntheses/:id/send-email -- email the synthesis to the given address. */ sendEmail: (id: string, email: string): Promise => api.post(`/syntheses/${id}/send-email`, { email } satisfies SendEmailRequest), /** Download the synthesis as a Markdown file via {@link fetchFile} + {@link triggerDownload}. */ exportMarkdown: async (id: string): Promise => { const response = await fetchFile(`/syntheses/${id}/export/markdown`); await triggerDownload(response, `synthese-${id}.md`); }, /** Download the synthesis as a PDF file via {@link fetchFile} + {@link triggerDownload}. */ exportPdf: async (id: string): Promise => { const response = await fetchFile(`/syntheses/${id}/export/pdf`); await triggerDownload(response, `synthese-${id}.pdf`); }, };