diff --git a/css/style.css b/css/style.css index e004ec2..8d90aa5 100644 --- a/css/style.css +++ b/css/style.css @@ -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; diff --git a/js/components/chat-area.js b/js/components/chat-area.js index 098b29b..b9f5421 100644 --- a/js/components/chat-area.js +++ b/js/components/chat-area.js @@ -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 += '
'; - msg.images.forEach(() => { - contentHtml += `用户图片`; - }); + for (const img of msg.images) { + contentHtml += `用户图片`; + } contentHtml += '
'; } - // 纯图片消息隐藏空内容 - if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && msg.images?.length > 0) { + // 文件 chip(展示用,不暴露文件内容) + if (msg.files && msg.files.length > 0) { + contentHtml += '
'; + for (const f of msg.files) { + const icon = getFileIcon(f.name); + contentHtml += `
${icon}${escapeHtml(f.name)}
`; + } + contentHtml += '
'; + } + + // 纯图片/文件消息隐藏空内容 + if (msg.role === 'user' && (!msg.content || msg.content.trim() === '') && (msg.images?.length > 0 || msg.files?.length > 0)) { contentHtml = contentHtml.replace('
', ''); + } 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;}${escapeHtml(session.title)}

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


`; session.messages.forEach(m => { - html += `
${m.role === 'user' ? '👤 用户' : '🤖 AI'}
${(m.content || '').replace(/\n/g, '
')}
`; + const fileChips = (m.files && m.files.length > 0) + ? '
' + m.files.map(f => `📎 ${escapeHtml(f.name)}`).join(' ') + : ''; + html += `
${m.role === 'user' ? '👤 用户' : '🤖 AI'}${fileChips}
${(m.content || '').replace(/\n/g, '
')}
`; }); 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'); diff --git a/js/components/input-area.js b/js/components/input-area.js index c176b0e..abbcefd 100644 --- a/js/components/input-area.js +++ b/js/components/input-area.js @@ -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,