Carga en lote concurrente con progreso visual: pool de 3 subidas simultaneas (PDF/audio/txt/imagenes), progreso real de bytes por archivo via XHR con panel de barras animadas, aislamiento de fallos por archivo y SQLite en modo WAL + busy_timeout para subidas concurrentes sin bloqueos. docker-compose.yaml sincronizado con docker-compose.yml
This commit is contained in:
+39
-8
@@ -45,6 +45,44 @@ async function parseResponse<T>(response: Response): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export type UploadProgress = { loaded: number; total: number };
|
||||
export type UploadResult = { source: Source; job: Job };
|
||||
|
||||
function buildUploadUrl(subjectId: number, weekNumber: number, useOcr: boolean, pageRanges?: string) {
|
||||
const params = new URLSearchParams({ use_ocr: String(useOcr) });
|
||||
if (pageRanges?.trim()) params.set("page_ranges", pageRanges.trim());
|
||||
return `${API_URL}/subjects/${subjectId}/weeks/${weekNumber}/files?${params.toString()}`;
|
||||
}
|
||||
|
||||
// fetch no expone progreso de subida; XHR si (xhr.upload.onprogress).
|
||||
export function uploadWithProgress(
|
||||
subjectId: number,
|
||||
weekNumber: number,
|
||||
file: File,
|
||||
useOcr: boolean,
|
||||
onProgress: (progress: UploadProgress) => void,
|
||||
pageRanges?: string,
|
||||
): Promise<UploadResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", buildUploadUrl(subjectId, weekNumber, useOcr, pageRanges));
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable && event.total > 0) onProgress({ loaded: event.loaded, total: event.total });
|
||||
};
|
||||
xhr.onload = () => {
|
||||
let body: { detail?: string } | null = null;
|
||||
try { body = JSON.parse(xhr.responseText || "null"); } catch { body = null; }
|
||||
if (xhr.status >= 200 && xhr.status < 300 && body) resolve(body as UploadResult);
|
||||
else reject(new Error(body?.detail || `Error ${xhr.status}`));
|
||||
};
|
||||
xhr.onerror = () => reject(new Error("Error de red al subir el archivo"));
|
||||
xhr.onabort = () => reject(new Error("Subida cancelada"));
|
||||
xhr.send(form);
|
||||
});
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async health() {
|
||||
return parseResponse<{ status: string }>(await fetch(`${API_URL}/health`, { cache: "no-store" }));
|
||||
@@ -119,14 +157,7 @@ export const api = {
|
||||
return parseResponse<{ source: Source; job: Job }>(await fetch(`${API_URL}/sources/${sourceId}/reprocess?${params.toString()}`, { method: "POST" }));
|
||||
},
|
||||
async upload(subjectId: number, weekNumber: number, file: File, useOcr: boolean, pageRanges?: string) {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const params = new URLSearchParams({ use_ocr: String(useOcr) });
|
||||
if (pageRanges?.trim()) params.set("page_ranges", pageRanges.trim());
|
||||
return parseResponse<{ source: Source; job: Job }>(await fetch(`${API_URL}/subjects/${subjectId}/weeks/${weekNumber}/files?${params.toString()}`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
}));
|
||||
return uploadWithProgress(subjectId, weekNumber, file, useOcr, () => {}, pageRanges);
|
||||
},
|
||||
async markdown(documentId: number) {
|
||||
const response = await fetch(`${API_URL}/documents/${documentId}/markdown`, { cache: "no-store" });
|
||||
|
||||
Reference in New Issue
Block a user