refactor: 文件上传展示与发送逻辑分离
- 消息对象新增 files 元数据数组(name/language/size)和 _fileContents 内容数组 - content 字段仅保留用户输入的纯文本 - 新增 buildApiMessages() 构建 API 消息时动态拼接文件内容 - chat-area 渲染时从 msg.files 取文件名+图标展示 chip,不暴露文件内容 - 用户消息气泡内文件 chip 用紫色主题区分于输入区的青色 chip - 导出功能(MD/HTML/TXT)同步适配
This commit is contained in:
@@ -739,6 +739,24 @@ body::before {
|
|||||||
margin-top: 8px;
|
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 {
|
.msg-img {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
|
|||||||
@@ -9,6 +9,28 @@ import { escapeHtml, formatTime, downloadFile } from '../utils.js';
|
|||||||
|
|
||||||
let chatAreaEl, messagesContainerEl, emptyStateEl;
|
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() {
|
export function initChatArea() {
|
||||||
chatAreaEl = document.querySelector('#chatArea');
|
chatAreaEl = document.querySelector('#chatArea');
|
||||||
messagesContainerEl = document.querySelector('#messagesContainer');
|
messagesContainerEl = document.querySelector('#messagesContainer');
|
||||||
@@ -98,15 +120,27 @@ export function appendMessageDOM(msg, index) {
|
|||||||
// 图片
|
// 图片
|
||||||
if (msg.images && msg.images.length > 0) {
|
if (msg.images && msg.images.length > 0) {
|
||||||
contentHtml += '<div class="msg-images">';
|
contentHtml += '<div class="msg-images">';
|
||||||
msg.images.forEach(() => {
|
for (const img of msg.images) {
|
||||||
contentHtml += `<img class="msg-img" src="data:image/png;base64,${msg.images[0]}" alt="用户图片" data-lightbox="true">`;
|
contentHtml += `<img class="msg-img" src="data:image/png;base64,${img}" alt="用户图片" data-lightbox="true">`;
|
||||||
});
|
}
|
||||||
contentHtml += '</div>';
|
contentHtml += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 纯图片消息隐藏空内容
|
// 文件 chip(展示用,不暴露文件内容)
|
||||||
if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && msg.images?.length > 0) {
|
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>', '');
|
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 => {
|
session.messages.forEach(m => {
|
||||||
const role = m.role === 'user' ? '👤 用户' : '🤖 AI';
|
const role = m.role === 'user' ? '👤 用户' : '🤖 AI';
|
||||||
md += `### ${role}\n\n${m.content || ''}\n\n`;
|
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`;
|
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>`;
|
<h1>${escapeHtml(session.title)}</h1><p>${formatTime(session.createdAt)} · ${session.model}</p><hr>`;
|
||||||
|
|
||||||
session.messages.forEach(m => {
|
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>';
|
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`;
|
let txt = `${session.title}\n模型: ${session.model}\n时间: ${formatTime(session.createdAt)}\n${'='.repeat(50)}\n\n`;
|
||||||
|
|
||||||
session.messages.forEach(m => {
|
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');
|
downloadFile(`${session.title}.txt`, txt, 'text/plain');
|
||||||
|
|||||||
+46
-26
@@ -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() {
|
export async function sendMessage() {
|
||||||
const text = chatInputEl.value.trim();
|
const text = chatInputEl.value.trim();
|
||||||
if (!text && pendingImages.length === 0 && pendingFiles.length === 0) return;
|
if (!text && pendingImages.length === 0 && pendingFiles.length === 0) return;
|
||||||
@@ -240,8 +266,14 @@ export async function sendMessage() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const msgsToAdd = [];
|
const msgsToAdd = [];
|
||||||
|
|
||||||
// ── 构造用户消息内容 ──
|
// ── 构造用户消息:展示用(content=纯文本,files=元数据) ──
|
||||||
// 场景1:有图片(Ollama 需要单独的 images 消息)
|
const userFiles = pendingFiles.map(f => ({
|
||||||
|
name: f.name,
|
||||||
|
language: f.language,
|
||||||
|
size: f.size
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 场景1:有图片
|
||||||
if (pendingImages.length > 0) {
|
if (pendingImages.length > 0) {
|
||||||
msgsToAdd.push({
|
msgsToAdd.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -255,27 +287,19 @@ export async function sendMessage() {
|
|||||||
|
|
||||||
// 场景2:有文本输入 + 文件,或者只有文件
|
// 场景2:有文本输入 + 文件,或者只有文件
|
||||||
if (text || pendingFiles.length > 0) {
|
if (text || pendingFiles.length > 0) {
|
||||||
let content = text || '';
|
const msg = {
|
||||||
|
|
||||||
// 把文件内容格式化为代码块拼入消息
|
|
||||||
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({
|
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
content: text || '',
|
||||||
timestamp: now
|
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 = '';
|
let modelName = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiMessages = currentSession.messages.map(m => ({
|
const apiMessages = buildApiMessages(currentSession.messages);
|
||||||
role: m.role,
|
|
||||||
content: m.content || '',
|
|
||||||
...(m.images && { images: m.images })
|
|
||||||
}));
|
|
||||||
|
|
||||||
const chatParams = {
|
const chatParams = {
|
||||||
model,
|
model,
|
||||||
|
|||||||
Reference in New Issue
Block a user