- 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 -Временная ошибка конвертации-.
115 lines
2.9 KiB
TypeScript
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;
|
|
}
|