/* =========================================================================
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)}
`;
}
}
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'
? ``
: `
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();
};
});
}