feat: stream PDF import progress via SSE with global progress bar

Replace the synchronous PDF import with Server-Sent Events streaming
between the backend (LLM) and the browser. The user can now close the
import modal and continue working while the conversion runs — a fixed
progress bar in the Layout shows real-time stage and percentage.
This commit is contained in:
vakabunga
2026-03-14 16:18:31 +03:00
parent 627706228b
commit 1d7fbea9ef
9 changed files with 680 additions and 70 deletions

View File

@@ -11,3 +11,74 @@ export async function importStatement(
formData,
);
}
export interface SseProgressEvent {
stage: 'pdf' | 'llm' | 'import';
progress: number;
message: string;
}
export interface SseDoneEvent {
stage: 'done';
progress: 100;
result: ImportStatementResponse;
}
export interface SseErrorEvent {
stage: 'error';
message: string;
}
export type SseEvent = SseProgressEvent | SseDoneEvent | SseErrorEvent;
export async function importStatementStream(
file: File,
onEvent: (event: SseEvent) => void,
): Promise<void> {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/import/statement', {
method: 'POST',
body: formData,
credentials: 'include',
});
if (!res.ok) {
let msg = 'Ошибка импорта';
try {
const body = await res.json();
if (body.message) msg = body.message;
} catch { /* use default */ }
onEvent({ stage: 'error', message: msg });
return;
}
const reader = res.body?.getReader();
if (!reader) {
onEvent({ stage: 'error', message: 'Streaming не поддерживается' });
return;
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data: ')) continue;
try {
const parsed = JSON.parse(trimmed.slice(6)) as SseEvent;
onEvent(parsed);
} catch { /* skip malformed lines */ }
}
}
}