+ +
-

AI 对话 v3.0

+
+ +

AI 对话

+
-
- -
连接中...
- -
+
@@ -225,9 +332,7 @@

能力

-
- -
+
@@ -236,7 +341,7 @@
-
+

👋 开始对话

选择Agent,开始聊天

@@ -249,7 +354,6 @@
-
@@ -280,9 +384,8 @@ 放大图片
- @@ -312,20 +410,21 @@ let currentConversationId = null; let currentAgentId = null; let agents = []; - let providers = []; // 大模型池数据 + let providers = []; let quickPhrases = []; - let lastSentMessage = null; // 记录最后发送的消息 - let lastSentFiles = null; // 记录发送的文件 - let lastSentMessageWithFiles = null; // 记录包含文件信息的完整消息 - let pendingFiles = []; // 待发送的文件 - let lastUserMessage = null; // 存储最后一条用户消息,用于重新生成 - let isRegenerating = false; // 标志:正在重新生成,跳过用户消息显示 - let regeneratingMessageId = null; // 正在重新生成的消息ID - let messageVersionCounter = 0; // 消息版本计数器 - let messageVersions = {}; // 存储每个assistant消息的多个版本 { messageId: [{content, thinking}] } + let lastSentMessage = null; + let lastSentFiles = null; + let lastSentMessageWithFiles = null; + let pendingFiles = []; + let lastUserMessage = null; + let isRegenerating = false; + let regeneratingMessageId = null; + let messageVersionCounter = 0; + let messageVersions = {}; + let currentConversationTitle = '新对话'; document.addEventListener('DOMContentLoaded', () => { - loadProviders(); // 加载大模型池 + loadProviders(); loadAgents(); loadQuickPhrases(); connectWebSocket(); @@ -333,18 +432,31 @@ setupTextarea(); }); - // 加载大模型池 + // ===== 页面切换 ===== + function showHistoryView() { + document.getElementById('historyView').classList.remove('hidden'); + document.getElementById('chatView').classList.remove('active'); + } + + function showChatView() { + document.getElementById('historyView').classList.add('hidden'); + document.getElementById('chatView').classList.add('active'); + } + + function goBackToHistory() { + showHistoryView(); + } + + // ===== Provider & Agent ===== async function loadProviders() { try { const res = await fetch('/api/v2/providers'); const data = await res.json(); providers = data.providers || []; - // 加载后更新Agent信息侧边栏(如果agents已加载) if (agents.length > 0) renderAgentInfoSidebar(); } catch (e) { console.error('加载Provider失败:', e); } } - // 检查当前Agent是否支持视觉 function checkAgentVisionSupport() { const agent = agents.find(a => a.id === currentAgentId); if (!agent) return false; @@ -352,15 +464,6 @@ return provider?.supports_vision || false; } - // 检查当前Agent是否支持某个工具 - function checkAgentToolSupport(toolType) { - const agent = agents.find(a => a.id === currentAgentId); - if (!agent) return false; - const agentTools = agent.tools || []; - return agentTools.includes(toolType); - } - - // 加载Agent async function loadAgents() { try { const res = await fetch('/api/v2/agents'); @@ -368,90 +471,59 @@ agents = data.agents || []; const defaultAgent = agents.find(a => a.is_default) || agents[0]; if (defaultAgent) currentAgentId = defaultAgent.id; - renderAgentSelect(); - renderAgentInfoSidebar(); // 渲染Agent信息侧边栏 + renderAgentSelectMini(); + renderAgentInfoSidebar(); } catch (e) { console.error('加载Agent失败:', e); } } - // 渲染Agent信息侧边栏 function renderAgentInfoSidebar() { const agent = agents.find(a => a.id === currentAgentId); if (!agent) return; - // 更新名称 document.getElementById('agentDisplayName').textContent = agent.display_name || agent.name; document.getElementById('agentName').textContent = agent.name; - // 更新头像(用emoji或首字母) const avatar = document.getElementById('agentAvatar'); avatar.textContent = agent.display_name?.charAt(0) || agent.name?.charAt(0) || '🤖'; - // 更新描述 document.getElementById('agentDescription').textContent = agent.description || '暂无描述'; - // 更新能力标签 const capabilitiesHtml = []; - - // 检查思考能力 const provider = providers.find(p => p.id === agent.llm_provider_id); if (provider) { - if (provider.supports_thinking) { - capabilitiesHtml.push(' 思考'); - } - if (provider.supports_vision) { - capabilitiesHtml.push(' 视觉'); - } - if (provider.supports_function_calling) { - capabilitiesHtml.push(' 工具调用'); - } else { - capabilitiesHtml.push(' 工具(手动)'); - } + if (provider.supports_thinking) capabilitiesHtml.push(' 思考'); + if (provider.supports_vision) capabilitiesHtml.push(' 视觉'); + if (provider.supports_function_calling) capabilitiesHtml.push(' 工具调用'); + else capabilitiesHtml.push(' 工具(手动)'); - // 更新模型信息 const model = agent.model_override || provider.default_model || 'auto'; document.getElementById('agentModelInfo').textContent = model; } - // 检查工具配置 const agentTools = agent.tools || []; - if (agentTools.includes('search')) { - capabilitiesHtml.push(' 搜索'); - } + if (agentTools.includes('search')) capabilitiesHtml.push(' 搜索'); document.getElementById('agentCapabilities').innerHTML = capabilitiesHtml.join('') || '基础对话'; } - function renderAgentSelect() { - const select = document.getElementById('agentSelect'); + function renderAgentSelectMini() { + const select = document.getElementById('agentSelectMini'); select.innerHTML = agents.filter(a => a.is_active).map(a => `` ).join(''); } - function updateAgentSelect(agentId) { - currentAgentId = agentId; - document.getElementById('agentSelect').value = agentId; - } - async function switchAgent() { - const newAgentId = parseInt(document.getElementById('agentSelect').value); + const newAgentId = parseInt(document.getElementById('agentSelectMini').value); if (newAgentId && newAgentId !== currentAgentId) { currentAgentId = newAgentId; if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'switch_agent', agent_id: currentAgentId })); await createNewConversation(); - showAgentSwitchNotice(); - renderAgentInfoSidebar(); // 更新侧边栏信息 + renderAgentInfoSidebar(); } } - function showAgentSwitchNotice() { - const agent = agents.find(a => a.id === currentAgentId); - document.getElementById('messagesContainer').innerHTML = ` -

