From 65360ad822316a8e01ab7487387219f0e835e8d3 Mon Sep 17 00:00:00 2001
From: hubian <908234780@qq.com>
Date: Sun, 26 Apr 2026 10:56:40 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?=
=?UTF-8?q?=E6=8C=89=E9=92=AE=E6=94=AF=E6=8C=81=E5=9B=BE=E7=89=87=E5=92=8C?=
=?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
www/app.js | 324 ++++++++++++++++++++++++++++++++++++++++++++++++-
www/index.html | 6 +-
www/style.css | 114 +++++++++++++++++
3 files changed, 439 insertions(+), 5 deletions(-)
diff --git a/www/app.js b/www/app.js
index fd34aa7..b48fff3 100644
--- a/www/app.js
+++ b/www/app.js
@@ -172,6 +172,9 @@ function openConversation(id) {
+
+
+
+
+
`;
@@ -205,6 +224,45 @@ function openConversation(id) {
userInput.addEventListener('input', (e) => autoResize(e.target));
sendBtn.addEventListener('click', sendMessage);
+ // 绑定上传按钮事件
+ const attachBtn = document.getElementById('attachBtn');
+ const attachPanel = document.getElementById('attachPanel');
+ const imageInput = document.getElementById('imageInput');
+ const fileInput = document.getElementById('fileInput');
+
+ if (attachBtn) {
+ attachBtn.addEventListener('click', () => {
+ attachPanel.classList.toggle('show');
+ });
+ }
+
+ // 点击其他地方关闭面板
+ document.addEventListener('click', (e) => {
+ if (attachPanel && attachPanel.classList.contains('show') &&
+ !attachPanel.contains(e.target) && e.target !== attachBtn) {
+ attachPanel.classList.remove('show');
+ }
+ });
+
+ // 上传选项点击
+ attachPanel.querySelectorAll('.attach-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const type = item.getAttribute('data-type');
+ attachPanel.classList.remove('show');
+ if (type === 'image') {
+ imageInput.click();
+ } else if (type === 'file') {
+ fileInput.click();
+ }
+ });
+ });
+
+ // 图片上传处理
+ imageInput.addEventListener('change', handleImageUpload);
+
+ // 文件上传处理
+ fileInput.addEventListener('change', handleFileUpload);
+
// 绑定快捷按钮事件
document.querySelectorAll('.quick-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -452,7 +510,18 @@ function renderMessages() {
messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => {
const isUser = msg.role === 'user';
const avatar = isUser ? '👤' : '🤖';
- const content = renderMarkdown(msg.content);
+
+ // 处理消息内容(支持图片)
+ let contentHtml = '';
+ if (msg.image) {
+ // 图片消息
+ contentHtml = ``;
+ if (msg.content && msg.content !== '[图片]') {
+ contentHtml += `${renderMarkdown(msg.content)}
`;
+ }
+ } else {
+ contentHtml = renderMarkdown(msg.content);
+ }
const copyIcon = ``;
@@ -477,7 +546,7 @@ function renderMessages() {
${avatar}
-
${content}
+
${contentHtml}
${actions}
@@ -562,4 +631,255 @@ function formatTime(timestamp) {
// PWA 注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {});
+}
+
+// ==================== 文件上传处理 ====================
+
+// 处理图片上传
+async function handleImageUpload(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ // 读取图片为base64
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ const base64 = event.target.result;
+
+ // 添加用户消息(显示图片)
+ currentConversation.messages.push({
+ role: 'user',
+ content: '[图片]',
+ image: base64,
+ imageName: file.name
+ });
+
+ currentConversation.updatedAt = Date.now();
+ saveConversations();
+ renderMessages();
+
+ // 隐藏欢迎界面
+ if (welcome) welcome.style.display = 'none';
+
+ // 调用AI生成
+ await streamGenerateWithImage(base64, file.name);
+ };
+ reader.readAsDataURL(file);
+
+ // 清空input以便再次选择同一文件
+ e.target.value = '';
+}
+
+// 处理文件上传
+async function handleFileUpload(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ const content = event.target.result;
+ const fileName = file.name;
+
+ // 添加用户消息
+ currentConversation.messages.push({
+ role: 'user',
+ content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}`
+ });
+
+ currentConversation.updatedAt = Date.now();
+ saveConversations();
+ renderMessages();
+
+ if (welcome) welcome.style.display = 'none';
+
+ // 调用AI生成
+ await streamGenerateWithFile(content, fileName);
+ };
+
+ // 根据文件类型读取
+ if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) {
+ // PDF/Word文件暂时只显示文件名
+ showToast('PDF/Word文件暂不支持解析,请上传文本文件');
+ e.target.value = '';
+ return;
+ }
+
+ reader.readAsText(file);
+ e.target.value = '';
+}
+
+// 带图片的流式生成
+async function streamGenerateWithImage(base64, imageName) {
+ isLoading = true;
+ sendBtn.disabled = true;
+
+ const aiMessageIndex = currentConversation.messages.length;
+ currentConversation.messages.push({ role: 'assistant', content: '' });
+ renderMessages();
+
+ const lastMessageEl = messagesDiv.lastElementChild;
+ const contentEl = lastMessageEl.querySelector('.message-content');
+ contentEl.innerHTML = '▌';
+
+ try {
+ // 构建多模态消息
+ const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => {
+ if (m.image) {
+ return {
+ role: m.role,
+ content: [
+ { type: 'image_url', image_url: { url: m.image } },
+ { type: 'text', text: '请分析这张图片' }
+ ]
+ };
+ }
+ return { role: m.role, content: m.content };
+ });
+
+ const response = await fetch(CONFIG.apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${CONFIG.apiKey}`
+ },
+ body: JSON.stringify({
+ model: 'glm-4v-flash', // 视觉模型
+ messages: messages,
+ max_tokens: CONFIG.maxTokens,
+ stream: true
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`API错误: ${response.status}`);
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\\n');
+ buffer = lines.pop() || '';
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const jsonStr = line.slice(6).trim();
+ if (jsonStr === '[DONE]') continue;
+
+ try {
+ const data = JSON.parse(jsonStr);
+ if (data.choices && data.choices[0]?.delta?.content) {
+ currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '▌';
+ scrollToBottom();
+ }
+ } catch (err) {}
+ }
+ }
+ }
+
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
+
+ } catch (error) {
+ console.error('Error:', error);
+ currentConversation.messages[aiMessageIndex].content = `抱歉,图片分析失败:${error.message}`;
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
+ } finally {
+ isLoading = false;
+ sendBtn.disabled = false;
+ currentConversation.updatedAt = Date.now();
+ saveConversations();
+ renderMessages();
+ }
+}
+
+// 带文件的流式生成
+async function streamGenerateWithFile(content, fileName) {
+ isLoading = true;
+ sendBtn.disabled = true;
+
+ const aiMessageIndex = currentConversation.messages.length;
+ currentConversation.messages.push({ role: 'assistant', content: '' });
+ renderMessages();
+
+ const lastMessageEl = messagesDiv.lastElementChild;
+ const contentEl = lastMessageEl.querySelector('.message-content');
+ contentEl.innerHTML = '▌';
+
+ try {
+ const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
+ role: m.role,
+ content: m.content
+ }));
+
+ // 添加文件内容作为系统提示
+ messages.unshift({
+ role: 'system',
+ content: `以下是用户上传的文件内容,请根据内容回答问题:\\n文件名:${fileName}\\n内容:\\n${content}`
+ });
+
+ const response = await fetch(CONFIG.apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${CONFIG.apiKey}`
+ },
+ body: JSON.stringify({
+ model: CONFIG.model,
+ messages: messages,
+ max_tokens: CONFIG.maxTokens,
+ stream: true
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`API错误: ${response.status}`);
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\\n');
+ buffer = lines.pop() || '';
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const jsonStr = line.slice(6).trim();
+ if (jsonStr === '[DONE]') continue;
+
+ try {
+ const data = JSON.parse(jsonStr);
+ if (data.choices && data.choices[0]?.delta?.content) {
+ currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '▌';
+ scrollToBottom();
+ }
+ } catch (err) {}
+ }
+ }
+ }
+
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
+
+ } catch (error) {
+ console.error('Error:', error);
+ currentConversation.messages[aiMessageIndex].content = `抱歉,文件处理失败:${error.message}`;
+ contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
+ } finally {
+ isLoading = false;
+ sendBtn.disabled = false;
+ currentConversation.updatedAt = Date.now();
+ saveConversations();
+ renderMessages();
+ }
}
\ No newline at end of file
diff --git a/www/index.html b/www/index.html
index 3cd8b4b..7df9df2 100644
--- a/www/index.html
+++ b/www/index.html
@@ -8,12 +8,12 @@
AI助手
-
+
-
-
+
+