/** * ChatArea - 聊天区域组件 * 负责消息渲染、流式更新、导出功能 */ import { state, KEYS } from '../state.js'; import { marked } from '../marked-config.js'; import { escapeHtml, formatTime, downloadFile } from '../utils.js'; let chatAreaEl, messagesContainerEl, emptyStateEl; export function initChatArea() { chatAreaEl = document.querySelector('#chatArea'); messagesContainerEl = document.querySelector('#messagesContainer'); emptyStateEl = document.querySelector('#emptyState'); } /** 安全地将 Markdown 转为 HTML */ export function safeMarkdown(text) { if (!text && text !== 0) return ''; const str = typeof text === 'string' ? text : String(text); if (!str) return ''; try { const result = marked.parse(str); return typeof result === 'string' ? result : escapeHtml(str); } catch (e) { return escapeHtml(str); } } /** 滚动到底部 */ export function scrollToBottom() { requestAnimationFrame(() => { chatAreaEl.scrollTop = chatAreaEl.scrollHeight; }); } /** 渲染所有消息(增量渲染) */ export function renderMessages() { const currentSession = state.get(KEYS.CURRENT_SESSION); const msgs = currentSession ? currentSession.messages : []; if (msgs.length === 0) { emptyStateEl.style.display = ''; messagesContainerEl.style.display = 'none'; return; } emptyStateEl.style.display = 'none'; messagesContainerEl.style.display = ''; const existingCount = messagesContainerEl.children.length; for (let i = existingCount; i < msgs.length; i++) { appendMessageDOM(msgs[i], i); } scrollToBottom(); } /** 追加单条消息 DOM */ export function appendMessageDOM(msg, index) { const div = document.createElement('div'); div.className = `message ${msg.role}`; div.dataset.index = index; const avatar = msg.role === 'user' ? '👤' : '🤖'; const roleLabel = msg.role === 'user' ? '你' : 'AI'; const modelTag = (msg.role === 'assistant' && msg.model) ? `${escapeHtml(msg.model)}` : ''; let contentHtml = ''; const safeContent = (msg.content != null) ? String(msg.content) : ''; if (msg.role === 'assistant') { // 思考块 if (msg.think) { contentHtml += `
🤔 思考过程
${escapeHtml(msg.think)}
`; } if (safeContent) { contentHtml += `
${safeMarkdown(safeContent)}
`; } else { contentHtml += `
`; } } else { contentHtml += `
${escapeHtml(safeContent)}
`; } // 图片 if (msg.images && msg.images.length > 0) { contentHtml += '
'; msg.images.forEach(() => { contentHtml += `用户图片`; }); contentHtml += '
'; } // 纯图片消息隐藏空内容 if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && msg.images?.length > 0) { contentHtml = contentHtml.replace('
', ''); } // 统计信息 const stats = []; if (msg.timestamp) stats.push(formatTime(msg.timestamp)); if (msg.eval_count) stats.push(`${msg.eval_count} tokens`); if (msg.total_duration) stats.push(`${(msg.total_duration / 1e9).toFixed(1)}s`); if (stats.length > 0) { contentHtml += `
${stats.map(s => `${s}`).join(' · ')}
`; } div.innerHTML = `
${avatar}
${roleLabel}${modelTag}
${contentHtml}
`; messagesContainerEl.appendChild(div); } /** 更新最后一条 assistant 消息(流式输出) */ export function updateLastAssistantMessage(content, think, stats, model) { const lastMsg = messagesContainerEl.querySelector('.message.assistant:last-child'); if (!lastMsg) return; const loadingDots = lastMsg.querySelector('.loading-dots'); const loadingText = lastMsg.querySelector('.loading-text'); if (loadingDots) loadingDots.remove(); if (loadingText) loadingText.remove(); if (model) { let modelTag = lastMsg.querySelector('.model-tag'); if (!modelTag) { modelTag = document.createElement('span'); modelTag.className = 'model-tag'; const roleEl = lastMsg.querySelector('.msg-role'); if (roleEl) roleEl.appendChild(modelTag); } modelTag.textContent = model; } const contentDiv = lastMsg.querySelector('.msg-content'); const safeContent = (content != null) ? String(content) : ''; if (contentDiv && safeContent) { contentDiv.innerHTML = safeMarkdown(safeContent); } // 思考块 let thinkBlock = lastMsg.querySelector('.think-block'); if (think) { if (!thinkBlock) { const msgBody = lastMsg.querySelector('.msg-body'); thinkBlock = document.createElement('div'); thinkBlock.className = 'think-block'; thinkBlock.innerHTML = `
🤔 思考过程
`; msgBody.insertBefore(thinkBlock, contentDiv); } thinkBlock.querySelector('pre').textContent = think; } if (stats) { let statsDiv = lastMsg.querySelector('.msg-stats'); if (!statsDiv) { statsDiv = document.createElement('div'); statsDiv.className = 'msg-stats'; lastMsg.querySelector('.msg-body').appendChild(statsDiv); } const parts = []; if (stats.eval_count) parts.push(`${stats.eval_count} tokens`); if (stats.total_duration) parts.push(`${(stats.total_duration / 1e9).toFixed(1)}s`); if (parts.length) statsDiv.innerHTML = parts.join(' · '); } scrollToBottom(); } /** 追加临时的 assistant 消息占位 */ export function appendAssistantPlaceholder() { const div = document.createElement('div'); div.className = 'message assistant'; div.innerHTML = `
🤖
AI
正在思考...
`; messagesContainerEl.appendChild(div); scrollToBottom(); } /** 追加系统提示消息 */ export function appendSystemMessage(text) { const div = document.createElement('div'); div.style.cssText = 'text-align:center;padding:8px;font-size:12px;color:var(--text-muted);'; div.textContent = text; emptyStateEl.style.display = 'none'; messagesContainerEl.style.display = ''; messagesContainerEl.appendChild(div); scrollToBottom(); } /** 获取消息容器 */ export function getMessagesContainer() { return messagesContainerEl; } /** 清空消息 DOM */ export function clearMessages() { messagesContainerEl.innerHTML = ''; } // ── 导出功能 ── /** 导出为 Markdown */ export function exportAsMarkdown(session) { let md = `# ${session.title}\n\n`; md += `**模型:** ${session.model} \n`; md += `**时间:** ${formatTime(session.createdAt)}\n\n---\n\n`; session.messages.forEach(m => { const role = m.role === 'user' ? '👤 用户' : '🤖 AI'; md += `### ${role}\n\n${m.content || ''}\n\n`; if (m.think) md += `> **思考:** ${m.think}\n\n`; }); downloadFile(`${session.title}.md`, md, 'text/markdown'); } /** 导出为 HTML */ export function exportAsHtml(session) { let html = `${escapeHtml(session.title)}

${escapeHtml(session.title)}

${formatTime(session.createdAt)} · ${session.model}


`; session.messages.forEach(m => { html += `
${m.role === 'user' ? '👤 用户' : '🤖 AI'}
${(m.content || '').replace(/\n/g, '
')}
`; }); html += ''; downloadFile(`${session.title}.html`, html, 'text/html'); } /** 导出为 TXT */ export function exportAsTxt(session) { let txt = `${session.title}\n模型: ${session.model}\n时间: ${formatTime(session.createdAt)}\n${'='.repeat(50)}\n\n`; session.messages.forEach(m => { txt += `[${m.role === 'user' ? '用户' : 'AI'}]\n${m.content || ''}\n\n`; }); downloadFile(`${session.title}.txt`, txt, 'text/plain'); }