/**
* 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 += `
`;
}
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 = `
🤖
`;
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');
}