feat: Split-screen с Debug панелью в реальном времени!

Новый UI:
 Split-screen layout:
   - Слева (60%): форма заявки
   - Справа (40%): Debug Console в реальном времени

Компонент DebugPanel.tsx:
 Темная тема (VS Code style)
 Timeline с событиями
 Real-time обновления
 Показывает:
   - Form Data (JSON в реальном времени)
   - Events Log с иконками и цветами
   - Детали каждого события

События которые отображаются:
1. policy_check:
   -  Полис найден в MySQL БД
   - ⚠️ Полис не найден
   - Показывает: voucher, found status

2. upload:
   - 📤 Загружаю X файлов в S3
   -  Загружено в S3: X/Y
   - Показывает: file_id, size, S3 URL

3. ocr:
   - 🔍 Запущен OCR
   - 📄 OCR завершен: XXX символов
   - Показывает: текст preview

4. ai_analysis:
   - 🤖 AI: policy/garbage, confidence: 95%
   - 🗑️ ШЛЯПА DETECTED! (пользователю не говорим)
   - Показывает: document_type, is_valid, confidence, extracted_data

5. sms:
   - 📱 Отправляю SMS
   -  SMS отправлен (DEBUG mode)
   - 🔐 Проверяю код
   -  Телефон подтвержден
   - Показывает: phone, debug_code

UX:
- Sticky panel (прилипает при скролле)
- Monospace шрифт для данных
- Цветовая кодировка статусов
- JSON форматирование

Layout:
- Row + Col от Ant Design
- Responsive: mobile = 1 column, desktop = split

Теперь видно ВСЁ что происходит в реальном времени! 🔍
This commit is contained in:
AI Assistant
2025-10-24 22:13:52 +03:00
parent 20bad53008
commit 720d4ebdd9
4 changed files with 375 additions and 35 deletions

View File

