Files
aiform_dev/docs/CODE1_FIXED_CODE.js
AI Assistant 0978e485dc feat: Add claim plan confirmation flow via Redis SSE
Problem:
- After wizard form submission, need to wait for claim data from n8n
- Claim data comes via Redis channel claim:plan:{session_token}
- Need to display confirmation form with claim data

Solution:
1. Backend: Added SSE endpoint /api/v1/claim-plan/{session_token}
   - Subscribes to Redis channel claim:plan:{session_token}
   - Streams claim data from n8n to frontend
   - Handles timeouts and errors gracefully

2. Frontend: Added subscription to claim:plan channel
   - StepWizardPlan: After form submission, subscribes to SSE
   - Waits for claim_plan_ready event
   - Shows loading message while waiting
   - On success: saves claimPlanData and shows confirmation step

3. New component: StepClaimConfirmation
   - Displays claim confirmation form in iframe
   - Receives claimPlanData from parent
   - Generates HTML form (placeholder - should call n8n for real HTML)
   - Handles confirmation/cancellation via postMessage

4. ClaimForm: Added conditional step for confirmation
   - Shows StepClaimConfirmation when showClaimConfirmation=true
   - Step appears after StepWizardPlan
   - Only visible when claimPlanData is available

Flow:
1. User fills wizard form → submits
2. Form data sent to n8n via /api/v1/claims/wizard
3. Frontend subscribes to SSE /api/v1/claim-plan/{session_token}
4. n8n processes data → publishes to Redis claim:plan:{session_token}
5. Backend receives → streams to frontend via SSE
6. Frontend receives → shows StepClaimConfirmation
7. User confirms → proceeds to next step

Files:
- backend/app/api/events.py: Added stream_claim_plan endpoint
- frontend/src/components/form/StepWizardPlan.tsx: Added subscribeToClaimPlan
- frontend/src/components/form/StepClaimConfirmation.tsx: New component
- frontend/src/pages/ClaimForm.tsx: Added confirmation step to steps array
2025-11-24 13:36:14 +03:00

215 lines
7.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

