Revert SSE streaming for PDF import, use synchronous flow

SSE streaming added unnecessary complexity and latency due to
buffering issues across Node.js event loop, Nginx proxy, and
Docker layers. Reverted to a simple synchronous request/response
for PDF conversion. Kept extractLlmErrorMessage for user-friendly
LLM errors, lazy-loaded pdf-parse, and extended Nginx timeout.
This commit is contained in:
vakabunga
2026-03-14 20:12:27 +03:00
parent ea234ea007
commit 8b57dd987e
10 changed files with 73 additions and 695 deletions

View File

@@ -2,7 +2,6 @@ import { useState, useRef } from 'react';
import type { ImportStatementResponse } from '@family-budget/shared';
import { importStatement } from '../api/import';
import { updateAccount } from '../api/accounts';
import { useImport } from '../context/ImportContext';
interface Props {
onClose: () => void;
@@ -10,19 +9,13 @@ interface Props {
}
export function ImportModal({ onClose, onDone }: Props) {
const { importState, startImport, clearImport } = useImport();
const [jsonResult, setJsonResult] = useState<ImportStatementResponse | null>(null);
const [jsonError, setJsonError] = useState('');
const [jsonLoading, setJsonLoading] = useState(false);
const [result, setResult] = useState<ImportStatementResponse | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [alias, setAlias] = useState('');
const [aliasSaved, setAliasSaved] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const result = importState.result ?? jsonResult;
const error = importState.error || jsonError;
const loading = importState.active || jsonLoading;
const handleFileChange = async (
e: React.ChangeEvent<HTMLInputElement>,
) => {
@@ -35,45 +28,26 @@ export function ImportModal({ onClose, onDone }: Props) {
const isJson = type === 'application/json' || name.endsWith('.json');
if (!isPdf && !isJson) {
setJsonError('Допустимы только файлы PDF или JSON');
setError('Допустимы только файлы PDF или JSON');
return;
}
setJsonError('');
setJsonResult(null);
setLoading(true);
setError('');
setResult(null);
if (isPdf) {
startImport(file);
return;
}
setJsonLoading(true);
try {
const resp = await importStatement(file);
setJsonResult(resp);
setResult(resp);
} catch (err: unknown) {
const msg =
err instanceof Error ? err.message : 'Ошибка импорта';
setJsonError(msg);
setError(msg);
} finally {
setJsonLoading(false);
setLoading(false);
}
};
const handleClose = () => {
if (importState.active) {
if (window.confirm('Импорт продолжится в фоне. Закрыть окно?')) {
onClose();
}
} else {
onClose();
}
};
const handleDone = () => {
onDone();
};
const handleSaveAlias = async () => {
if (!result || !alias.trim()) return;
try {
@@ -84,21 +58,12 @@ export function ImportModal({ onClose, onDone }: Props) {
}
};
const stageLabel = (stage: string) => {
switch (stage) {
case 'pdf': return 'Извлечение текста...';
case 'llm': return 'Конвертация через LLM...';
case 'import': return 'Сохранение в базу...';
default: return 'Обработка...';
}
};
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Импорт выписки</h2>
<button className="btn-close" onClick={handleClose}>
<button className="btn-close" onClick={onClose}>
&times;
</button>
</div>
@@ -106,7 +71,7 @@ export function ImportModal({ onClose, onDone }: Props) {
<div className="modal-body">
{error && <div className="alert alert-error">{error}</div>}
{!result && !importState.active && (
{!result && (
<div className="import-upload">
<p>Выберите файл выписки (PDF или JSON, формат 1.0)</p>
<input
@@ -115,30 +80,13 @@ export function ImportModal({ onClose, onDone }: Props) {
accept=".pdf,.json,application/pdf,application/json"
onChange={handleFileChange}
className="file-input"
disabled={loading}
/>
{jsonLoading && (
{loading && (
<div className="import-loading">Импорт...</div>
)}
</div>
)}
{importState.active && (
<div className="import-upload">
<div className="import-progress-modal">
<div className="import-progress-modal-bar">
<div
className="import-progress-modal-fill"
style={{ width: `${importState.progress}%` }}
/>
</div>
<p className="import-progress-modal-label">
{stageLabel(importState.stage)} {importState.progress}%
</p>
</div>
</div>
)}
{result && (
<div className="import-result">
<div className="import-result-icon" aria-hidden="true"></div>
@@ -201,15 +149,16 @@ export function ImportModal({ onClose, onDone }: Props) {
<div className="modal-footer">
{result ? (
<button className="btn btn-primary" onClick={handleDone}>
<button className="btn btn-primary" onClick={onDone}>
Готово
</button>
) : (
<button
className="btn btn-secondary"
onClick={handleClose}
onClick={onClose}
disabled={loading}
>
{importState.active ? 'Свернуть' : 'Отмена'}
Отмена
</button>
)}
</div>