-
-
- AI 对话 v3.0
+
+
+
AI 对话
+
-
-
-
连接中...
+
-
- `;
- }
-
- // 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 = `
@@ -225,9 +332,7 @@
能力
-
-
-
+
@@ -236,7 +341,7 @@
-
+
@@ -249,7 +354,6 @@
👋 开始对话
选择Agent,开始聊天
@@ -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 || '助手'}
${avatar}
`;
- // 思考内容
if (thinking) {
html += `
@@ -527,7 +617,6 @@
`;
}
- // 消息内容 - assistant使用版本容器
html += ``;
if (role === 'assistant') {
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 = '
@@ -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 += '';
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 `暂无对话记录
+点击右上角「新对话」开始聊天
+
+
`;
+ }).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 = `
+
+ ${escapeHtml(title)}
+
+ ${time}
+ ${msgCount}条
+
+
+
+
+ ${avatar}
`;
- // 显示文本内容
if (content) {
html += ``;
}
- // 显示文件(图片直接显示,文本文件显示名称)
if (files && files.length > 0) {
html += '
`;
} 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(); });
${escapeHtml(content)}
';
- 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 += `
';
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 = `
`;
html += `
';
-
- // 记录完整消息(用于判断是否重复显示)
lastSentMessageWithFiles = content + '\n[文件:' + files.map(f => f.name).join(',') + ']';
}
html += ' ${f.name}
`;
if (f.content && f.content.length < 2000) {
@@ -1212,19 +1210,16 @@
}
}
html += '