Files
family_budget/frontend/src/context/ImportContext.tsx
vakabunga e28d0f46d0 fix: reopen result modal from progress bar, faster progress, handle LLM context error
- Move import modal visibility into ImportContext so the Layout
  progress pill can reopen the result dialog after the modal was closed.
- Raise LLM progress cap from 85% to 98% and drop the intermediate
  -import 88%- SSE event to eliminate the visual stall after LLM finishes.
- Detect LLM context-length errors (n_keep >= n_ctx) and surface a
  clear message instead of generic -Временная ошибка конвертации-.
2026-03-14 17:05:55 +03:00

115 lines
2.9 KiB
TypeScript

import {
createContext,
useContext,
useState,
useCallback,
useRef,
type ReactNode,
} from 'react';
import type { ImportStatementResponse } from '@family-budget/shared';
import { importStatementStream, type SseEvent } from '../api/import';
export interface ImportProgress {
active: boolean;
stage: string;
progress: number;
message: string;
result?: ImportStatementResponse;
error?: string;
}
interface ImportContextValue {
importState: ImportProgress;
showModal: boolean;
openModal: () => void;
closeModal: () => void;
startImport: (file: File) => void;
clearImport: () => void;
}
const INITIAL: ImportProgress = {
active: false,
stage: '',
progress: 0,
message: '',
};
const ImportContext = createContext<ImportContextValue | null>(null);
export function ImportProvider({ children }: { children: ReactNode }) {
const [importState, setImportState] = useState<ImportProgress>(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;
setImportState({
active: true,
stage: 'pdf',
progress: 0,
message: 'Загрузка файла...',
});
importStatementStream(file, (event: SseEvent) => {
if (event.stage === 'done') {
setImportState({
active: false,
stage: 'done',
progress: 100,
message: 'Импорт завершён',
result: event.result,
});
runningRef.current = false;
} else if (event.stage === 'error') {
setImportState({
active: false,
stage: 'error',
progress: 0,
message: event.message,
error: event.message,
});
runningRef.current = false;
} else {
setImportState({
active: true,
stage: event.stage,
progress: event.progress,
message: event.message,
});
}
}).catch((err) => {
setImportState({
active: false,
stage: 'error',
progress: 0,
message: err instanceof Error ? err.message : 'Ошибка импорта',
error: err instanceof Error ? err.message : 'Ошибка импорта',
});
runningRef.current = false;
});
}, []);
const clearImport = useCallback(() => {
setImportState(INITIAL);
}, []);
return (
<ImportContext.Provider value={{
importState, showModal, openModal, closeModal, startImport, clearImport,
}}>
{children}
</ImportContext.Provider>
);
}
export function useImport(): ImportContextValue {
const ctx = useContext(ImportContext);
if (!ctx) throw new Error('useImport must be used within ImportProvider');
return ctx;
}