@@ -0,0 +1,236 @@
import { Card, Timeline, Tag, Descriptions } from 'antd';
import { CheckCircleOutlined, LoadingOutlined, ExclamationCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
interface DebugEvent {
timestamp: string;
type: 'policy_check' | 'ocr' | 'ai_analysis' | 'upload' | 'error';
status: 'pending' | 'success' | 'warning' | 'error';
message: string;
data?: any;
}
interface Props {
events: DebugEvent[];
formData: any;
}
export default function DebugPanel({ events, formData }: Props) {
const getIcon = (status: string) => {
switch (status) {
case 'success': return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
case 'pending': return <LoadingOutlined style={{ color: '#1890ff' }} />;
case 'warning': return <ExclamationCircleOutlined style={{ color: '#faad14' }} />;
case 'error': return <CloseCircleOutlined style={{ color: '#f5222d' }} />;
default: return <CheckCircleOutlined />;
}
};
const getColor = (status: string) => {
switch (status) {
case 'success': return 'success';
case 'pending': return 'processing';
case 'warning': return 'warning';
case 'error': return 'error';
default: return 'default';
}
};
return (
<div style={{
position: 'sticky',
top: 20,
height: 'calc(100vh - 40px)',
overflowY: 'auto'
}}>
<Card
title="🔧 Debug Console"
size="small"
style={{
background: '#1e1e1e',
color: '#d4d4d4',
border: '1px solid #333'
}}
headStyle={{
background: '#252526',
color: '#fff',
borderBottom: '1px solid #333'
}}
bodyStyle={{ padding: 12 }}
>
{/* Текущие данные формы */}
<div style={{ marginBottom: 16, padding: 12, background: '#2d2d30', borderRadius: 4 }}>
<div style={{ fontSize: 12, color: '#9cdcfe', marginBottom: 8, fontFamily: 'monospace' }}>
<strong>Form Data:</strong>
</div>
<pre style={{
margin: 0,
fontSize: 11,
color: '#ce9178',
fontFamily: 'Consolas, monospace',
maxHeight: 150,
overflow: 'auto'
}}>
{JSON.stringify(formData, null, 2)}
</pre>
</div>
{/* События */}
<div style={{ marginBottom: 8, fontSize: 12, color: '#9cdcfe', fontFamily: 'monospace' }}>
<strong>Events Log:</strong>
</div>
<Timeline style={{ marginTop: 16 }}>
{events.length === 0 && (
<Timeline.Item color="gray">
<span style={{ color: '#888', fontSize: 12 }}>Нет событий...</span>
</Timeline.Item>
)}
{events.map((event, index) => (
<Timeline.Item
key={index}
dot={getIcon(event.status)}
>
<div style={{ fontSize: 11, fontFamily: 'monospace' }}>
<div style={{ color: '#888', marginBottom: 4 }}>
{event.timestamp}
</div>
<div style={{ marginBottom: 8 }}>
<Tag color={getColor(event.status)} style={{ fontSize: 11 }}>
{event.type.toUpperCase()}
</Tag>
<span style={{ color: '#d4d4d4' }}>{event.message}</span>
</div>
{event.data && (
<div style={{
marginTop: 8,
padding: 8,
background: '#1e1e1e',
borderRadius: 4,
border: '1px solid #333'
}}>
{event.type === 'policy_check' && event.data.found !== undefined && (
<Descriptions size="small" column={1} bordered>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Found</span>}
labelStyle={{ background: '#252526', color: '#9cdcfe' }}
contentStyle={{ background: '#1e1e1e', color: event.data.found ? '#4ec9b0' : '#f48771' }}
>
{event.data.found ? 'TRUE' : 'FALSE'}
</Descriptions.Item>
{event.data.holder_name && (
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Holder</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#ce9178' }}
>
{event.data.holder_name}
</Descriptions.Item>
)}
</Descriptions>
)}
{event.type === 'ocr' && (
<pre style={{
margin: 0,
fontSize: 10,
color: '#ce9178',
maxHeight: 100,
overflow: 'auto',
whiteSpace: 'pre-wrap'
}}>
{event.data.text?.substring(0, 300)}...
</pre>
)}
{event.type === 'ai_analysis' && (
<Descriptions size="small" column={1} bordered>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Type</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#4ec9b0' }}
>
{event.data.document_type}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Valid</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: event.data.is_valid ? '#4ec9b0' : '#f48771' }}
>
{event.data.is_valid ? 'TRUE' : 'FALSE'}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Confidence</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
>
{(event.data.confidence * 100).toFixed(0)}%
</Descriptions.Item>
{event.data.extracted_data && Object.keys(event.data.extracted_data).length > 0 && (
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Extracted</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e' }}
>
<pre style={{
margin: 0,
fontSize: 10,
color: '#ce9178',
whiteSpace: 'pre-wrap'
}}>
{JSON.stringify(event.data.extracted_data, null, 2)}
</pre>
</Descriptions.Item>
)}
</Descriptions>
)}
{event.type === 'upload' && (
<Descriptions size="small" column={1} bordered>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>File ID</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#569cd6', fontSize: 10 }}
>
{event.data.file_id}
</Descriptions.Item>
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>Size</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', color: '#dcdcaa' }}
>
{(event.data.size / 1024).toFixed(1)} KB
</Descriptions.Item>
{event.data.url && (
<Descriptions.Item
label={<span style={{ color: '#9cdcfe' }}>S3 URL</span>}
labelStyle={{ background: '#252526' }}
contentStyle={{ background: '#1e1e1e', fontSize: 9 }}
>
<a href={event.data.url} target="_blank" rel="noopener noreferrer" style={{ color: '#4ec9b0' }}>
{event.data.url.substring(0, 50)}...
</a>
</Descriptions.Item>
)}
</Descriptions>
)}
</div>
)}
</div>
</Timeline.Item>
))}
</Timeline>
{events.length > 0 && (
<div style={{ marginTop: 16, padding: 8, background: '#2d2d30', borderRadius: 4, textAlign: 'center' }}>
<span style={{ fontSize: 11, color: '#888' }}>
Total events: {events.length}
</span>
</div>
)}
</Card>
</div>
);
}

View File

