diff --git a/backend/src/routes/import.ts b/backend/src/routes/import.ts index a988215..0823f07 100644 --- a/backend/src/routes/import.ts +++ b/backend/src/routes/import.ts @@ -79,12 +79,6 @@ router.post( return; } - sseWrite(res, { - stage: 'import', - progress: 88, - message: 'Импорт в базу данных...', - }); - const result = await importStatement(converted); if (isValidationError(result)) { sseWrite(res, { diff --git a/backend/src/services/pdfToStatement.ts b/backend/src/services/pdfToStatement.ts index 6bc8401..c7fe5a6 100644 --- a/backend/src/services/pdfToStatement.ts +++ b/backend/src/services/pdfToStatement.ts @@ -137,7 +137,7 @@ export async function convertPdfToStatement( return { status: 502, error: 'BAD_GATEWAY', - message: 'Временная ошибка конвертации', + message: extractLlmErrorMessage(err), }; } } @@ -146,7 +146,7 @@ export type ProgressStage = 'pdf' | 'llm' | 'import'; export type OnProgress = (stage: ProgressStage, progress: number, message: string) => void; const LLM_PROGRESS_MIN = 10; -const LLM_PROGRESS_MAX = 85; +const LLM_PROGRESS_MAX = 98; const LLM_PROGRESS_RANGE = LLM_PROGRESS_MAX - LLM_PROGRESS_MIN; const THROTTLE_MS = 300; @@ -248,11 +248,25 @@ export async function convertPdfToStatementStreaming( return { status: 502, error: 'BAD_GATEWAY', - message: 'Временная ошибка конвертации', + message: extractLlmErrorMessage(err), }; } } +function extractLlmErrorMessage(err: unknown): string { + const raw = String( + (err as Record)?.message ?? + (err as Record>)?.error?.message ?? '', + ); + if (/context.length|n_ctx|too.many.tokens|maximum.context/i.test(raw)) { + return 'PDF-файл слишком большой для обработки. Попробуйте файл с меньшим количеством операций или используйте модель с большим контекстным окном.'; + } + if (/timeout|timed?\s*out|ETIMEDOUT|ECONNREFUSED/i.test(raw)) { + return 'LLM-сервер не отвечает. Проверьте, что сервер запущен и доступен.'; + } + return 'Временная ошибка конвертации'; +} + function parseConversionResult(content: string): StatementFile | PdfConversionError { const jsonMatch = content.match(/\{[\s\S]*\}/); const jsonStr = jsonMatch ? jsonMatch[0] : content; diff --git a/frontend/src/components/ImportModal.tsx b/frontend/src/components/ImportModal.tsx index bae22db..fe98df6 100644 --- a/frontend/src/components/ImportModal.tsx +++ b/frontend/src/components/ImportModal.tsx @@ -66,13 +66,11 @@ export function ImportModal({ onClose, onDone }: Props) { onClose(); } } else { - if (result || importState.error) clearImport(); onClose(); } }; const handleDone = () => { - clearImport(); onDone(); }; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4390f97..a1e7b08 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -4,7 +4,7 @@ import { useAuth } from '../context/AuthContext'; import { useImport } from '../context/ImportContext'; function ImportProgressBar() { - const { importState, clearImport } = useImport(); + const { importState, clearImport, openModal } = useImport(); const [visible, setVisible] = useState(false); const hideTimerRef = useRef | undefined>(undefined); @@ -31,9 +31,9 @@ function ImportProgressBar() { const handleClick = useCallback(() => { if (hideTimerRef.current) clearTimeout(hideTimerRef.current); + openModal(); setVisible(false); - clearImport(); - }, [clearImport]); + }, [openModal]); if (!visible) return null; diff --git a/frontend/src/context/ImportContext.tsx b/frontend/src/context/ImportContext.tsx index b0edff1..3fb5344 100644 --- a/frontend/src/context/ImportContext.tsx +++ b/frontend/src/context/ImportContext.tsx @@ -20,6 +20,9 @@ export interface ImportProgress { interface ImportContextValue { importState: ImportProgress; + showModal: boolean; + openModal: () => void; + closeModal: () => void; startImport: (file: File) => void; clearImport: () => void; } @@ -35,8 +38,12 @@ const ImportContext = createContext(null); export function ImportProvider({ children }: { children: ReactNode }) { const [importState, setImportState] = useState(INITIAL); + const [showModal, setShowModal] = useState(false); const runningRef = useRef(false); + const openModal = useCallback(() => setShowModal(true), []); + const closeModal = useCallback(() => setShowModal(false), []); + const startImport = useCallback((file: File) => { if (runningRef.current) return; runningRef.current = true; @@ -92,7 +99,9 @@ export function ImportProvider({ children }: { children: ReactNode }) { }, []); return ( - + {children} ); diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx index e1a6c0c..40334a3 100644 --- a/frontend/src/pages/HistoryPage.tsx +++ b/frontend/src/pages/HistoryPage.tsx @@ -18,6 +18,7 @@ import { TransactionTable } from '../components/TransactionTable'; import { Pagination } from '../components/Pagination'; import { EditTransactionModal } from '../components/EditTransactionModal'; import { ImportModal } from '../components/ImportModal'; +import { useImport } from '../context/ImportContext'; import { toISODate } from '../utils/format'; const PARAM_KEYS = [ @@ -125,7 +126,7 @@ export function HistoryPage() { const [accounts, setAccounts] = useState([]); const [categories, setCategories] = useState([]); const [editingTx, setEditingTx] = useState(null); - const [showImport, setShowImport] = useState(false); + const { showModal: showImport, openModal: openImport, closeModal: closeImport, clearImport } = useImport(); useEffect(() => { getAccounts().then(setAccounts).catch(() => {}); @@ -197,7 +198,8 @@ export function HistoryPage() { }; const handleImportDone = () => { - setShowImport(false); + closeImport(); + clearImport(); fetchData(); getAccounts().then(setAccounts).catch(() => {}); }; @@ -208,7 +210,7 @@ export function HistoryPage() {

История операций

@@ -262,7 +264,7 @@ export function HistoryPage() { {showImport && ( setShowImport(false)} + onClose={closeImport} onDone={handleImportDone} /> )}