🔄 已切换 Agent

${agent?.display_name || '助手'}

- `; - } - - // WebSocket + // ===== WebSocket ===== function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${protocol}//${window.location.host}/ws/${userId}`); @@ -463,38 +535,59 @@ function handleWebSocketMessage(data) { switch (data.type) { - case 'history': displayHistory(data.messages); if (data.agent_id) updateAgentSelect(data.agent_id); break; - case 'conversation_created': currentConversationId = data.conversation_id; loadConversations(); break; - case 'new_conversation': currentConversationId = data.conversation_id; loadConversations(); clearMessages(); break; - case 'agent_switched': currentAgentId = data.agent_id; break; - case 'stream_end': document.getElementById('sendBtn').disabled = false; break; + case 'history': + displayHistory(data.messages); + if (data.agent_id) currentAgentId = data.agent_id; + renderAgentSelectMini(); + renderAgentInfoSidebar(); + break; + case 'conversation_created': + currentConversationId = data.conversation_id; + loadConversations(); + break; + case 'new_conversation': + currentConversationId = data.conversation_id; + loadConversations(); + clearMessages(); + showChatView(); + break; + case 'agent_switched': + currentAgentId = data.agent_id; + renderAgentSelectMini(); + renderAgentInfoSidebar(); + break; + case 'stream_end': + document.getElementById('sendBtn').disabled = false; + break; case 'user_message': - lastUserMessage = data.message.content; // 存储最后一条用户消息 - // 如果是刚发送的消息(包含文件),已经显示了,不再重复显示 + lastUserMessage = data.message.content; if (!isRegenerating && data.message.content !== lastSentMessage && data.message.content !== lastSentMessageWithFiles) { appendMessage('user', data.message.content); } lastSentMessage = null; - lastSentMessageWithFiles = null; // 清除标记 - // 注意:不要在这里重置 isRegenerating,要等 assistant_message 处理后再重置 + lastSentMessageWithFiles = null; break; case 'assistant_message': if (isRegenerating && regeneratingMessageId) { - // 添加新版本到现有消息 addResponseVersion(regeneratingMessageId, data.message.content, data.message.thinking_content); regeneratingMessageId = null; - isRegenerating = false; // 在这里重置标志 + isRegenerating = false; } else { appendMessage('assistant', data.message.content, data.message.thinking_content, data.message.agent_name); } document.getElementById('sendBtn').disabled = false; break; - case 'error': showError(data.message); document.getElementById('sendBtn').disabled = false; break; - case 'search_results': displaySearchResults(data.results, data.query); break; + case 'error': + showError(data.message); + document.getElementById('sendBtn').disabled = false; + break; + case 'search_results': + displaySearchResults(data.results, data.query); + break; } } - // 消息渲染 + // ===== 消息渲染 ===== function appendMessage(role, content, thinking = null, agentName = null, extraData = null) { const container = document.getElementById('messagesContainer'); container.querySelector('.welcome')?.remove(); @@ -506,17 +599,14 @@ let messageId = null; if (role === 'assistant') { - // 为assistant消息生成唯一ID messageId = `msg_${++messageVersionCounter}`; div.id = messageId; div.dataset.messageId = messageId; - // 初始化版本存储 messageVersions[messageId] = [{ content, thinking }]; } let html = `
${avatar}
`; - // 思考内容 if (thinking) { html += `
@@ -527,7 +617,6 @@
`; } - // 消息内容 - assistant使用版本容器 html += `
`; if (role === 'assistant') { html += `
@@ -540,25 +629,21 @@ } html += `
`; - // 操作按钮 - 使用隐藏input存储原始内容 html += ``; html += `
`; html += ``; if (role === 'assistant') { html += ``; - // 版本切换控件 - 放在重新生成按钮后面 html += ``; html += ``; html += `1/1`; html += ``; html += ``; } else { - // 用户消息添加编辑按钮 html += ``; } html += `
`; - // Agent信息 if (role === 'assistant' && agentName) { html += `
${agentName}
`; } @@ -566,22 +651,16 @@ html += '
'; div.innerHTML = html; - // 如果是用户消息且有额外数据(搜索结果、图片、文件),在设置innerHTML后追加 if (role === 'user' && extraData) { - console.log('Processing extraData for user message:', extraData); const bodyDiv = div.querySelector('.message-body'); - - // 处理图片(如果有服务器URL,显示图片) if (extraData.images && extraData.images.length > 0) { let imagesHtml = '
'; for (const img of extraData.images) { if (img.url) { - // 有服务器URL,显示真实图片 imagesHtml += `
`; } else { - // 没有URL,显示占位符 imagesHtml += `
${escapeHtml(img.name || '图片')} @@ -592,7 +671,6 @@ if (bodyDiv) bodyDiv.insertAdjacentHTML('beforeend', imagesHtml); } - // 处理文本文件 if (extraData.files && extraData.files.length > 0) { let filesHtml = '
'; for (const f of extraData.files) { @@ -605,9 +683,7 @@ if (bodyDiv) bodyDiv.insertAdjacentHTML('beforeend', filesHtml); } - // 处理搜索结果 if (extraData.search_results && extraData.search_results.length > 0) { - console.log('Building search results HTML for', extraData.search_results.length, 'results'); const searchHtml = buildSearchResultsHtml(extraData.search_results, extraData.search_query || content); if (bodyDiv) bodyDiv.insertAdjacentHTML('beforeend', searchHtml); } @@ -617,29 +693,22 @@ container.scrollTop = container.scrollHeight; } - // 添加新版本到现有消息 function addResponseVersion(messageId, content, thinking) { const versions = messageVersions[messageId]; if (!versions) return; - // 添加新版本 versions.push({ content, thinking }); const newVersionIndex = versions.length - 1; - // 更新容器 const container = document.getElementById(`${messageId}_container`); if (container) { - // 先移除loading(如果有) hideLoadingIndicator(messageId); - // 添加思考块(如果有) const messageBody = container.closest('.message-body'); if (thinking && messageBody) { - // 先移除旧的思考块 const oldThinking = messageBody.querySelector('.thinking-block'); if (oldThinking) oldThinking.remove(); - // 添加新的思考块 const thinkingHtml = `
思考过程 @@ -650,7 +719,6 @@ messageBody.insertAdjacentHTML('afterbegin', thinkingHtml); } - // 隐藏所有旧版本,显示最新版本 container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active')); const newVersionHtml = `
${marked.parse(content)}
@@ -658,24 +726,19 @@ container.insertAdjacentHTML('beforeend', newVersionHtml); } - // 更新复制源 const messageDiv = document.getElementById(messageId); if (messageDiv) { const copySource = messageDiv.querySelector('.copy-source'); if (copySource) copySource.value = content; } - // 显示版本切换控件并更新指示器 showVersionControls(messageId); } - // 显示loading动画 function showLoadingIndicator(messageId) { const container = document.getElementById(`${messageId}_container`); if (container) { - // 隐藏所有版本 container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active')); - // 显示loading const loadingHtml = `
正在生成... @@ -684,22 +747,18 @@ } } - // 隐藏loading动画 function hideLoadingIndicator(messageId) { const loading = document.getElementById(`${messageId}_loading`); if (loading) loading.remove(); } - // 显示版本切换控件 function showVersionControls(messageId) { const switcher = document.getElementById(`${messageId}_version_switcher`); const versions = messageVersions[messageId]; if (switcher && versions && versions.length > 1) { switcher.classList.add('show'); - // 新生成的版本是最后一个,切换到最新版本 const container = document.getElementById(`${messageId}_container`); if (container) { - // 隐藏所有版本,显示最后一个 container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active')); const lastVersion = container.querySelector(`.response-version[data-version="${versions.length - 1}"]`); if (lastVersion) lastVersion.classList.add('active'); @@ -708,20 +767,17 @@ } } - // 更新版本指示器 function updateVersionIndicator(messageId) { const container = document.getElementById(`${messageId}_container`); const label = document.getElementById(`${messageId}_version_label`); const versions = messageVersions[messageId]; if (!container || !label || !versions) return; - // 找到当前激活的版本 const activeVersion = container.querySelector('.response-version.active'); const currentIndex = activeVersion ? parseInt(activeVersion.dataset.version) : 0; label.textContent = `${currentIndex + 1}/${versions.length}`; - // 更新按钮状态 const switcher = document.getElementById(`${messageId}_version_switcher`); if (switcher) { const prevBtn = switcher.querySelector('[data-dir="prev"]'); @@ -731,40 +787,33 @@ } } - // 切换版本 function switchVersion(messageId, direction) { const container = document.getElementById(`${messageId}_container`); const versions = messageVersions[messageId]; if (!container || !versions) return; - // 找到当前激活的版本 const activeVersion = container.querySelector('.response-version.active'); const currentIndex = activeVersion ? parseInt(activeVersion.dataset.version) : 0; const newIndex = currentIndex + direction; if (newIndex < 0 || newIndex >= versions.length) return; - // 切换显示 container.querySelectorAll('.response-version').forEach(v => { v.classList.remove('active'); if (parseInt(v.dataset.version) === newIndex) v.classList.add('active'); }); - // 更新复制源 const messageDiv = document.getElementById(messageId); if (messageDiv) { const copySource = messageDiv.querySelector('.copy-source'); if (copySource) copySource.value = versions[newIndex].content; } - // 更新思考块 const messageBody = container.closest('.message-body'); if (messageBody && versions[newIndex].thinking) { - // 移除旧的思考块 const oldThinking = messageBody.querySelector('.thinking-block'); if (oldThinking) oldThinking.remove(); - // 添加新版本的思考块 const thinkingHtml = `
思考过程 @@ -792,18 +841,12 @@ } function copyMessage(btn) { - // 从隐藏input获取原始内容 const actionsDiv = btn.parentElement; const messageBody = actionsDiv.parentElement; const hiddenInput = messageBody.querySelector('.copy-source'); - if (!hiddenInput) { - console.error('找不到复制源'); - btn.innerHTML = ' 失败'; - return; - } + if (!hiddenInput) { btn.innerHTML = ' 失败'; return; } const text = hiddenInput.value; - // 使用传统复制方法(兼容性更好) const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; @@ -818,137 +861,77 @@ if (success) { btn.innerHTML = ' 已复制'; btn.classList.add('copied'); - setTimeout(() => { - btn.innerHTML = ' 复制'; - btn.classList.remove('copied'); - }, 2000); - } else { - btn.innerHTML = ' 失败'; - } - } catch (err) { - console.error('复制失败:', err); - btn.innerHTML = ' 失败'; - } + setTimeout(() => { btn.innerHTML = ' 复制'; btn.classList.remove('copied'); }, 2000); + } else btn.innerHTML = ' 失败'; + } catch (err) { console.error('复制失败:', err); btn.innerHTML = ' 失败'; } document.body.removeChild(textarea); } function regenerateMessage(messageId) { - if (!lastUserMessage) { - alert('没有可重新生成的消息'); - return; - } - // 设置重新生成标志,避免再次显示用户消息 + if (!lastUserMessage) { alert('没有可重新生成的消息'); return; } isRegenerating = true; regeneratingMessageId = messageId; - - // 显示loading动画 showLoadingIndicator(messageId); - - // 重新发送最后一条用户消息 document.getElementById('sendBtn').disabled = true; if (ws?.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ - action: 'chat', - message: lastUserMessage, - conversation_id: currentConversationId, - agent_id: currentAgentId - })); + ws.send(JSON.stringify({ action: 'chat', message: lastUserMessage, conversation_id: currentConversationId, agent_id: currentAgentId })); } } - // 编辑用户消息并重新发送 - let editingUserMessage = null; // 正在编辑的用户消息元素 + let editingUserMessage = null; function editUserMessage(btn) { const messageDiv = btn.closest('.message.user'); if (!messageDiv) return; - // 获取消息内容 const contentDiv = messageDiv.querySelector('.user-message-text'); const originalContent = contentDiv.textContent; const actionsDiv = messageDiv.querySelector('.message-actions'); - // 保存原始状态用于取消 - editingUserMessage = { - element: messageDiv, - originalContent: originalContent, - contentDiv: contentDiv, - actionsDiv: actionsDiv - }; + editingUserMessage = { element: messageDiv, originalContent: originalContent, contentDiv: contentDiv, actionsDiv: actionsDiv }; - // 将消息文本变成可编辑的textarea contentDiv.innerHTML = ``; - - // 隐藏原有操作按钮,显示编辑操作按钮 actionsDiv.innerHTML = ` `; - // 聚焦到textarea const textarea = contentDiv.querySelector('.edit-textarea'); textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); - // 添加Ctrl+Enter快捷键确认 textarea.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && e.ctrlKey) { - e.preventDefault(); - confirmEditMessage(); - } - if (e.key === 'Escape') { - e.preventDefault(); - cancelEditMessage(); - } + if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); confirmEditMessage(); } + if (e.key === 'Escape') { e.preventDefault(); cancelEditMessage(); } }); } function cancelEditMessage() { if (!editingUserMessage) return; - - // 恢复原始内容 editingUserMessage.contentDiv.innerHTML = escapeHtml(editingUserMessage.originalContent); - - // 恢复原始操作按钮 editingUserMessage.actionsDiv.innerHTML = ` `; - editingUserMessage = null; } function confirmEditMessage() { if (!editingUserMessage) return; - const textarea = editingUserMessage.contentDiv.querySelector('.edit-textarea'); const newContent = textarea.value.trim(); + if (!newContent) { alert('消息内容不能为空'); return; } + if (newContent === editingUserMessage.originalContent) { cancelEditMessage(); return; } - if (!newContent) { - alert('消息内容不能为空'); - return; - } - - // 如果内容没有变化,直接取消编辑 - if (newContent === editingUserMessage.originalContent) { - cancelEditMessage(); - return; - } - - // 更新消息内容 editingUserMessage.contentDiv.innerHTML = escapeHtml(newContent); - - // 恢复操作按钮 editingUserMessage.actionsDiv.innerHTML = ` `; - // 更新最后用户消息记录 lastUserMessage = newContent; - // 找到下一条assistant消息,设置重新生成标志 const container = document.getElementById('messagesContainer'); const allMessages = container.querySelectorAll('.message'); let nextAssistantId = null; @@ -968,22 +951,14 @@ regeneratingMessageId = nextAssistantId; showLoadingIndicator(nextAssistantId); } else { - // 没有assistant消息,需要创建新的 isRegenerating = false; regeneratingMessageId = null; } editingUserMessage = null; - - // 重新发送编辑后的消息 document.getElementById('sendBtn').disabled = true; if (ws?.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ - action: 'chat', - message: newContent, - conversation_id: currentConversationId, - agent_id: currentAgentId - })); + ws.send(JSON.stringify({ action: 'chat', message: newContent, conversation_id: currentConversationId, agent_id: currentAgentId })); } } @@ -994,14 +969,12 @@ function displayHistory(messages) { const container = document.getElementById('messagesContainer'); container.innerHTML = ''; - messages.forEach(m => { - console.log('displayHistory message:', m.role, 'extra_data:', m.extra_data); - appendMessage(m.role, m.content, m.thinking_content, null, m.extra_data); - }); + messages.forEach(m => appendMessage(m.role, m.content, m.thinking_content, null, m.extra_data)); } function clearMessages() { document.getElementById('messagesContainer').innerHTML = '

👋 开始对话

'; + document.getElementById('chatTitle').textContent = '新对话'; } function showError(msg) { @@ -1014,28 +987,19 @@ function displaySearchResults(results, query) { if (!results || results.length === 0) return; - const container = document.getElementById('messagesContainer'); - - // 找到最后一条用户消息 const userMessages = container.querySelectorAll('.message.user'); const lastUserMsg = userMessages[userMessages.length - 1]; if (!lastUserMsg) { - // 没有用户消息,作为独立消息显示 const div = document.createElement('div'); div.className = 'message assistant'; div.innerHTML = `
🔍
${buildSearchResultsHtml(results, query)}
`; container.appendChild(div); } else { - // 在用户消息的 message-body 中追加搜索结果 const msgBody = lastUserMsg.querySelector('.message-body'); - if (msgBody) { - msgBody.innerHTML += buildSearchResultsHtml(results, query); - } + if (msgBody) msgBody.innerHTML += buildSearchResultsHtml(results, query); } - - // 滚动到底部 container.scrollTop = container.scrollHeight; } @@ -1072,72 +1036,121 @@ } } - // 会话管理 + // ===== 历史对话列表 ===== async function loadConversations() { const res = await fetch('/api/conversations'); const data = await res.json(); const conversations = data.conversations || []; - renderConversations(conversations); - if (!currentConversationId && conversations.length > 0) selectConversation(conversations[0].id); + renderHistoryList(conversations); } - function renderConversations(list) { - const container = document.getElementById('conversationList'); - if (list.length === 0) { container.innerHTML = '
暂无对话
'; return; } - container.innerHTML = list.map(c => ` -
- ${c.title || '新对话'} - -
- `).join(''); + function renderHistoryList(list) { + const container = document.getElementById('historyList'); + if (list.length === 0) { + container.innerHTML = `
+ +

暂无对话记录

+

点击右上角「新对话」开始聊天

+
`; + return; + } + + container.innerHTML = list.map(c => { + const title = c.title || '新对话'; + const time = formatTime(c.updated_at || c.created_at); + const msgCount = c.message_count || 0; + + return `
+
+
${escapeHtml(title)}
+
+ ${time} + ${msgCount}条 +
+
+
+ +
+
`; + }).join(''); } - function selectConversation(id) { + function formatTime(timestamp) { + if (!timestamp) return '未知时间'; + const date = new Date(timestamp); + const now = new Date(); + const diff = now - date; + + // 今天:显示时间 + if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + // 昨天 + if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) { + return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + // 本周 + if (diff < 7 * 24 * 60 * 60 * 1000) { + const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + return weekdays[date.getDay()] + ' ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + } + // 其他:显示日期 + return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); + } + + async function selectConversation(id) { currentConversationId = id; - renderConversations([]); - loadConversations(); - if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id })); + showChatView(); + if (ws?.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id })); + } + + // 获取对话标题 + const res = await fetch(`/api/conversations/${id}`); + const data = await res.json(); + currentConversationTitle = data.title || '新对话'; + document.getElementById('chatTitle').textContent = currentConversationTitle; } async function createNewConversation() { - // 检查是否已经是新建对话状态(无对话ID且无消息) + // 检查是否已经是新建状态 const container = document.getElementById('messagesContainer'); const hasMessages = container.querySelectorAll('.message').length > 0; - - // 如果当前没有对话ID且没有消息,则不创建新对话 - if (!currentConversationId && !hasMessages) { - return; // 已经是新建对话状态,无需创建 - } + if (!currentConversationId && !hasMessages) return; const res = await fetch('/api/conversations', { method: 'POST' }); const data = await res.json(); currentConversationId = data.id; + currentConversationTitle = '新对话'; clearMessages(); + showChatView(); loadConversations(); } async function deleteConversation(id, e) { e.stopPropagation(); - if (!confirm('确定删除?')) return; + if (!confirm('确定删除此对话?')) return; await fetch(`/api/conversations/${id}`, { method: 'DELETE' }); - if (id === currentConversationId) { currentConversationId = null; clearMessages(); } + if (id === currentConversationId) { + currentConversationId = null; + clearMessages(); + showHistoryView(); + } loadConversations(); } - // 发送消息 + // ===== 发送消息 ===== function sendMessage() { const input = document.getElementById('messageInput'); const msg = input.value.trim(); - // 如果没有消息且没有文件,不发送 if (!msg && pendingFiles.length === 0) return; - // 检查是否有图片上传 const hasImages = pendingFiles.some(f => f.type && f.type.startsWith('image/')); if (hasImages && !checkAgentVisionSupport()) { const agent = agents.find(a => a.id === currentAgentId); const agentName = agent?.display_name || agent?.name || '当前Agent'; - alert(`⚠️ ${agentName} 不支持图片识别功能\n\n请选择支持视觉能力的Agent(如 vlm-agent),或者移除图片后再发送。`); + alert(`⚠️ ${agentName} 不支持图片识别功能\n\n请选择支持视觉能力的Agent,或者移除图片后再发送。`); document.getElementById('sendBtn').disabled = false; return; } @@ -1146,31 +1159,25 @@ input.value = ''; input.style.height = 'auto'; - // 立即显示用户消息(包含文件) lastSentMessage = msg; appendMessageWithFiles('user', msg, pendingFiles); - // 清空文件预览 pendingFiles = []; document.getElementById('filePreviewArea').innerHTML = ''; - // v3.0: Function Calling模式,不再需要 disabled_tools - - // 发送消息(包含文件) if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'chat', message: msg, conversation_id: currentConversationId, agent_id: currentAgentId, - files: lastSentFiles || [] // 发送的文件列表 + files: lastSentFiles || [] })); } - lastSentFiles = null; // 清空 + lastSentFiles = null; } - // 显示带文件的用户消息 function appendMessageWithFiles(role, content, files) { const container = document.getElementById('messagesContainer'); container.querySelector('.welcome')?.remove(); @@ -1181,28 +1188,19 @@ const avatar = role === 'user' ? '👤' : '🤖'; let html = `
${avatar}
`; - // 显示文本内容 if (content) { html += `
${escapeHtml(content)}
`; } - // 显示文件(图片直接显示,文本文件显示名称) if (files && files.length > 0) { html += '
'; - lastSentFiles = files.map(f => ({ - name: f.name, - type: f.type, - content: f.content, - serverPath: f.serverPath // 服务器路径(用于历史记录) - })); + lastSentFiles = files.map(f => ({ name: f.name, type: f.type, content: f.content, serverPath: f.serverPath })); for (const f of files) { if (f.type.startsWith('image/')) { - // 图片直接显示(用服务器路径或base64) const imgSrc = f.serverPath || f.content; html += `
`; } else { - // 文本文件显示名称和内容摘要 html += `
`; html += `
${f.name}
`; if (f.content && f.content.length < 2000) { @@ -1212,19 +1210,16 @@ } } html += '
'; - - // 记录完整消息(用于判断是否重复显示) lastSentMessageWithFiles = content + '\n[文件:' + files.map(f => f.name).join(',') + ']'; } html += '
'; div.innerHTML = html; container.appendChild(div); - container.scrollTop = container.scrollHeight; } - // 文件上传处理 + // ===== 文件上传 ===== async function handleFileUpload(event) { const files = event.target.files; const previewArea = document.getElementById('filePreviewArea'); @@ -1232,42 +1227,30 @@ for (const file of files) { const fileId = 'file-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); - // 读取文件内容 const reader = new FileReader(); reader.onload = async (e) => { const base64Content = e.target.result; - // 图片:先上传到服务器保存 let serverPath = null; if (file.type.startsWith('image/')) { serverPath = await uploadImageToServer(base64Content, file.name); console.log('图片上传结果:', serverPath); } - const fileData = { - id: fileId, - name: file.name, - type: file.type, - size: file.size, - content: base64Content, // base64数据(用于多模态模型) - serverPath: serverPath // 服务器路径(用于历史记录显示) - }; + const fileData = { id: fileId, name: file.name, type: file.type, size: file.size, content: base64Content, serverPath: serverPath }; pendingFiles.push(fileData); - // 显示预览 const previewItem = document.createElement('div'); previewItem.className = 'file-preview-item'; previewItem.id = fileId + '-preview'; if (file.type.startsWith('image/')) { previewItem.classList.add('image-preview'); - // 预览用本地base64,显示更快 previewItem.innerHTML = ` ${file.name} `; } else { - // 文件图标 let iconClass = 'ri-file-text-line'; if (file.name.endsWith('.pdf')) iconClass = 'ri-file-pdf-line'; else if (file.name.endsWith('.json')) iconClass = 'ri-code-line'; @@ -1285,7 +1268,6 @@ previewArea.appendChild(previewItem); - // 如果上传的是图片且当前Agent不支持视觉,显示警告提示 if (file.type.startsWith('image/') && !checkAgentVisionSupport()) { const agent = agents.find(a => a.id === currentAgentId); const agentName = agent?.display_name || agent?.name || '当前Agent'; @@ -1294,25 +1276,15 @@ warningDiv.id = 'vision-warning-tip'; warningDiv.style.cssText = 'margin-top:8px;padding:8px 12px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;font-size:13px;color:#856404;'; warningDiv.innerHTML = ` ${agentName} 不支持图片识别,请选择支持视觉的Agent或移除图片`; - // 避免重复添加 - if (!document.getElementById('vision-warning-tip')) { - previewArea.appendChild(warningDiv); - } + if (!document.getElementById('vision-warning-tip')) previewArea.appendChild(warningDiv); } }; - // 根据文件类型选择读取方式 - if (file.type.startsWith('image/')) { - reader.readAsDataURL(file); - } else if (file.type === 'application/pdf') { - reader.readAsDataURL(file); - } else { - // 文本类文件直接读取内容 - reader.readAsText(file); - } + if (file.type.startsWith('image/')) reader.readAsDataURL(file); + else if (file.type === 'application/pdf') reader.readAsDataURL(file); + else reader.readAsText(file); } - // 清空 input 以便再次选择 event.target.value = ''; } @@ -1321,7 +1293,6 @@ const preview = document.getElementById(fileId + '-preview'); if (preview) preview.remove(); - // 如果没有图片了,移除视觉警告提示 const hasImages = pendingFiles.some(f => f.type && f.type.startsWith('image/')); if (!hasImages) { const warning = document.getElementById('vision-warning-tip'); @@ -1329,15 +1300,13 @@ } } - // 发送消息 - function setupTextarea() { const textarea = document.getElementById('messageInput'); textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); textarea.addEventListener('input', () => { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; }); } - // 快捷语句 + // ===== 快捷语句 ===== function loadQuickPhrases() { const saved = localStorage.getItem('quickPhrases'); quickPhrases = saved ? JSON.parse(saved) : ['请帮我总结一下', '帮我分析这个问题', '用简单的话解释一下', '给我举个例子', '有什么建议吗?']; @@ -1363,7 +1332,6 @@ renderQuickPhrases(); } - // 鼠标滚轮横向滚动 function scrollPhrases(e) { const wrapper = document.getElementById('phraseListWrapper'); wrapper.scrollLeft += e.deltaY; @@ -1390,7 +1358,7 @@ document.getElementById('newPhraseInput').addEventListener('keydown', e => { if (e.key === 'Enter') addPhrase(); if (e.key === 'Escape') hidePhraseModal(); }); - // 图片放大弹窗 + // ===== 图片弹窗 ===== function openImageLightbox(imageSrc) { const lightbox = document.getElementById('imageLightbox'); const lightboxImg = document.getElementById('lightboxImage'); @@ -1399,16 +1367,10 @@ } function closeImageLightbox() { - const lightbox = document.getElementById('imageLightbox'); - lightbox.classList.remove('show'); + document.getElementById('imageLightbox').classList.remove('show'); } - // ESC键关闭图片弹窗 - document.addEventListener('keydown', e => { - if (e.key === 'Escape') { - closeImageLightbox(); - } - }); + document.addEventListener('keydown', e => { if (e.key === 'Escape') closeImageLightbox(); }); \ No newline at end of file