diff --git a/frontend/src/components/form/Step1Policy.tsx b/frontend/src/components/form/Step1Policy.tsx index cad1043..8be77cd 100644 --- a/frontend/src/components/form/Step1Policy.tsx +++ b/frontend/src/components/form/Step1Policy.tsx @@ -622,6 +622,168 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug ) : null} + {/* 🔧 Технические кнопки для разработки */} +
+
+ 🔧 DEV MODE - Быстрая навигация (без валидации) +
+
+ +
+
+ + ); +} + + )} + + + + {/* Прогресс обработки */} + {uploading && uploadProgress && ( + } />} + style={{ marginBottom: 16 }} + /> + )} + + +
+ + +
+
+ + )} + + {!policyNotFound && ( +
+

+ 💡 Введите номер полиса. Кириллица автоматически заменяется на латиницу, тире вставляется автоматически +

+
+ )} + + {/* Модальное окно ожидания OCR результата */} + { + setOcrModalVisible(false); + onNext(); // Переход на следующий шаг + }}> + Продолжить → + + ] : [ + // ❌ Полис не распознан - кнопка "Загрузить другой файл" + + ] + } + width={700} + centered + > + {ocrModalContent === 'loading' ? ( +
+ } /> +

⏳ Обрабатываем документ

+

OCR распознавание текста...

+

AI анализ содержимого...

+

Проверка валидности полиса...

+

+ Это может занять 20-30 секунд. Пожалуйста, подождите... +

