refactor: 文件上传展示与发送逻辑分离

- 消息对象新增 files 元数据数组(name/language/size)和 _fileContents 内容数组
- content 字段仅保留用户输入的纯文本
- 新增 buildApiMessages() 构建 API 消息时动态拼接文件内容
- chat-area 渲染时从 msg.files 取文件名+图标展示 chip,不暴露文件内容
- 用户消息气泡内文件 chip 用紫色主题区分于输入区的青色 chip
- 导出功能(MD/HTML/TXT)同步适配
This commit is contained in:
Metona
2026-04-04 21:15:02 +08:00
parent 7f31034073
commit 5259b8017a
3 changed files with 115 additions and 33 deletions
+51 -7
View File
@@ -9,6 +9,28 @@ import { escapeHtml, formatTime, downloadFile } from '../utils.js';
let chatAreaEl, messagesContainerEl, emptyStateEl;
/** 根据文件名返回合适的图标 */
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
py: '🐍', pyw: '🐍', pyi: '🐍',
js: '💛', mjs: '💛', cjs: '💛',
ts: '🔷', tsx: '⚛️', jsx: '⚛️',
java: '☕', go: '🐹', rs: '🦀', rb: '💎',
php: '🐘', sh: '🐚', bash: '🐚', zsh: '🐚',
html: '🌐', htm: '🌐', css: '🎨', svg: '🎨',
json: '📋', yaml: '📋', yml: '📋', toml: '📋',
xml: '📰', sql: '🗃️', md: '📝', markdown: '📝',
txt: '📄', log: '📜', csv: '📊', tsv: '📊',
cpp: '⚙️', c: '⚙️', h: '⚙️', hpp: '⚙️',
swift: '🍎', kt: '🤖', lua: '🌙',
dockerfile: '🐳', makefile: '🔨',
};
if (filename.toLowerCase() === 'dockerfile') return '🐳';
if (filename.toLowerCase() === 'makefile') return '🔨';
return iconMap[ext] || '📄';
}
export function initChatArea() {
chatAreaEl = document.querySelector('#chatArea');
messagesContainerEl = document.querySelector('#messagesContainer');
@@ -98,15 +120,27 @@ export function appendMessageDOM(msg, index) {
// 图片
if (msg.images && msg.images.length > 0) {
contentHtml += '<div class="msg-images">';
msg.images.forEach(() => {
contentHtml += `<img class="msg-img" src="data:image/png;base64,${msg.images[0]}" alt="用户图片" data-lightbox="true">`;
});
for (const img of msg.images) {
contentHtml += `<img class="msg-img" src="data:image/png;base64,${img}" alt="用户图片" data-lightbox="true">`;
}
contentHtml += '</div>';
}
// 纯图片消息隐藏空内容
if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && msg.images?.length > 0) {
// 文件 chip(展示用,不暴露文件内容
if (msg.files && msg.files.length > 0) {
contentHtml += '<div class="msg-files">';
for (const f of msg.files) {
const icon = getFileIcon(f.name);
contentHtml += `<div class="file-chip"><span class="file-icon">${icon}</span><span class="file-name">${escapeHtml(f.name)}</span></div>`;
}
contentHtml += '</div>';
}
// 纯图片/文件消息隐藏空内容
if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && (msg.images?.length > 0 || msg.files?.length > 0)) {
contentHtml = contentHtml.replace('<div class="msg-content"></div>', '');
} else if (msg.role === 'user' && msg.files?.length > 0 && msg.content) {
// 有文本也有文件,保留文本内容展示
}
// 统计信息
@@ -243,6 +277,9 @@ export function exportAsMarkdown(session) {
session.messages.forEach(m => {
const role = m.role === 'user' ? '👤 用户' : '🤖 AI';
md += `### ${role}\n\n${m.content || ''}\n\n`;
if (m.files && m.files.length > 0) {
md += m.files.map(f => `📎 \`${f.name}\``).join(' · ') + '\n\n';
}
if (m.think) md += `> **思考:** ${m.think}\n\n`;
});
@@ -261,7 +298,10 @@ code{background:rgba(0,0,0,0.2);padding:2px 6px;border-radius:4px;}</style></hea
<h1>${escapeHtml(session.title)}</h1><p>${formatTime(session.createdAt)} · ${session.model}</p><hr>`;
session.messages.forEach(m => {
html += `<div class="${m.role}"><strong>${m.role === 'user' ? '👤 用户' : '🤖 AI'}</strong><br>${(m.content || '').replace(/\n/g, '<br>')}</div>`;
const fileChips = (m.files && m.files.length > 0)
? '<br>' + m.files.map(f => `<span style="background:rgba(123,47,247,0.15);padding:2px 8px;border-radius:4px;font-size:12px;margin:2px;display:inline-block;">📎 ${escapeHtml(f.name)}</span>`).join(' ')
: '';
html += `<div class="${m.role}"><strong>${m.role === 'user' ? '👤 用户' : '🤖 AI'}</strong>${fileChips}<br>${(m.content || '').replace(/\n/g, '<br>')}</div>`;
});
html += '</body></html>';
@@ -273,7 +313,11 @@ 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`;
txt += `[${m.role === 'user' ? '用户' : 'AI'}]\n`;
if (m.files && m.files.length > 0) {
txt += m.files.map(f => `📎 ${f.name}`).join(' · ') + '\n';
}
txt += `${m.content || ''}\n\n`;
});
downloadFile(`${session.title}.txt`, txt, 'text/plain');