Files
aiform_dev/frontend/src/components/form/StepDocumentUpload.tsx
AI Assistant 67f054d0b9 fix: Улучшено логирование SSE - убраны ложные ошибки
- Изменён console.error на console.log для нормального закрытия SSE
- Теперь показывается ' SSE закрыто после получения результата - всё ОК'
- Реальная ошибка выводится только если данные не получены
- Консоль больше не пугает красными ошибками при успешной работе
2025-10-29 12:58:09 +03:00

424 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Upload, Button, Card, Alert, Modal, Spin, Progress, message } from 'antd';
import { UploadOutlined, FileTextOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { useState, useEffect, useRef } from 'react';
import type { UploadFile } from 'antd/es/upload/interface';
interface DocumentConfig {
name: string;
field: string;
file_type: string;
required: boolean;
maxFiles: number;
description: string;
}
interface Props {
documentConfig: DocumentConfig;
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
onPrev: () => void;
isLastDocument: boolean;
currentDocNumber: number;
totalDocs: number;
}
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://147.45.146.17:8100';
const StepDocumentUpload: React.FC<Props> = ({
documentConfig,
formData,
updateFormData,
onNext,
onPrev,
isLastDocument,
currentDocNumber,
totalDocs
}) => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [processingModalVisible, setProcessingModalVisible] = useState(false);
const [processingModalContent, setProcessingModalContent] = useState<any>('loading');
const eventSourceRef = useRef<EventSource | null>(null);
const claimId = formData.claim_id;
const sessionId = formData.session_id || `sess-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Проверяем, загружен ли уже документ
const documentData = formData.documents?.[documentConfig.file_type];
const isAlreadyUploaded = documentData?.uploaded;
useEffect(() => {
// Если документ уже загружен, можно сразу показать кнопку "Продолжить"
if (isAlreadyUploaded) {
console.log(`✅ Документ ${documentConfig.file_type} уже загружен`);
}
}, [isAlreadyUploaded, documentConfig.file_type]);
const handleUpload = async () => {
console.log('🚀 handleUpload called', { fileListLength: fileList.length });
if (fileList.length === 0) {
message.error('Пожалуйста, выберите файл для загрузки');
return;
}
setUploading(true);
try {
// Берём первый файл (у нас только один файл на шаг)
const file = fileList[0];
if (!file.originFileObj) {
message.error('Ошибка: файл не найден');
setUploading(false);
return;
}
console.log('📎 File:', file.name, file.originFileObj);
const formDataToSend = new FormData();
formDataToSend.append('claim_id', claimId);
formDataToSend.append('file_type', documentConfig.file_type);
formDataToSend.append('filename', file.name); // Оригинальное имя файла
formDataToSend.append('voucher', formData.voucher || '');
formDataToSend.append('session_id', sessionId);
formDataToSend.append('upload_timestamp', new Date().toISOString());
formDataToSend.append('file', file.originFileObj); // 'file' - единственное число!
console.log('📤 Uploading to n8n:', {
claim_id: claimId,
session_id: sessionId,
file_type: documentConfig.file_type,
filename: file.name,
voucher: formData.voucher,
upload_timestamp: new Date().toISOString()
});
// Показываем модалку обработки
setProcessingModalVisible(true);
setProcessingModalContent('loading');
// Подключаемся к SSE для получения результата
const event_type = `${documentConfig.file_type}_processed`;
const eventSource = new EventSource(
`${API_BASE_URL}/events/${claimId}?event_type=${event_type}`
);
eventSourceRef.current = eventSource;
eventSource.onmessage = (event) => {
console.log('📨 SSE message received:', event.data);
try {
const result = JSON.parse(event.data);
console.log('📦 Parsed result:', result);
if (result.event_type === event_type) {
setProcessingModalContent(result);
// Сохраняем данные документа в formData
const updatedDocuments = {
...(formData.documents || {}),
[documentConfig.file_type]: {
uploaded: true,
data: result.data,
file_type: documentConfig.file_type
}
};
updateFormData({ documents: updatedDocuments, session_id: sessionId });
}
} catch (error) {
console.error('❌ Error parsing SSE message:', error);
}
};
eventSource.onerror = (error) => {
console.log('🔌 SSE connection closed');
setProcessingModalContent((prev: any) => {
if (prev && prev !== 'loading') {
console.log('✅ SSE закрыто после получения результата - всё ОК');
return prev; // Оставляем полученный результат
}
// Реальная ошибка - не получили данные
console.error('❌ SSE ошибка: не получили данные', error);
return {
success: false,
message: 'Ошибка подключения к серверу',
data: null
};
});
setUploading(false);
eventSource.close();
};
// Отправляем файл на сервер (n8n webhook)
const response = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
method: 'POST',
body: formDataToSend,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
console.log('✅ File uploaded successfully');
} catch (error) {
console.error('❌ Upload error:', error);
message.error('Ошибка загрузки файла');
setUploading(false);
setProcessingModalVisible(false);
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
}
};
const handleContinue = () => {
setProcessingModalVisible(false);
setUploading(false);
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
onNext();
};
const handleSkipDocument = () => {
if (documentConfig.required) {
message.warning('Этот документ обязателен для загрузки');
return;
}
// Пропускаем необязательный документ
const updatedDocuments = {
...(formData.documents || {}),
[documentConfig.file_type]: {
uploaded: false,
skipped: true,
data: null
}
};
updateFormData({ documents: updatedDocuments });
onNext();
};
return (
<div style={{ maxWidth: 700, margin: '0 auto' }}>
<Card>
{/* Прогресс */}
<div style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span style={{ fontSize: 12, color: '#666' }}>
Документ {currentDocNumber} из {totalDocs}
</span>
<span style={{ fontSize: 12, color: '#666' }}>
{Math.round((currentDocNumber / totalDocs) * 100)}% завершено
</span>
</div>
<Progress
percent={Math.round((currentDocNumber / totalDocs) * 100)}
showInfo={false}
strokeColor="#1890ff"
/>
</div>
{/* Заголовок */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>
<FileTextOutlined style={{ marginRight: 8, color: '#1890ff' }} />
{documentConfig.name}
{documentConfig.required && <span style={{ color: '#ff4d4f', marginLeft: 8 }}>*</span>}
</h2>
<p style={{ color: '#666', margin: 0 }}>
{documentConfig.description}
</p>
{!documentConfig.required && (
<p style={{ color: '#faad14', fontSize: 12, marginTop: 4 }}>
Этот документ необязателен, можно пропустить
</p>
)}
</div>
{/* Если документ уже загружен */}
{isAlreadyUploaded && (
<Alert
message="✅ Документ уже загружен"
description="Вы можете продолжить к следующему документу или загрузить другой файл"
type="success"
showIcon
style={{ marginBottom: 24 }}
/>
)}
{/* Загрузка файла */}
<Upload
fileList={fileList}
onChange={({ fileList: newFileList }) => {
console.log('📁 Upload onChange:', newFileList?.length, 'files');
setFileList(newFileList || []);
}}
beforeUpload={() => false}
maxCount={documentConfig.maxFiles}
accept="image/*,application/pdf"
listType="picture"
disabled={uploading}
>
<Button icon={<UploadOutlined />} size="large" block disabled={uploading}>
{documentConfig.maxFiles > 1
? `Выберите файлы (до ${documentConfig.maxFiles})`
: 'Выберите файл'
}
</Button>
</Upload>
<p style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
Поддерживаются: JPG, PNG, PDF (до 10 МБ)
</p>
{/* Кнопки */}
<div style={{ marginTop: 24, display: 'flex', gap: 12 }}>
<Button onClick={onPrev} size="large">
Назад
</Button>
{isAlreadyUploaded ? (
<Button type="primary" onClick={handleContinue} size="large" style={{ flex: 1 }}>
{isLastDocument ? 'Далее: Оплата →' : 'Продолжить →'}
</Button>
) : (
<>
<Button
type="primary"
onClick={handleUpload}
loading={uploading}
disabled={fileList.length === 0}
size="large"
style={{ flex: 1 }}
>
Загрузить и обработать
</Button>
{!documentConfig.required && (
<Button onClick={handleSkipDocument} size="large" disabled={uploading}>
Пропустить
</Button>
)}
</>
)}
</div>
{/* 🔧 DEV MODE */}
<div style={{
marginTop: 24,
padding: 16,
background: '#f0f0f0',
borderRadius: 8,
border: '2px dashed #999'
}}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
🔧 DEV MODE - Быстрая навигация
</div>
<div style={{ display: 'flex', gap: 8 }}>
<Button onClick={onPrev} size="small">
Назад
</Button>
<Button
type="dashed"
onClick={() => {
// Эмулируем загрузку документа
const updatedDocuments = {
...(formData.documents || {}),
[documentConfig.file_type]: {
uploaded: true,
data: { test: 'dev_mode_skip' },
file_type: documentConfig.file_type
}
};
updateFormData({ documents: updatedDocuments });
message.success('DEV: Документ пропущен');
onNext();
}}
size="small"
style={{ flex: 1 }}
>
Пропустить [dev]
</Button>
</div>
</div>
</Card>
{/* Модалка обработки */}
<Modal
title="Обработка документа"
open={processingModalVisible}
onCancel={() => {
if (processingModalContent !== 'loading') {
setProcessingModalVisible(false);
setUploading(false);
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
}
}}
footer={processingModalContent === 'loading' ? null : [
<Button key="continue" type="primary" onClick={handleContinue}>
{isLastDocument ? 'Далее: Оплата →' : 'Продолжить к следующему документу →'}
</Button>
]}
closable={processingModalContent !== 'loading'}
maskClosable={false}
>
{processingModalContent === 'loading' ? (
<div style={{ textAlign: 'center', padding: 24 }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 48 }} spin />} />
<p style={{ marginTop: 16, fontSize: 16 }}>
Обрабатываем документ...
</p>
<p style={{ color: '#999', fontSize: 12 }}>
Извлекаем данные с помощью AI
</p>
</div>
) : (
<div>
<Alert
message={
<span>
<CheckCircleOutlined style={{ marginRight: 8 }} />
Документ обработан
</span>
}
description={processingModalContent.message || 'Данные успешно извлечены'}
type="success"
showIcon={false}
style={{ marginBottom: 16 }}
/>
<div style={{
background: '#f5f5f5',
padding: 12,
borderRadius: 8,
maxHeight: 300,
overflow: 'auto'
}}>
<pre style={{
margin: 0,
fontSize: 12,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{JSON.stringify(processingModalContent.data?.output || processingModalContent.data, null, 2)}
</pre>
</div>
</div>
)}
</Modal>
</div>
);
};
export default StepDocumentUpload;