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:
@@ -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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user