diff --git a/SESSION_LOG_2025-11-14.md b/SESSION_LOG_2025-11-14.md
index 2dca859..ece1de2 100644
--- a/SESSION_LOG_2025-11-14.md
+++ b/SESSION_LOG_2025-11-14.md
@@ -188,3 +188,38 @@ Unit-тестов почти нет, поэтому проверяем сцен
Upd 14.11.2025, автор: GPT-5.1 Codex.
+---
+
+## 11. Ticket Form — доработки 15.11.2025
+
+### 11.1. SSE + Wizard Plan
+- Новая стадия формы `StepWizardPlan` между описанием и выбором услуги:
+ - подключается к `/events/{claim_id}`, выбирает payload даже если `wizard_plan` лежит в `data`, `redis_value` или `event`.
+ - отображает иллюстрацию/спиннер, пишет события в DebugPanel.
+ - при Success сохраняет `wizardPlan`, `answers_prefill`, `coverage_report`, `wizardPrefillMap` в состоянии.
+- На случай отладки добавлен чекбокс в `StepDescription`: «Использовать сохранённые рекомендации (DEV)».
+ - По умолчанию включен; берёт мок `wizardPlanSample` (лежит в `frontend/src/mocks`), пропускает вызов AI и блокирует textarea.
+ - При снятом чекбоксе описание снова обязательное и реально отправляется на `/api/v1/claims/description`.
+
+### 11.2. Динамическая анкета
+- `StepWizardPlan` строит форму исключительно из `wizard_plan.questions`: текст, textarea, радио.
+- Въелся прогресс-бар с подсчётом обязательных полей (done / total).
+- `wizardPlanStatus` принимает значения `pending | ready | answered`, чтобы следующие шаги понимали, прошёл ли пользователь анкету.
+
+### 11.3. Документы прямо в анкете
+- Под вопросами «Есть ли документы?» и «Есть ли переписка?» появляются мультилоадеры:
+ - группы файлов с описанием, категорией (select), списком допустимых форматов, лимитом 20 МБ.
+ - для каждого документа из `plan.documents` можно создать несколько блоков; храним их в `wizardUploads.documents`.
+ - кастомная секция «Дополнительные документы» позволяет добавить произвольные блоки (категория + описание + файлы), лежат в `wizardUploads.custom`.
+- Валидация: если ответ «Да», но файлы не добавлены или нет описаний — показываем ошибку, не пускаем дальше.
+- До отправки (переход на следующий шаг) сохраняем `wizardUploads` для дальнейшего api/n8n.
+
+### 11.4. Прочее
+- `ClaimForm` логи перенесены в `useEffect`, чтобы StrictMode не писал дубль.
+- Кнопка «Обновить рекомендации» сбрасывает `wizardPlan` и пересоздаёт SSE.
+- Docker: каждый раз после правок фронт пересобирали `docker compose build ticket_form_frontend && docker compose up -d ticket_form_frontend`.
+
+### TODO (перенесено в бэклог)
+- На backend обезопасить хранение `wizard_plan` в Redis (по ключу `wizard_plan:{claim_id}`) и отдавать кеш при DEV-галке.
+- Передать `wizardUploads` в следующий шаг & далее в n8n, чтобы фактически загрузить файлы/метаданные.
+
diff --git a/frontend/src/assets/ai-working.svg b/frontend/src/assets/ai-working.svg
new file mode 100644
index 0000000..5bb92c0
--- /dev/null
+++ b/frontend/src/assets/ai-working.svg
@@ -0,0 +1,51 @@
+
+
diff --git a/frontend/src/components/form/StepDescription.tsx b/frontend/src/components/form/StepDescription.tsx
index 48ac521..322e76d 100644
--- a/frontend/src/components/form/StepDescription.tsx
+++ b/frontend/src/components/form/StepDescription.tsx
@@ -1,5 +1,6 @@
-import { Form, Input, Button, Typography, message } from 'antd';
+import { Form, Input, Button, Typography, message, Checkbox } from 'antd';
import { useEffect, useState } from 'react';
+import wizardPlanSample from '../../mocks/wizardPlanSample';
const { TextArea } = Input;
const { Paragraph } = Typography;
@@ -19,6 +20,19 @@ export default function StepDescription({
}: Props) {
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
+ const [useMockWizard, setUseMockWizard] = useState(true);
+
+ const buildPrefillMap = (prefill?: Array<{ name: string; value: any }>) => {
+ if (!prefill) {
+ return {};
+ }
+ return prefill.reduce>((acc, item) => {
+ if (item?.name) {
+ acc[item.name] = item.value;
+ }
+ return acc;
+ }, {});
+ };
useEffect(() => {
form.setFieldsValue({
@@ -28,15 +42,44 @@ export default function StepDescription({
const handleContinue = async () => {
try {
- const values = await form.validateFields();
+ let problemDescription = form.getFieldValue('problemDescription');
+ if (!useMockWizard) {
+ const values = await form.validateFields();
+ problemDescription = values.problemDescription;
+ }
+ const safeDescription = problemDescription || '';
if (!formData.session_id) {
message.error('Не найден session_id. Попробуйте обновить страницу.');
return;
}
+ if (!formData.claim_id) {
+ message.error('Не удалось определить номер обращения. Вернитесь на шаг с телефоном.');
+ return;
+ }
setSubmitting(true);
+ if (useMockWizard && wizardPlanSample?.wizard_plan) {
+ const mockPrefill = buildPrefillMap(wizardPlanSample.answers_prefill);
+ const mockClaimId = wizardPlanSample.claim_id || formData.claim_id;
+
+ updateFormData({
+ problemDescription: safeDescription,
+ claim_id: mockClaimId,
+ wizardPlan: wizardPlanSample.wizard_plan,
+ wizardPlanStatus: 'ready',
+ wizardPrefill: mockPrefill,
+ wizardPrefillArray: wizardPlanSample.answers_prefill,
+ wizardCoverageReport: wizardPlanSample.coverage_report,
+ wizardAnswers: undefined,
+ });
+
+ message.success('Загружены сохранённые рекомендации (DEV).');
+ onNext();
+ return;
+ }
+
const response = await fetch('/api/v1/claims/description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -45,7 +88,7 @@ export default function StepDescription({
claim_id: formData.claim_id,
phone: formData.phone,
email: formData.email,
- problem_description: values.problemDescription,
+ problem_description: safeDescription,
}),
});
@@ -53,8 +96,15 @@ export default function StepDescription({
throw new Error(`Ошибка API: ${response.status}`);
}
- message.success('Описание отправлено, продолжаем заполнение');
- updateFormData(values);
+ message.success('Описание отправлено, подбираем рекомендации...');
+ updateFormData({
+ problemDescription: safeDescription,
+ wizardPlan: undefined,
+ wizardPlanStatus: 'pending',
+ wizardAnswers: undefined,
+ wizardPrefill: undefined,
+ wizardPrefillArray: undefined,
+ });
onNext();
} catch (error) {
console.error(error);
@@ -93,22 +143,54 @@ export default function StepDescription({
label="Описание ситуации"
name="problemDescription"
rules={[
- { required: true, message: 'Поле обязательно' },
{
- min: 20,
- message: 'Опишите, пожалуйста, минимум в пару предложений',
+ validator: (_, value) => {
+ if (useMockWizard) {
+ return Promise.resolve();
+ }
+ if (!value) {
+ return Promise.reject(new Error('Поле обязательно'));
+ }
+ if (value.length < 20) {
+ return Promise.reject(
+ new Error('Опишите, пожалуйста, минимум в пару предложений')
+ );
+ }
+ return Promise.resolve();
+ },
},
]}
>
+
+ setUseMockWizard(e.target.checked)}
+ >
+ Использовать сохранённые рекомендации (DEV)
+
+
+ Если включено, план вопросов берётся из локального файла и не запускает модель.
+
+
+
+ Мы собираем рекомендации для вашего случая
+
+ Наш AI-ассистент анализирует ваше описание и подбирает вопросы и список документов,
+ которые помогут быстро решить проблему.
+
+
+
+
+
+
+ Подождите несколько секунд…
+
+ {connectionError && (
+
+ {connectionError}
+
+
+
+
+ )}
+
+ )}
+
+ {!isWaiting && plan && (
+
+
+ План действий
+
+
+ {plan.user_text || 'Ответьте на вопросы и подготовьте документы, чтобы мы могли продолжить.'}
+
+
+ {documents.length > 0 && (
+
+
+ {documents.map((doc: any) => (
+