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
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`);
|
|
},
|
|
};
|