fix: 3 критических исправления - OCR прогресс, условные поля, убран некорректный статус

1.  OCR Progress Bar:
   - Добавлен polling OCR результатов каждые 3 сек
   - Визуальный индикатор: 🔍 Обработка OCR... (1/10)
   - Progress bar с анимацией
   - Статусы: 🔄 Запуск → 🔍 Обработка →  Завершен
   - Gemini Vision результаты в Debug панели

2.  Убран некорректный 'Полис найден':
   - Было: показывался сразу после загрузки файла
   - Проблема: OCR еще не закончился, может быть шляпа
   - Решение: убрана зеленая плашка с Step2
   - Статус полиса только после реальной проверки

3.  Условные поля для стыковочного рейса:
   - Если выбран 'miss_connection' → показываются 4 доп поля:
     • Номер рейса прибытия
     • Дата рейса прибытия
     • Номер рейса отправления
     • Дата рейса отправления
   - Если выбран 'cancel_flight' → доп поле:
     • Подтверждение отмены от АК
   - Для обычных рейсов: только номер рейса

Frontend изменения:
- Step1Policy: OCR polling, progress bar
- Step2Details: условная логика полей (как в erv_ticket)
- useState для eventType
- handleEventTypeChange для динамики

Теперь:
 Видно прогресс OCR
 Видно результаты Gemini Vision
 Условные поля работают
 Нет ложных статусов