@@ -7,6 +7,7 @@ interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
// Расширенная функция автозамены кириллицы на латиницу
@@ -49,7 +50,7 @@ const formatVoucher = (value: string): string => {
}
};
export default function Step1Policy({ formData, updateFormData, onNext }: Props) {
export default function Step1Policy({ formData, updateFormData, onNext, addDebugEvent }: Props) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [policyNotFound, setPolicyNotFound] = useState(false);
@@ -77,6 +78,8 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
setLoading(true);
setPolicyNotFound(false);
addDebugEvent?.('policy_check', 'pending', `Проверяю полис: ${values.voucher}`, { voucher: values.voucher });
// Проверка полиса через API
const response = await fetch('http://147.45.146.17:8100/api/v1/policy/check', {
method: 'POST',
@@ -92,15 +95,24 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
if (response.ok) {
if (result.found) {
// Полис найден - переходим дальше
addDebugEvent?.('policy_check', 'success', `✅ Полис найден в MySQL БД (33,963 полисов)`, {
found: true,
voucher: values.voucher
});
message.success('Полис найден в базе данных');
updateFormData(values);
onNext();
} else {
// Полис НЕ найден - показываем загрузку скана
addDebugEvent?.('policy_check', 'warning', `⚠️ Полис не найден → требуется загрузка скана`, {
found: false,
voucher: values.voucher
});
message.warning('Полис не найден в базе. Загрузите скан полиса');
setPolicyNotFound(true);
}
} else {
addDebugEvent?.('policy_check', 'error', `❌ Ошибка API: ${result.detail}`, { error: result.detail });
message.error(result.detail || 'Ошибка проверки полиса');
}
} catch (error: any) {
@@ -133,6 +145,10 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
setUploading(true);
const values = await form.validateFields(['voucher']);
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} файл(ов) в S3...`, {
count: fileList.length
});
// Загружаем файлы в S3 с OCR проверкой
const formData = new FormData();
fileList.forEach((file: any) => {
@@ -150,19 +166,59 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props)
const uploadResult = await uploadResponse.json();
if (uploadResult.success) {
// TODO: OCR проверка что это полис, а не шляпа
// Если шляпа - помечаем себе, пользователю не говорим
addDebugEvent?.('upload', 'success', `✅ Загружено в S3: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
uploaded_count: uploadResult.uploaded_count,
files: uploadResult.files
});
// Проверяем OCR результаты
if (uploadResult.files && uploadResult.files.length > 0) {
const firstFile = uploadResult.files[0];
addDebugEvent?.('ocr', 'pending', `🔍 Запущен OCR для: ${firstFile.filename}`, {
file_id: firstFile.file_id,
filename: firstFile.filename
});
// Если есть OCR результат
if (firstFile.ocr_result) {
const ocr = firstFile.ocr_result;
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
}
);
}
}
}
updateFormData({
...values,
policyScanUploaded: true,
policyScanFiles: uploadResult.files,
policyValidationWarning: '' // TODO: OCR validation
policyValidationWarning: '' // Silent validation
});
message.success(`Загружено файлов: ${uploadResult.uploaded_count}`);
onNext();
} else {
addDebugEvent?.('upload', 'error', `❌ Ошибка загрузки файлов`, { error: 'Upload failed' });
message.error('Ошибка загрузки файлов');
}
} catch (error) {

View File

@@ -11,6 +11,7 @@ interface Props {
onSubmit: () => void;
isPhoneVerified: boolean;
setIsPhoneVerified: (verified: boolean) => void;
addDebugEvent?: (type: string, status: string, message: string, data?: any) => void;
}
export default function Step3Payment({
@@ -19,7 +20,8 @@ export default function Step3Payment({
onPrev,
onSubmit,
isPhoneVerified,
setIsPhoneVerified
setIsPhoneVerified,
addDebugEvent
}: Props) {
const [form] = Form.useForm();
const [codeSent, setCodeSent] = useState(false);
@@ -36,6 +38,9 @@ export default function Step3Payment({
}
setLoading(true);
addDebugEvent?.('sms', 'pending', `📱 Отправляю SMS на ${phone}...`, { phone });
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -45,12 +50,18 @@ export default function Step3Payment({
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ SMS отправлен (DEBUG mode)`, {
phone,
debug_code: result.debug_code,
message: result.message
});
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
if (result.debug_code) {
message.info(`DEBUG: Код ${result.debug_code}`);
}
} else {
addDebugEvent?.('sms', 'error', `❌ Ошибка SMS: ${result.detail}`, { error: result.detail });
message.error(result.detail || 'Ошибка отправки кода');
}
} catch (error) {
@@ -71,6 +82,9 @@ export default function Step3Payment({
}
setVerifyLoading(true);
addDebugEvent?.('sms', 'pending', `🔐 Проверяю SMS код...`, { phone, code });
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -80,9 +94,18 @@ export default function Step3Payment({
const result = await response.json();
if (response.ok) {
addDebugEvent?.('sms', 'success', `✅ Телефон подтвержден успешно`, {
phone,
verified: true
});
message.success('Телефон подтвержден!');
setIsPhoneVerified(true);
} else {
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, {
phone,
code,
error: result.detail
});
message.error(result.detail || 'Неверный код');
}
} catch (error) {

View File

@@ -1,8 +1,9 @@
import { useState } from 'react';
import { Steps, Card, message } from 'antd';
import { Steps, Card, message, Row, Col } from 'antd';
import Step1Policy from '../components/form/Step1Policy';
import Step2Details from '../components/form/Step2Details';
import Step3Payment from '../components/form/Step3Payment';
import DebugPanel from '../components/DebugPanel';
import './ClaimForm.css';
const { Step } = Steps;
@@ -35,6 +36,18 @@ export default function ClaimForm() {
paymentMethod: 'sbp',
});
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
const [debugEvents, setDebugEvents] = useState<any[]>([]);
const addDebugEvent = (type: string, status: string, message: string, data?: any) => {
const event = {
timestamp: new Date().toLocaleTimeString('ru-RU'),
type,
status,
message,
data
};
setDebugEvents(prev => [event, ...prev]);
};
const updateFormData = (data: Partial<FormData>) => {
setFormData({ ...formData, ...data });
@@ -100,6 +113,7 @@ export default function ClaimForm() {
formData={formData}
updateFormData={updateFormData}
onNext={nextStep}
addDebugEvent={addDebugEvent}
/>
),
},
@@ -124,6 +138,7 @@ export default function ClaimForm() {
onSubmit={handleSubmit}
isPhoneVerified={isPhoneVerified}
setIsPhoneVerified={setIsPhoneVerified}
addDebugEvent={addDebugEvent}
/>
),
},
@@ -142,35 +157,45 @@ export default function ClaimForm() {
};
return (
<div className="claim-form-container">
<Card
title="Подать заявку на выплату"
className="claim-form-card"
extra={
currentStep > 0 && (
<button
onClick={handleReset}
style={{
padding: '4px 12px',
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
🔄 Начать заново
</button>
)
}
>
<Steps current={currentStep} className="steps">
{steps.map((item) => (
<Step key={item.title} title={item.title} />
))}
</Steps>
<div className="steps-content">{steps[currentStep].content}</div>
</Card>
<div className="claim-form-container" style={{ padding: '20px', background: '#f0f2f5' }}>
<Row gutter={16}>
{/* Левая часть - Форма */}
<Col xs={24} lg={14}>
<Card
title="Подать заявку на выплату"
className="claim-form-card"
extra={
currentStep > 0 && (
<button
onClick={handleReset}
style={{
padding: '4px 12px',
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
🔄 Начать заново
</button>
)
}
>
<Steps current={currentStep} className="steps">
{steps.map((item) => (
<Step key={item.title} title={item.title} />
))}
</Steps>
<div className="steps-content">{steps[currentStep].content}</div>
</Card>
</Col>
{/* Правая часть - Debug консоль */}
<Col xs={24} lg={10}>
<DebugPanel events={debugEvents} formData={formData} />
</Col>
</Row>
</div>
);
}