+
+ ) : ocrModalContent ? ( +
+

+ {ocrModalContent.success ? '✅ Результат распознавания' : '❌ Ошибка распознавания'} +

+ {ocrModalContent.success ? ( +
+

Номер полиса: {ocrModalContent.data?.policy_number || 'н/д'}

+

Владелец: {ocrModalContent.data?.policyholder_full_name || 'н/д'}

+ {ocrModalContent.data?.insured_persons?.length > 0 && ( + <> +

Застрахованные лица:

+
    + {ocrModalContent.data.insured_persons.map((person: any, i: number) => ( +
  • {person.full_name} (ДР: {person.birth_date || 'н/д'})
  • + ))} +
+ + )} + {ocrModalContent.data?.policy_period && ( +

Период: {ocrModalContent.data.policy_period.insured_from} - {ocrModalContent.data.policy_period.insured_to}

+ )} +

Полный ответ AI:

+
+                  {JSON.stringify(ocrModalContent.data, null, 2)}
+                
+
+ ) : ( +
+

{ocrModalContent.message || 'Документ не распознан'}

+

Полный ответ:

+
+                  {JSON.stringify(ocrModalContent.data, null, 2)}
+                
+
+ )} +
+ ) : null} +
+ {/* 🔧 Технические кнопки для разработки */}
= { export default function Step2Details({ formData, updateFormData, onNext, onPrev, addDebugEvent }: Props) { const [form] = Form.useForm(); const [eventType, setEventType] = useState(formData.eventType || ''); - const [documentFiles, setDocumentFiles] = useState>({}); + const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0); + const [processedDocuments, setProcessedDocuments] = useState>({}); + const [currentFile, setCurrentFile] = useState(null); const [uploading, setUploading] = useState(false); - const [uploadProgress, setUploadProgress] = useState(''); - const [waitingForOcr, setWaitingForOcr] = useState(false); - const [ocrResults, setOcrResults] = useState(null); + const [ocrModalVisible, setOcrModalVisible] = useState(false); + const [ocrModalContent, setOcrModalContent] = useState(null); const eventSourceRef = useRef(null); const handleEventTypeChange = (value: string) => { setEventType(value); - setDocumentFiles({}); // Очищаем загруженные файлы при смене типа + setCurrentDocumentIndex(0); + setProcessedDocuments({}); + setCurrentFile(null); form.setFieldValue('eventType', value); }; // Получаем конфигурацию документов для выбранного типа события const currentDocuments = eventType ? DOCUMENT_CONFIGS[eventType] || [] : []; + const currentDocConfig = currentDocuments[currentDocumentIndex]; + + // Проверяем все ли обязательные документы обработаны + const allRequiredProcessed = currentDocuments + .filter(doc => doc.required) + .every(doc => processedDocuments[doc.field]); - const handleUploadChange = (field: string, { fileList: newFileList }: any) => { - setDocumentFiles(prev => ({ - ...prev, - [field]: newFileList - })); + // SSE подключение для получения результатов OCR + useEffect(() => { + const claimId = formData.claim_id; + if (!claimId || !uploading || !currentDocConfig) { + return; + } + + console.log('🔌 SSE: Открываю соединение для', currentDocConfig.file_type); + + const eventSource = new EventSource(`/events/${claimId}`); + eventSourceRef.current = eventSource; + + const expectedEventType = `${currentDocConfig.file_type}_processed`; + console.log('👀 Ожидаю event_type:', expectedEventType); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('📨 SSE event received:', data); + + if (data.event_type === expectedEventType) { + console.log('✅ Получил результат для документа:', currentDocConfig.name); + + // Сохраняем результат + setProcessedDocuments(prev => ({ + ...prev, + [currentDocConfig.field]: data.data?.output || data.data + })); + + // Показываем результат в модалке + setOcrModalContent({ + success: data.status === 'completed', + data: data.data?.output || data.data, + message: data.message, + documentName: currentDocConfig.name + }); + + setUploading(false); + eventSource.close(); + + addDebugEvent?.('ocr', 'success', `✅ ${currentDocConfig.name} обработан`, { + file_type: currentDocConfig.file_type, + data: data.data?.output || data.data + }); + } + } catch (error) { + console.error('SSE parse error:', error); + } + }; + + eventSource.onerror = (error) => { + console.error('❌ SSE connection error:', error); + + setOcrModalContent((prev) => { + if (prev && prev !== 'loading') { + console.log('✅ SSE закрыто после получения результата'); + return prev; + } + return { success: false, data: null, message: 'Ошибка подключения к серверу' }; + }); + + setUploading(false); + eventSource.close(); + }; + + eventSource.onopen = () => { + console.log('✅ SSE: Соединение открыто'); + }; + + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + }; + }, [formData.claim_id, uploading, currentDocConfig]); + + const handleFileSelect = (file: File) => { + setCurrentFile(file); + return false; // Предотвращаем автозагрузку }; - const handleNext = async () => { + const handleUploadAndProcess = async () => { + if (!currentFile || !currentDocConfig) { + message.error('Выберите файл'); + return; + } + try { - const values = await form.validateFields(); - - // Проверяем что все обязательные документы загружены - const missingDocs = currentDocuments.filter(doc => - doc.required && (!documentFiles[doc.field] || documentFiles[doc.field].length === 0) - ); - - if (missingDocs.length > 0) { - message.error(`Загрузите обязательные документы: ${missingDocs.map(d => d.name).join(', ')}`); - return; - } - - // Загружаем все документы в S3 через n8n setUploading(true); - setUploadProgress('📤 Загружаем документы...'); - + setOcrModalVisible(true); + setOcrModalContent('loading'); + const claimId = formData.claim_id; - const uploadedFiles: any[] = []; - - for (const docConfig of currentDocuments) { - const files = documentFiles[docConfig.field] || []; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (!file.originFileObj) continue; - - setUploadProgress(`📡 Загружаем: ${docConfig.name} (${i + 1}/${files.length})...`); - - const uploadFormData = new FormData(); - uploadFormData.append('claim_id', claimId); - uploadFormData.append('file_type', docConfig.file_type); // 🔑 Уникальный file_type для n8n - uploadFormData.append('filename', file.name); - uploadFormData.append('voucher', formData.voucher || ''); - uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown'); - uploadFormData.append('upload_timestamp', new Date().toISOString()); - uploadFormData.append('file', file.originFileObj); - - addDebugEvent?.('upload', 'pending', `📤 Загружаю документ: ${docConfig.name} (${docConfig.file_type})`, { - file_type: docConfig.file_type, - filename: file.name - }); - - const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', { - method: 'POST', - body: uploadFormData, - }); - - const uploadResult = await uploadResponse.json(); - const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult; - - if (resultData?.success) { - uploadedFiles.push({ - filename: file.name, - file_type: docConfig.file_type, - field: docConfig.field, - success: true - }); - - addDebugEvent?.('upload', 'success', `✅ Документ загружен: ${docConfig.name}`, { - file_type: docConfig.file_type, - filename: file.name - }); - } - } - } - - if (uploadedFiles.length > 0) { - setUploadProgress('🤖 AI анализирует документы...'); - - updateFormData({ - ...values, - uploadedDocuments: uploadedFiles - }); - - // TODO: Здесь будет ожидание SSE события с результатами OCR/AI - // Пока просто переходим дальше - setUploadProgress(''); - setUploading(false); - - message.success(`Загружено документов: ${uploadedFiles.length}. Переходим дальше...`); - onNext(); - } else { - message.error('Не удалось загрузить документы'); - setUploading(false); - setUploadProgress(''); - } - } catch (error) { - message.error('Заполните все обязательные поля'); + addDebugEvent?.('upload', 'pending', `📤 Загружаю: ${currentDocConfig.name}`, { + file_type: currentDocConfig.file_type, + filename: currentFile.name + }); + + const uploadFormData = new FormData(); + uploadFormData.append('claim_id', claimId); + uploadFormData.append('file_type', currentDocConfig.file_type); + uploadFormData.append('filename', currentFile.name); + uploadFormData.append('voucher', formData.voucher || ''); + uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown'); + uploadFormData.append('upload_timestamp', new Date().toISOString()); + uploadFormData.append('file', currentFile); + + const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', { + method: 'POST', + body: uploadFormData, + }); + + const uploadResult = await uploadResponse.json(); + console.log('📤 Файл загружен, ждём OCR результат...'); + + addDebugEvent?.('upload', 'success', `✅ Файл загружен, обрабатывается...`, { + file_type: currentDocConfig.file_type + }); + + // SSE обработчик получит результат и обновит состояние + + } catch (error: any) { + console.error('Ошибка загрузки:', error); + message.error('Ошибка загрузки файла'); setUploading(false); - setUploadProgress(''); + setOcrModalVisible(false); + + addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки: ${error.message}`); } }; + const handleContinueToNextDocument = () => { + setOcrModalVisible(false); + setCurrentFile(null); + setCurrentDocumentIndex(prev => prev + 1); + }; + + const handleSkipOptionalDocument = () => { + setCurrentFile(null); + setCurrentDocumentIndex(prev => prev + 1); + }; + + const handleFinishStep = () => { + updateFormData({ + eventType, + processedDocuments + }); + onNext(); + }; + + // Прогресс загрузки + const totalRequired = currentDocuments.filter(d => d.required).length; + const processedRequired = currentDocuments.filter(d => d.required && processedDocuments[d.field]).length; + const progressPercent = totalRequired > 0 ? Math.round((processedRequired / totalRequired) * 100) : 0; + return (
- {/* Показываем документы только после выбора типа события */} + {/* Прогресс обработки документов */} {eventType && currentDocuments.length > 0 && ( - -
-

- 💡 Просто загрузите документы — наш AI автоматически распознает все данные - (номера рейсов, даты, время, причины задержек) -

+ +
+ Прогресс обработки документов:
- - {currentDocuments.map((doc, index) => ( -
-
- - {doc.required ? '✅' : 'ℹ️'} {doc.name} - {doc.required && *} - -
- -
- 💡 {doc.description} -
- - handleUploadChange(doc.field, info)} - beforeUpload={(file) => { - const isLt15M = file.size / 1024 / 1024 < 15; - if (!isLt15M) { - message.error(`${file.name}: файл больше 15MB`); - return Upload.LIST_IGNORE; - } - - const currentFiles = documentFiles[doc.field] || []; - if (currentFiles.length >= doc.maxFiles) { - message.error(`Максимум ${doc.maxFiles} файл(ов) для этого документа`); - return Upload.LIST_IGNORE; - } - - const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf']; - const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i; - - if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) { - message.error(`${file.name}: неподдерживаемый формат`); - return Upload.LIST_IGNORE; - } - - return false; - }} - accept="image/*,.pdf,.heic,.heif,.webp" - multiple={doc.maxFiles > 1} - maxCount={doc.maxFiles} - showUploadList={{ - showPreviewIcon: true, - showRemoveIcon: true, - }} - > - - - -
- Загружено: {(documentFiles[doc.field] || []).length}/{doc.maxFiles} файл(ов) -
+ `${processedRequired}/${totalRequired} обязательных`} + /> + + {/* Список обработанных документов */} + {Object.keys(processedDocuments).length > 0 && ( +
+ {currentDocuments.map(doc => + processedDocuments[doc.field] ? ( +
+ {doc.name} - ✅ Обработан +
+ ) : null + )}
- ))} + )} )} - {/* Прогресс обработки */} - {uploading && uploadProgress && ( - } />} - style={{ marginBottom: 16, marginTop: 16 }} - /> + {/* Текущий документ для загрузки */} + {eventType && currentDocConfig && ( + +
+

+ 💡 {currentDocConfig.description} +

+ {currentDocConfig.required && ( +

+ ⚠️ Этот документ обязательный +

+ )} +
+ + setCurrentFile(null)} + > + + + +
+ + + {!currentDocConfig.required && ( + + )} +
+
)} + {/* Если все документы обработаны или текущий индекс вышел за пределы */} + {eventType && currentDocumentIndex >= currentDocuments.length && ( + +
+ +

✅ Все документы обработаны!

+

+ Обработано обязательных документов: {processedRequired}/{totalRequired} +

+
+
+ )} + + {/* Модальное окно обработки OCR */} + + {currentDocumentIndex < currentDocuments.length - 1 ? 'Продолжить к следующему документу →' : 'Завершить загрузку документов'} + + ] : [ + + ] + } + width={700} + centered + > + {ocrModalContent === 'loading' ? ( +
+ } /> +

⏳ Обрабатываем документ

+

📤 Загрузка в облако...

+

📝 OCR распознавание текста...

+

🤖 AI анализ документа...

+

✅ Извлечение данных...

+

+ Это может занять 20-30 секунд. Пожалуйста, подождите... +

+
+ ) : ocrModalContent ? ( +
+

+ {ocrModalContent.success ? '✅ Результат обработки' : '❌ Ошибка обработки'} +

+ {ocrModalContent.success ? ( +
+

+ 📋 {ocrModalContent.documentName} +

+
+

+ ✅ Документ успешно распознан +

+

+ Данные извлечены и сохранены +

+
+

Извлечённые данные:

+
+                  {JSON.stringify(ocrModalContent.data, null, 2)}
+                
+
+ ) : ( +
+

{ocrModalContent.message || 'Документ не распознан'}

+

Детали:

+
+                  {JSON.stringify(ocrModalContent.data, null, 2)}
+                
+
+ )} +
+ ) : null} +
+ + {/* Кнопки навигации */}
@@ -444,9 +615,12 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev, + + +
+
+ + )} + + ); +} diff --git a/monitor_redis_direct.py b/monitor_redis_direct.py index 2c07121..573e291 100755 --- a/monitor_redis_direct.py +++ b/monitor_redis_direct.py @@ -67,3 +67,4 @@ finally: r.close() + diff --git a/test_redis_publish_direct.py b/test_redis_publish_direct.py index 7cc2b6b..7527339 100755 --- a/test_redis_publish_direct.py +++ b/test_redis_publish_direct.py @@ -72,3 +72,4 @@ print("\n" + "=" * 60) print(f"⏰ Завершено: {datetime.now().strftime('%H:%M:%S')}") +