This commit is contained in:
AI Assistant
2025-10-25 10:12:41 +03:00
parent ddca18716d
commit a26cb772f5
2 changed files with 202 additions and 52 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Form, Input, Button, message, Upload } from 'antd';
import { FileProtectOutlined, UploadOutlined } from '@ant-design/icons';
import { Form, Input, Button, message, Upload, Progress } from 'antd';
import { FileProtectOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
interface Props {
@@ -56,6 +56,7 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
const [policyNotFound, setPolicyNotFound] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [ocrProgress, setOcrProgress] = useState<string>('');
// Обработчик изменения поля полиса с автозаменой и маской
const handleVoucherChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -130,6 +131,60 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
setFileList(newFileList);
};
// Polling для получения OCR результатов
const pollOcrResults = async (fileIds: string[]) => {
if (fileIds.length === 0) return;
const maxAttempts = 10;
const interval = 3000; // 3 секунды
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, interval));
setOcrProgress(`🔍 Обработка OCR... (${attempt + 1}/${maxAttempts})`);
for (const fileId of fileIds) {
try {
const response = await fetch(`http://147.45.146.17:8100/api/v1/upload/ocr-result/${fileId}`);
const result = await response.json();
if (result.found && result.ocr_result) {
const ocr = result.ocr_result;
addDebugEvent?.('ocr', 'success', `📄 OCR завершен: ${ocr.ocr_text?.length || 0} символов`, {
text: ocr.ocr_text?.substring(0, 300)
});
if (ocr.ai_analysis || ocr.document_type) {
const isGarbage = ocr.document_type === 'garbage';
addDebugEvent?.(
'ai_analysis',
isGarbage ? 'warning' : 'success',
isGarbage
? `🗑️ ШЛЯПА DETECTED! (пользователю не говорим)`
: `🤖 Gemini Vision: ${ocr.document_type}, confidence: ${(ocr.confidence * 100).toFixed(0)}%`,
{
document_type: ocr.document_type,
is_valid: ocr.is_valid,
confidence: ocr.confidence,
extracted_data: ocr.extracted_data
}
);
setOcrProgress(`✅ OCR завершен: ${ocr.document_type}`);
return; // Готово
}
}
} catch (error) {
console.error('OCR polling error:', error);
}
}
}
setOcrProgress('⏱️ OCR обрабатывается в фоне...');
};
const handleSubmitWithScan = async () => {
if (fileList.length === 0) {
message.error('Загрузите скан полиса');
@@ -173,6 +228,10 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
// Проверяем OCR результаты
if (uploadResult.files && uploadResult.files.length > 0) {
const fileIds = uploadResult.files
.filter((f: any) => f.file_id)
.map((f: any) => f.file_id);
const firstFile = uploadResult.files[0];
addDebugEvent?.('ocr', 'pending', `🔍 Запущен OCR для: ${firstFile.filename}`, {
@@ -180,32 +239,10 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
filename: firstFile.filename
});
// Если есть OCR результат
if (firstFile.ocr_result) {
const ocr = firstFile.ocr_result;
setOcrProgress('🔄 Запуск OCR...');
addDebugEvent?.('ocr', 'success', `📄 OCR завершен: ${ocr.ocr_text?.length || 0} символов`, {
text: ocr.ocr_text?.substring(0, 300)
});
if (ocr.ai_analysis) {
const isGarbage = ocr.document_type === 'garbage';
addDebugEvent?.(
'ai_analysis',
isGarbage ? 'warning' : 'success',
isGarbage
? `🗑️ ШЛЯПА DETECTED! (пользователю не говорим)`
: `🤖 AI: ${ocr.document_type}, confidence: ${(ocr.confidence * 100).toFixed(0)}%`,
{
document_type: ocr.document_type,
is_valid: ocr.is_valid,
confidence: ocr.confidence,
extracted_data: ocr.extracted_data
}
);
}
}
// Запускаем polling в фоне (не блокируем переход)
pollOcrResults(fileIds);
}
updateFormData({
@@ -327,6 +364,31 @@ export default function Step1Policy({ formData, updateFormData, onNext, addDebug
</div>
</Form.Item>
{/* OCR Progress */}
{ocrProgress && (
<div style={{
padding: 16,
background: '#f0f9ff',
border: '1px solid #91d5ff',
borderRadius: 8,
marginBottom: 16
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
{ocrProgress.includes('🔍') || ocrProgress.includes('🔄') ? (
<LoadingOutlined style={{ fontSize: 16, color: '#1890ff' }} />
) : null}
<span style={{ fontSize: 13, fontWeight: 500 }}>{ocrProgress}</span>
</div>
{ocrProgress.includes('Обработка') && (
<Progress
percent={Math.min(((ocrProgress.match(/(\d+)\/\d+/)?.[1] || 0) as any) * 10, 90)}
status="active"
showInfo={false}
/>
)}
</div>
)}
<Form.Item>
<div style={{ display: 'flex', gap: 8 }}>
<Button

View File

@@ -87,6 +87,17 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
setFileList(newFileList);
};
const [eventType, setEventType] = useState(formData.eventType || '');
const handleEventTypeChange = (value: string) => {
setEventType(value);
form.setFieldValue('eventType', value);
};
// Проверяем нужны ли дополнительные поля для стыковочного рейса
const showConnectionFields = eventType === 'miss_connection';
const showCancelFlightDocs = eventType === 'cancel_flight';
return (
<Form
form={form}
@@ -94,21 +105,6 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
initialValues={formData}
style={{ marginTop: 24 }}
>
{/* Индикатор что полис найден */}
{formData.voucher && (
<div style={{
padding: 12,
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: 8,
marginBottom: 24,
color: '#52c41a',
fontWeight: 500
}}>
Полис найден
</div>
)}
<Form.Item
label="Выберите тип события"
name="eventType"
@@ -117,6 +113,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
<Select
placeholder="Выберите тип события"
size="large"
onChange={handleEventTypeChange}
>
{EVENT_TYPES.map(type => (
<Option key={type.value} value={type.value}>
@@ -140,6 +137,68 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
/>
</Form.Item>
{/* Для стыковочного рейса - номер рейса прибытия */}
{showConnectionFields && (
<Form.Item
label="Укажите номер рейса прибытия"
name="arrivalFlightNumber"
rules={[{ required: true, message: 'Введите номер рейса прибытия' }]}
>
<Input
placeholder="Введите номер"
size="large"
/>
</Form.Item>
)}
{showConnectionFields && (
<Form.Item
label="Дата рейса прибытия"
name="arrivalFlightDate"
rules={[{ required: true, message: 'Укажите дату прибытия' }]}
>
<DatePicker
placeholder="Выберите дату"
size="large"
style={{ width: '100%' }}
format="DD.MM.YYYY"
disabledDate={(current) => current && current > dayjs().endOf('day')}
/>
</Form.Item>
)}
{/* Для стыковочного рейса - номер рейса отправления */}
{showConnectionFields && (
<Form.Item
label="Укажите номер рейса отправления"
name="departureFlightNumber"
rules={[{ required: true, message: 'Введите номер рейса отправления' }]}
>
<Input
placeholder="Введите номер рейса отправления"
size="large"
/>
</Form.Item>
)}
{showConnectionFields && (
<Form.Item
label="Дата рейса отправления"
name="departureFlightDate"
rules={[{ required: true, message: 'Укажите дату отправления' }]}
>
<DatePicker
placeholder="Выберите дату"
size="large"
style={{ width: '100%' }}
format="DD.MM.YYYY"
disabledDate={(current) => current && current > dayjs().endOf('day')}
/>
</Form.Item>
)}
{/* Для обычных рейсов */}
{!showConnectionFields && (
<Form.Item
label="Номер рейса/поезда/парома"
name="transportNumber"
@@ -150,6 +209,35 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
size="large"
/>
</Form.Item>
)}
{/* Дополнительные документы для отмены рейса */}
{showCancelFlightDocs && (
<Form.Item
label="Подтверждение уведомления об отмене рейса от АК"
name="cancelConfirmation"
tooltip="Уведомление от авиакомпании об отмене"
>
<Upload
listType="picture"
beforeUpload={(file) => {
const isLt15M = file.size / 1024 / 1024 < 15;
if (!isLt15M) {
message.error(`${file.name}: файл больше 15MB`);
return Upload.LIST_IGNORE;
}
return false;
}}
accept="image/*,.pdf,.heic,.heif"
multiple
maxCount={5}
>
<Button icon={<UploadOutlined />} size="large" block>
Загрузить подтверждение отмены
</Button>
</Upload>
</Form.Item>
)}
<Form.Item
label="Подтверждающие документы"