/* ========================================================================= Screen 3: Chat - 解析摘要 + AI 对话 ========================================================================= */ import { state } from '../state.js'; import { sendChat, validateExtraction } from '../api.js'; import { goStep, toast } from '../steps.js'; const TYPE_COLORS = { savings: { bg: '#E8F1FF', fg: '#007AFF' }, ci: { bg: '#FFF0F0', fg: '#FF3B30' }, iul: { bg: '#F0FFF4', fg: '#34C759' }, }; const TYPE_LABEL = { savings: '储蓄险', ci: '重疾险', iul: 'IUL' }; /* —— 渲染左侧摘要 —— */ function renderSummary() { const el = document.getElementById('chatSummary'); if (!el) return; el.innerHTML = ''; state.extractions.forEach((ext, idx) => { const ok = ext.status === 'success' || (!ext.error && ext.yearCount > 0); const color = TYPE_COLORS[ext.planType] || TYPE_COLORS.savings; const card = document.createElement('div'); card.className = 'bg-surface-container-lowest border border-border-subtle rounded-2xl p-4'; const d = ext.data || {}; const pol = d.policy || {}; let stats = ''; if (ok) { if (ext.planType === 'savings') { stats = `
年缴保费
$${fmt(pol.annual_premium)}
缴费年期
${pol.premium_payment_period || '-'}
`; } else if (ext.planType === 'ci') { stats = `
危疾保额
$${fmt(pol.sum_insured)}
年缴保费
$${fmt(pol.annual_premium)}
`; } else if (ext.planType === 'iul') { const rate = d.index_accounts?.[0]?.current_assumed_rate || d.rates?.fixed_account_current_rate || '-'; stats = `
身故保障
$${fmt(pol.sum_insured)}
演示利率
${rate}
`; } } card.innerHTML = `
${TYPE_LABEL[ext.planType]} ${ok ? '✓ 已解析' : '✗ 失败'}
${ext.productName || ext.pdfName}
${ext.pdfName}${ext.yearCount ? ` · ${ext.yearCount} 年数据` : ''}
${stats} ${ext.error ? `
${ext.error}
` : ''} `; el.appendChild(card); }); } function renderValidationSummary() { const summary = document.getElementById('chatValidationSummary'); const list = document.getElementById('chatValidationList'); if (!summary || !list) return; list.innerHTML = ''; if (!state.validation) { summary.textContent = '正在加载校验结果...'; return; } if (state.validation.validated) { summary.textContent = state.validation.warnCount > 0 ? `校验通过,但有 ${state.validation.warnCount} 条提示` : '校验通过,当前数据可用于正式生成'; } else { summary.textContent = `存在 ${state.validation.errorCount} 项错误,生成前建议先处理`; } (state.validation.issues || []).slice(0, 6).forEach((issue) => { const row = document.createElement('div'); row.className = `rounded-xl px-4 py-3 border ${issue.severity === 'error' ? 'bg-red-50 border-red-200 text-red-700' : 'bg-amber-50 border-amber-200 text-amber-700'}`; row.innerHTML = `
${issue.severity === 'error' ? 'ERROR' : 'WARN'} · ${issue.field}
${issue.message}
`; list.appendChild(row); }); } /* —— 消息流管理 —— */ function appendMessage(role, content) { const wrap = document.getElementById('chatMessages'); if (!wrap) return; const div = document.createElement('div'); div.className = role === 'user' ? 'flex gap-3 max-w-[90%] self-end flex-row-reverse fade-enter' : 'flex gap-3 max-w-[95%] fade-enter'; const avatar = role === 'user' ? `
person
` : `
auto_awesome
`; const bubble = role === 'user' ? `
${escapeHtml(content)}
` : `
AI 顾问
${formatMarkdown(content)}
`; div.innerHTML = avatar + bubble; wrap.appendChild(div); wrap.scrollTop = wrap.scrollHeight; } function appendTyping() { const wrap = document.getElementById('chatMessages'); if (!wrap) return null; const div = document.createElement('div'); div.id = 'typingIndicator'; div.className = 'flex gap-3 max-w-[95%] fade-enter'; div.innerHTML = `
auto_awesome
`; wrap.appendChild(div); wrap.scrollTop = wrap.scrollHeight; return div; } function removeTyping() { document.getElementById('typingIndicator')?.remove(); } function formatMarkdown(text) { if (!text) return ''; // 极简: **加粗** + 换行 + 列表 return escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/^•\s?(.+)$/gm, '
$1
') .replace(/\n\n/g, '

') .replace(/\n/g, '
'); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function fmt(n) { if (!n && n !== 0) return '-'; if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(0) + 'K'; return n.toLocaleString(); } /* —— 初始消息 (后端给的) —— */ function renderInitial() { const wrap = document.getElementById('chatMessages'); if (!wrap) return; wrap.innerHTML = ''; if (state.initialChatMsg) { appendMessage('assistant', state.initialChatMsg); } else { appendMessage('assistant', '解析已完成,我可以帮你解答关于这份计划书的任何问题。\n\n比如:\n• 这个产品的核心卖点是什么?\n• 20年后的预期回报是多少?\n• 和同类型产品相比有什么优势?'); } } async function sendUserMessage() { const input = document.getElementById('chatInput'); const btn = document.getElementById('chatSendBtn'); const text = input.value.trim(); if (!text) return; appendMessage('user', text); input.value = ''; input.style.height = 'auto'; btn.disabled = true; appendTyping(); try { const { message } = await sendChat(state.sessionId, text); removeTyping(); appendMessage('assistant', message); } catch (err) { removeTyping(); appendMessage('assistant', '❌ ' + err.message); toast(err.message, 'error'); } finally { btn.disabled = false; } } export function initChat() { renderSummary(); renderInitial(); renderValidationSummary(); if (state.sessionId && !state.validation) { validateExtraction(state.sessionId) .then((result) => { state.validation = result; renderValidationSummary(); }) .catch((err) => { state.validation = { validated: false, errorCount: 1, warnCount: 0, issues: [{ field: 'system', severity: 'error', message: err.message || '校验接口失败' }], }; renderValidationSummary(); }); } const input = document.getElementById('chatInput'); const btn = document.getElementById('chatSendBtn'); const goBtn = document.getElementById('chatGoGenerateBtn'); const summaryBtn = document.getElementById('chatSummaryToggle'); if (input && input.dataset.bound !== '1') { input.dataset.bound = '1'; // 自适应高度 input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 120) + 'px'; // 切换发送按钮态 if (btn) { const has = input.value.trim().length > 0; btn.disabled = !has; btn.classList.toggle('bg-primary-container', has); btn.classList.toggle('text-white', has); btn.classList.toggle('bg-surface-container-high', !has); btn.classList.toggle('text-text-tertiary', !has); } }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendUserMessage(); } }); } if (btn) btn.onclick = sendUserMessage; if (goBtn) goBtn.onclick = () => goStep('generate'); if (summaryBtn) summaryBtn.onclick = () => { const m = document.getElementById('summaryModal'); m?.classList.toggle('hidden'); }; const closeBtn = document.getElementById('summaryModalClose'); if (closeBtn) closeBtn.onclick = () => { document.getElementById('summaryModal')?.classList.add('hidden'); }; // 快速提问 chips document.querySelectorAll('.chat-quick-chip').forEach(chip => { chip.onclick = () => { const text = chip.textContent.trim(); input.value = text; sendUserMessage(); }; }); }