// Code node (JavaScript). Input: items[0].json = либо объект, либо массив таких объектов, как ты прислал.
// Output: по одному нормализованному объекту на кейс.
// Никаких внешних зависимостей, всё на ванильном JS.
function toNullish(v) {
if (v === undefined || v === null) return null;
if (typeof v === 'string' && v.trim() === '') return null;
return v;
}
function pick(o, path, def = null) {
try {
return toNullish(path.split('.').reduce((acc, k) => (acc == null ? undefined : acc[k]), o));
} catch {
return def;
}
}
function mapDocuments(docs = []) {
// Проверяем, что docs не null и является массивом
if (!docs || !Array.isArray(docs)) return [];
return docs.map(d => ({
id: toNullish(d.id),
claim_document_id: toNullish(d.id), // у тебя id = claim_document_id
file_id: toNullish(d.file_id),
file_url: toNullish(d.file_url),
file_name: toNullish(d.file_name),
original_file_name: toNullish(d.original_file_name),
field_name: toNullish(d.field_name),
upload_description: toNullish(d.upload_description),
uploaded_at: toNullish(d.uploaded_at),
filename_for_upload: toNullish(d.filename_for_upload),
}));
}
function mapVisionDocs(vds = []) {
// Проверяем, что vds не null и является массивом
if (!vds || !Array.isArray(vds)) return [];
return vds.map(v => ({
claim_document_id: toNullish(v.claim_document_id),
vision_document_id: toNullish(v.vision_document_id),
pages: toNullish(v.pages),
content_sha256: toNullish(v.content_sha256),
vision_text: toNullish(v.vision_text),
vision_pages: Array.isArray(v.vision_pages)
? v.vision_pages.map(p => ({
page: toNullish(p.page),
uid: toNullish(p.uid),
}))
: null,
}));
}
function mapCombinedDocs(cds = []) {
// Проверяем, что cds не null и является массивом
if (!cds || !Array.isArray(cds)) return [];
return cds.map(c => ({
claim_document_id: toNullish(c.claim_document_id),
combined_document_id: toNullish(c.combined_document_id),
pages: toNullish(c.pages),
content_sha256: toNullish(c.content_sha256),
combined_text: toNullish(c.combined_text),
page_summaries: Array.isArray(c.page_summaries)
? c.page_summaries.map(ps => ({
page: toNullish(ps.page),
chars: toNullish(ps.chars),
uid: toNullish(ps.uid),
image_url: toNullish(ps.image_url),
}))
: null,
}));
}
function mapDialogHistory(h = []) {
// ИСПРАВЛЕНО: Проверяем, что h не null и является массивом
if (!h || !Array.isArray(h)) return [];
return h.map(m => ({
id: toNullish(m.id),
role: toNullish(m.role),
message: toNullish(m.message),
message_type: toNullish(m.message_type),
tg_message_id: toNullish(m.tg_message_id),
created_at: toNullish(m.created_at),
}));
}
function mapCoverageReport(cr = null) {
if (!cr) return null;
return {
questions: Array.isArray(cr.questions)
? cr.questions.map(q => ({
name: toNullish(q.name),
value: toNullish(q.value),
status: toNullish(q.status),
source: toNullish(q.source),
confidence: toNullish(q.confidence),
}))
: null,
docs_missing: Array.isArray(cr.docs_missing) ? cr.docs_missing : null,
docs_received: Array.isArray(cr.docs_received) ? cr.docs_received : null,
};
}
function normalizeOne(src) {
const claim = src.claim ?? {};
const userInfo = src.user_info ?? {};
const propertyName = claim.propertyName ?? {};
// answers_parsed уже есть в claim; не мудрим — возвращаем как есть, пустоты -> null
const answersParsed = claim.answers_parsed
? Object.fromEntries(
Object.entries(claim.answers_parsed).map(([k, v]) => [k, toNullish(v)])
)
: null;
// wizard план (часто нужен на фронте) — оставим ключевые поля
let wizard = null;
try {
const parsed = typeof claim.wizard_plan === 'string'
? JSON.parse(claim.wizard_plan)
: (claim.wizard_plan_parsed ?? null);
if (parsed) {
wizard = {
version: toNullish(parsed.version),
case_type: toNullish(parsed.case_type),
goals: Array.isArray(parsed.goals) ? parsed.goals : null,
documents: Array.isArray(parsed.documents) ? parsed.documents : null,
questions: Array.isArray(parsed.questions) ? parsed.questions : null,
risks: Array.isArray(parsed.risks) ? parsed.risks : null,
deadlines: Array.isArray(parsed.deadlines) ? parsed.deadlines : null,
ask_order: Array.isArray(parsed.ask_order) ? parsed.ask_order : null,
notes: toNullish(parsed.notes),
user_text: toNullish(parsed.user_text),
};
}
} catch {
wizard = null;
}
// Склеиваем user — берём user_info, плюс propertyName на всякий, и то, что лежит в диалогах
const user = {
channel: toNullish(userInfo.channel ?? propertyName.channel),
user_id: toNullish(userInfo.user_id ?? propertyName.user_id),
unified_id: toNullish(userInfo.unified_id ?? propertyName.unified_id),
telegram_id: toNullish(userInfo.telegram_id ?? propertyName.telegram_id ?? claim.telegram_id),
session_token: toNullish(userInfo.session_token ?? propertyName.session_token ?? claim.session_token),
};
// Собираем
const out = {
case: {
id: toNullish(pick(claim, 'id')),
prefix: toNullish(pick(claim, 'prefix')),
channel: toNullish(pick(claim, 'channel')),
type_code: toNullish(pick(claim, 'type_code')),
status_code: toNullish(pick(claim, 'status_code')),
created_at: toNullish(pick(claim, 'created_at')),
updated_at: toNullish(pick(claim, 'updated_at')),
telegram_id: toNullish(pick(claim, 'telegram_id')),
session_token: toNullish(pick(claim, 'session_token')),
unified_id: toNullish(pick(claim, 'unified_id')),
case_type: toNullish(pick(claim, 'case_type')),
},
user, // см. выше
answers: answersParsed,
// что загрузили
documents: mapDocuments(src.documents),
// OCR/Vision/Combined, если есть
vision_docs: mapVisionDocs(src.vision_docs),
combined_docs: mapCombinedDocs(src.combined_docs),
// что там в "coverage_report" (кто что заполнил/не заполнил в мастере)
coverage_report: mapCoverageReport(pick(claim, 'coverage_report')),
// история чата (ID, роли, тексты)
dialog_history: mapDialogHistory(src.dialog_history),
// на всякий — куда и что складывали на S3 в момент сохранения
s3_manifest: {
session_token: toNullish(pick(claim, 'session_token')),
documents_meta: Array.isArray(claim.documents_meta) ? claim.documents_meta : null,
},
// флаги/риски, что засетили при сохранении
risks: Array.isArray(claim.risks) ? claim.risks : null,
// план (wizard), как есть — пригодится фронту и валидаторам
wizard_plan: wizard,
};
return out;
}
// === entrypoint ===
const raw = items[0]?.json ?? {};
const arr = Array.isArray(raw) ? raw : [raw];
// опциональный фильтр по claim_id, если в item передадут { claim_id: "..." }
const claimIdFilter = items[0]?.json?.claim_id || items[0]?.json?.claimId || null;
// Прогоняем всё, отдаём по одному Item на кейс
const results = arr
.map(normalizeOne)
.filter(obj => (claimIdFilter ? obj.case.id === claimIdFilter : true))
.map(obj => ({ json: obj }));
return results.length ? results : [{ json: null }];