You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
3.5 KiB
TypeScript

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<void> {
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<Response> {
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<SynthesisListItem[]> => {
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<Synthesis> =>
api.get<Synthesis>(`/syntheses/${id}`),
/** DELETE /syntheses/:id -- permanently delete a synthesis. */
remove: (id: string): Promise<void> =>
api.delete<void>(`/syntheses/${id}`),
/** POST /syntheses/generate -- kick off an async generation job, returns a job ID. */
generate: (): Promise<GenerateResponse> =>
api.post<GenerateResponse>('/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<void> =>
api.post<void>(`/syntheses/${id}/send-email`, { email } satisfies SendEmailRequest),
/** Download the synthesis as a Markdown file via {@link fetchFile} + {@link triggerDownload}. */
exportMarkdown: async (id: string): Promise<void> => {
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<void> => {
const response = await fetchFile(`/syntheses/${id}/export/pdf`);
await triggerDownload(response, `synthese-${id}.pdf`);
},
};