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
+18
View File
@@ -739,6 +739,24 @@ body::before {
margin-top: 8px;
}
/* 消息内的文件 chip 展示 */
.msg-files {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.msg-files .file-chip {
background: rgba(123, 47, 247, 0.08);
border-color: rgba(123, 47, 247, 0.2);
}
.msg-files .file-chip:hover {
border-color: rgba(123, 47, 247, 0.35);
background: rgba(123, 47, 247, 0.12);
}
.msg-img {
max-width: 200px;
max-height: 200px;
+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');
+46 -26
View File
@@ -223,6 +223,32 @@ function stopGeneration() {
/**
* 核心发送逻辑
*/
/** 构建发送给 Ollama API 的消息数组:将文件内容拼入 content */
function buildApiMessages(messages) {
return messages.map(m => {
let content = m.content || '';
// 如果有文件内容,拼接到 API 消息的 content 中
if (m._fileContents && m._fileContents.length > 0) {
const fileParts = m._fileContents.map(f => {
const lang = f.language || '';
return `📄 以下是一个文件内容:\n\n\`\`\`${lang}\n${f.content}\n\`\`\``;
});
if (content) {
content += '\n\n---\n' + fileParts.join('\n\n---\n');
} else {
const count = m._fileContents.length;
content = `请分析以下 ${count > 1 ? count + ' 个' : ''}文件:\n\n${fileParts.join('\n\n---\n')}`;
}
}
return {
role: m.role,
content,
...(m.images && { images: m.images })
};
});
}
export async function sendMessage() {
const text = chatInputEl.value.trim();
if (!text && pendingImages.length === 0 && pendingFiles.length === 0) return;
@@ -240,8 +266,14 @@ export async function sendMessage() {
const now = Date.now();
const msgsToAdd = [];
// ── 构造用户消息内容 ──
// 场景1:有图片(Ollama 需要单独的 images 消息)
// ── 构造用户消息:展示用(content=纯文本,files=元数据) ──
const userFiles = pendingFiles.map(f => ({
name: f.name,
language: f.language,
size: f.size
}));
// 场景1:有图片
if (pendingImages.length > 0) {
msgsToAdd.push({
role: 'user',
@@ -255,27 +287,19 @@ export async function sendMessage() {
// 场景2:有文本输入 + 文件,或者只有文件
if (text || pendingFiles.length > 0) {
let content = text || '';
// 把文件内容格式化为代码块拼入消息
if (pendingFiles.length > 0) {
const fileParts = pendingFiles.map(f => {
const lang = f.language || '';
return `📄 **${f.name}**\n\n\`\`\`${lang}\n${f.content}\n\`\`\``;
});
if (content) {
content += '\n\n---\n' + fileParts.join('\n\n---\n');
} else {
content = `请分析以下 ${pendingFiles.length > 1 ? pendingFiles.length + ' 个' : ''}文件:\n\n${fileParts.join('\n\n---\n')}`;
}
}
msgsToAdd.push({
const msg = {
role: 'user',
content,
content: text || '',
timestamp: now
});
};
if (userFiles.length > 0) {
msg.files = userFiles;
msg._fileContents = pendingFiles.map(f => ({
language: f.language,
content: f.content
}));
}
msgsToAdd.push(msg);
}
// 更新会话标题
@@ -323,11 +347,7 @@ export async function sendMessage() {
let modelName = '';
try {
const apiMessages = currentSession.messages.map(m => ({
role: m.role,
content: m.content || '',
...(m.images && { images: m.images })
}));
const apiMessages = buildApiMessages(currentSession.messages);
const chatParams = {
model,