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