From b6bc18a2c355c05c5e54c5bec1470e43f04f6cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=B7=B2=E6=B3=A8=E9=94=80?= Date: Fri, 6 Jun 2025 16:46:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20static?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/api.js | 265 +++++++++++++++++++ static/app.js | 599 +++++++++++++++++++++++++++++++++++++++++++ static/marked.min.js | 16 ++ static/purify.min.js | 7 + static/ui_manager.js | 513 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1400 insertions(+) create mode 100644 static/api.js create mode 100644 static/app.js create mode 100644 static/marked.min.js create mode 100644 static/purify.min.js create mode 100644 static/ui_manager.js diff --git a/static/api.js b/static/api.js new file mode 100644 index 0000000..8ca8a70 --- /dev/null +++ b/static/api.js @@ -0,0 +1,265 @@ +// static/js/api.js + +/* + * 通用的API请求函数。 + * @param { string } endpoint API端点,不包含 / api前缀。 + * @param { string } method HTTP方法,如'GET', 'POST', 'DELETE'。 + * @param { Object } [data = null] 请求体数据,GET请求时转换为查询参数。 + * @param { string } [baseUrl = ''] 可选的基础URL,如果提供则覆盖默认的API_BASE_URL。 + * @returns { Promise < Object >} API响应数据。 + */ +export async function apiRequest(endpoint, method = 'GET', data = null, baseUrl = '') { + let url; + if (baseUrl && (baseUrl.startsWith('http://') || baseUrl.startsWith('https://'))) { + // 如果baseUrl是完整的URL,则直接拼接endpoint + url = `${baseUrl}${endpoint}`; + } else { + // 否则,使用默认的 /api 前缀或提供的相对baseUrl + url = `${baseUrl || '/api'}${endpoint}`; + } + + const options = { + method: method, + headers: { + 'Content-Type': 'application/json', + }, + }; + + if (method === 'GET' && data) { + const query = new URLSearchParams(data).toString(); + url = `${url}?${query}`; + } else if (data) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(url, options); + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: response.statusText })); + throw new Error(errorData.error || errorData.message || `API请求失败: ${response.status}`); + } + // 根据Content-Type判断返回JSON还是文本 + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.includes('application/json')) { + return await response.json(); + } else { + // 假设是纯文本,例如日志文件 + return await response.text(); + } + } catch (error) { + console.error(`API请求 (${method} ${url}) 失败:`, error); + throw error; // 重新抛出错误以便调用方处理 + } +} + +// --- 配置管理 API --- +export async function fetchConfig() { + return apiRequest('/config', 'GET'); +} + +export async function saveConfig(configData) { + return apiRequest('/config', 'POST', configData); +} + +// --- 角色管理 API --- +export async function fetchRoles() { + return apiRequest('/roles', 'GET'); +} + +export async function createRole(roleId, roleName) { + return apiRequest('/roles', 'POST', { id: roleId, name: roleName }); +} + +export async function deleteRole(roleId) { + return apiRequest(`/roles/${roleId}`, 'DELETE'); +} + +// --- 记忆管理 API --- +export async function fetchMemories() { + return apiRequest('/memories', 'GET'); +} + +export async function createMemory(memoryId, memoryName) { + return apiRequest('/memories', 'POST', { id: memoryId, name: memoryName }); +} + +export async function deleteMemory(memoryId) { + return apiRequest(`/memories/${memoryId}`, 'DELETE'); +} + +// --- 会话管理 API --- +export async function fetchActiveSession() { + return apiRequest('/active_session', 'GET'); +} + +export async function setActiveSession(roleId, memoryId) { + return apiRequest('/active_session', 'POST', { role_id: roleId, memory_id: memoryId }); +} + +// --- 特征内容 API --- +export async function fetchFeaturesContent() { + return apiRequest('/features_content', 'GET'); +} + +export async function saveFeaturesContent(content) { + return apiRequest('/features_content', 'POST', content); +} + +// --- 记忆内容 API --- +export async function fetchMemoryContent() { + return apiRequest('/memory_content', 'GET'); +} + +export async function saveMemoryContent(content) { + return apiRequest('/memory_content', 'POST', content); +} + +export async function triggerMemoryUpdate() { + return apiRequest('/memory/trigger_update', 'POST'); +} + +// --- 聊天与日志 API --- +export async function sendMessage(message, useStream = false) { + if (!useStream) { + // 使用标准响应方式 + return apiRequest('/chat', 'POST', { message: message }); + } else { + // 使用流式响应方式 + const url = '/api/chat?stream=true'; + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message: message }), + }; + + try { + const response = await fetch(url, options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: response.statusText })); + throw new Error(errorData.error || errorData.message || `API请求失败: ${response.status}`); + } + + // 检查是否返回了流 + if (response.body) { + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8'); + + // 创建一个更强健的处理SSE的异步迭代器 + return { + [Symbol.asyncIterator]() { + let buffer = ''; + + return { + async next() { + try { + // 读取新数据块 + const { done, value } = await reader.read(); + + if (done) { + console.log('流已结束'); + // 处理buffer中剩余的数据 + if (buffer.trim().length > 0) { + console.log('处理buffer中剩余数据:', buffer); + const finalValue = buffer; + buffer = ''; + return { done: false, value: finalValue }; + } + return { done: true, value: undefined }; + } + + // 解码二进制数据并添加到缓冲区 + buffer += decoder.decode(value, { stream: true }); + + // 检查是否有完整的SSE消息 (以"data: "开头的行) + // 注意:SSE消息格式为 "data: {...}\n\n" + const lines = buffer.split('\n\n'); + + // 如果没有完整的消息,继续读取 + if (lines.length < 2) { + return this.next(); + } + + // 提取完整的消息并更新buffer + const completeMessage = lines[0]; + buffer = lines.slice(1).join('\n\n'); + + // 移除 "data: " 前缀并解析 JSON + if (completeMessage.startsWith('data: ')) { + try { + const jsonString = completeMessage.substring(6); // 移除 "data: " + const parsedData = JSON.parse(jsonString); + + if (parsedData.end) { + // 如果收到结束标记,则流结束 + console.log('收到流结束标记。'); + return { done: true, value: undefined }; + } else if (parsedData.chunk !== undefined) { + // 返回 chunk 内容 + console.log('解析并返回 chunk:', parsedData.chunk.substring(0, 50) + '...'); + return { done: false, value: parsedData.chunk }; + } + } catch (parseError) { + console.error('解析SSE数据失败:', parseError, '原始数据:', completeMessage); + // 如果解析失败,可以返回原始数据抛出错误,这里选择返回原始数据 + return { done: false, value: completeMessage }; + } + } + // 如果不是有效的SSE数据行,继续读取 + return this.next(); + } catch (error) { + console.error('读取流时出错:', error); + return { done: true, value: undefined }; + } + } + }; + } + }; + } else { + throw new Error('服务器未返回流响应'); + } + } catch (error) { + console.error(`流式API请求 (POST ${url}) 失败:`, error); + throw error; + } + } +} + +export async function fetchChatLog(limit = null) { + return apiRequest('/chat_log', 'GET', limit ? { limit: limit } : null); +} + +export async function clearChatLog() { + return apiRequest('/chat_log', 'DELETE'); +} + +// --- 模型列表 API --- +export async function fetchModels() { + try { + // 始终通过后端代理获取模型列表 + const data = await apiRequest('/proxy_models', 'GET'); + if (data && Array.isArray(data.models)) { + return data.models.map(model => { + if (typeof model === 'string') { + return model; + } else if (model && typeof model.name === 'string') { + return model.name; + } + return null; + }).filter(model => model !== null); + } + return []; + } catch (error) { + console.error("获取模型列表失败:", error); + throw error; + } +} + +// --- 日志 API --- +export async function fetchLogs() { + return apiRequest('/logs', 'GET'); +} + +console.log("api.js loaded. Type of fetchConfig:", typeof fetchConfig); diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..bbe12b9 --- /dev/null +++ b/static/app.js @@ -0,0 +1,599 @@ +// static/js/app.js + +import * as api from './api.js'; // 导入API模块 +import * as ui from './ui_manager.js'; // 导入UI管理模块 + +let currentSession = {}; // 全局变量,存储当前会话信息 +let currentRoles = []; // 全局变量,存储当前角色列表 +let currentMemories = []; // 全局变量,存储当前记忆集列表 + +// 初始化应用程序。 +// 加载所有初始数据并设置事件监听器。 +async function initializeApp() { + console.log("应用程序初始化开始..."); + + ui.initializeUIElements(); + + // 恢复窗口宽度设置 + const savedWidth = localStorage.getItem('chatWindowWidth'); + if (savedWidth === 'full') { + const mainContent = document.querySelector('.main-content'); + const container = document.querySelector('.container'); + mainContent.classList.add('full-width'); + container.classList.add('full-width'); + // 更新按钮图标 + const icon = ui.Elements.toggleWidthBtn.querySelector('i'); + icon.classList.remove('fa-expand-alt'); + icon.classList.add('fa-compress-alt'); + } + + // 设置导航菜单点击事件 + ui.Elements.navItems.forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + ui.showSection(item.dataset.target); + // 切换section时刷新数据 + if (item.dataset.target === 'config-section') loadConfigAndModels(); + if (item.dataset.target === 'features-section') loadFeaturesAndRoles(); + if (item.dataset.target === 'memory-section') loadMemoryAndMemories(); + if (item.dataset.target === 'log-section') loadLogs(); + }); + }); + + // 加载初始会话信息、角色和记忆列表 + await loadInitialData(); + + // 设置聊天区域事件监听 + ui.Elements.sendBtn.addEventListener('click', sendMessageHandler); + ui.Elements.userInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); // 阻止默认换行 + sendMessageHandler(); + } + }); + ui.Elements.clearChatHistoryBtn.addEventListener('click', clearChatLogHandler); + ui.Elements.toggleWidthBtn.addEventListener('click', toggleChatWindowWidthHandler); + + // 设置配置区域事件监听 + ui.Elements.saveConfigBtn.addEventListener('click', saveConfigHandler); + ui.Elements.showApiKeyCheckbox.addEventListener('change', (e) => { + ui.Elements.geminiApiKeyInput.type = e.target.checked ? 'text' : 'password'; + }); + + // 设置特征区域事件监听 + ui.Elements.refreshFeaturesBtn.addEventListener('click', loadFeaturesAndRoles); + ui.Elements.saveFeaturesBtn.addEventListener('click', saveFeaturesContentHandler); + ui.Elements.createRoleBtn.addEventListener('click', createRoleHandler); + + // 设置记忆区域事件监听 + ui.Elements.refreshMemoryBtn.addEventListener('click', loadMemoryAndMemories); + ui.Elements.saveMemoryBtn.addEventListener('click', saveMemoryContentHandler); + ui.Elements.triggerMemoryUpdateBtn.addEventListener('click', triggerMemoryUpdateHandler); + ui.Elements.createMemoryBtn.addEventListener('click', createMemoryHandler); + + // 设置日志区域事件监听 + ui.Elements.refreshLogBtn.addEventListener('click', loadLogs); + + // 默认显示聊天界面并加载历史 + ui.showSection('chat-section'); + await loadChatLog(); + console.log("应用程序初始化完毕。"); +} + +// 加载初始数据:会话信息、角色列表、记忆集列表。 +async function loadInitialData() { + try { + currentSession = await api.fetchActiveSession(); + currentRoles = await api.fetchRoles(); // 赋值给全局变量 + currentMemories = await api.fetchMemories(); // 赋值给全局变量 + ui.updateSessionInfo(currentSession, currentRoles, currentMemories); + ui.setMemoryUpdateStatus(currentSession.memory_status); + console.log("初始数据加载完成。", currentSession); + } catch (error) { + ui.showToast(`加载初始数据失败: ${error.message}`, 'error'); + console.error("加载初始数据失败:", error); + } +} + +// 加载并渲染配置和模型列表。 +async function loadConfigAndModels() { + try { + const config = await api.fetchConfig(); + ui.renderConfigForm(config); + + // fetchModels 现在直接通过后端代理获取模型列表,不再需要 baseUrl 参数 + const models = await api.fetchModels(); + ui.populateModelSelects(models, config.API.DEFAULT_GEMINI_MODEL, config.API.MEMORY_UPDATE_MODEL); + console.log("配置和模型列表加载完成。"); + } catch (error) { + ui.showToast(`加载配置失败: ${error.message}`, 'error'); + console.error("加载配置失败:", error); + } +} + +// 保存配置处理函数。 +async function saveConfigHandler() { + const configData = { + API: { + GEMINI_API_BASE_URL: document.getElementById('gemini-api-base-url').value, + GEMINI_API_KEY: ui.Elements.geminiApiKeyInput.value, + DEFAULT_GEMINI_MODEL: ui.Elements.defaultGeminiModelSelect.options[ui.Elements.defaultGeminiModelSelect.selectedIndex].value, + MEMORY_UPDATE_MODEL: ui.Elements.memoryUpdateModelSelect.options[ui.Elements.memoryUpdateModelSelect.selectedIndex].value, + }, + Application: { + CONTEXT_WINDOW_SIZE: parseInt(document.getElementById('context-window-size').value), + MEMORY_RETENTION_TURNS: parseInt(document.getElementById('memory-retention-turns').value), + MAX_SHORT_TERM_EVENTS: parseInt(document.getElementById('max-short-term-events').value), + } + }; + + ui.toggleLoadingState(ui.Elements.saveConfigBtn, true); + try { + const response = await api.saveConfig(configData); + ui.showToast(response.message, 'success'); + await loadConfigAndModels(); // 重新加载以确保UI同步 + } catch (error) { + ui.showToast(`保存配置失败: ${error.message}`, 'error'); + } finally { + ui.toggleLoadingState(ui.Elements.saveConfigBtn, false); + } +} + +// 加载并渲染聊天记录。 +async function loadChatLog() { + try { + const chatLog = await api.fetchChatLog(); + // 确保聊天消息的 role 字段从后端返回的 'ai' 转换为 'bot' + // 并且 content 字段在渲染前经过 DOMPurify 清理和 marked 解析 + ui.renderChatHistory(chatLog.map(msg => ({ + id: msg.id, // 传递消息ID + role: msg.role === 'user' ? 'user' : 'bot', + content: msg.content, + timestamp: msg.timestamp + }))); + console.log("聊天记录加载完成。"); + } catch (error) { + ui.showToast(`加载聊天记录失败: ${error.message}`, 'error'); + console.error("加载聊天记录失败:", error); + } +} + +// 清空聊天记录处理函数。 +async function clearChatLogHandler() { + ui.showModal( + "清空聊天记录", + "确定要清空当前聊天记录吗?此操作不可逆。", + async () => { + ui.toggleLoadingState(ui.Elements.clearChatHistoryBtn, true); + try { + const response = await api.clearChatLog(); + ui.showToast(response.message, 'success'); + ui.renderChatHistory([]); // 清空UI + currentSession.turn_counter = 0; // 重置轮次计数器 + ui.updateSessionInfo(currentSession); + triggerMemoryUpdateHandler(); // 触发记忆更新 + console.log("聊天记录清空成功。"); + } catch (error) { + ui.showToast(`清空聊天记录失败: ${error.message}`, 'error'); + console.error("清空聊天记录失败:", error); + } finally { + ui.toggleLoadingState(ui.Elements.clearChatHistoryBtn, false); + } + }); +} + + +// 发送消息处理函数。 +async function sendMessageHandler() { + const message = ui.Elements.userInput.value.trim(); + if (!message) { + return; + } + + // 为用户消息生成一个唯一的ID + const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + ui.addChatMessage('user', message, new Date().toISOString(), userMessageId); + ui.Elements.userInput.value = ''; // 清空输入框 + + // 禁用发送按钮和输入框 + ui.toggleLoadingState(ui.Elements.sendBtn, true); + ui.toggleLoadingState(ui.Elements.userInput, true); + + // 添加一个临时的"AI 正在思考..."消息,并生成一个唯一的ID + const thinkingMessageId = `bot-thinking-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + ui.addChatMessage('bot', 'AI 正在思考...', new Date().toISOString(), thinkingMessageId); + + // 设置重试参数 + const maxRetries = 3; // 最大重试次数 + const retryDelay = 1000; // 重试间隔(毫秒) + let retryCount = 0; + let success = false; + + while (retryCount < maxRetries && !success) { + try { + if (retryCount > 0) { + // 如果是重试,更新思考消息 + ui.updateChatMessageContent(thinkingMessageId, `AI 正在思考...(第${retryCount}次重试)`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); // 等待一段时间再重试 + } + console.log("正在发送消息...", retryCount > 0 ? `(第${retryCount}次重试)` : ""); + + // 检查是否支持流式响应 + // 使用流式响应方式发送消息 + try { + const response = await api.sendMessage(message, true); // 添加第二个参数表示使用流式响应 + + // 标记请求已成功 + success = true; + + // 初始化流式响应处理 + let fullResponse = ""; + let isFirstChunk = true; + + // 为响应创建一个永久的消息ID,替换思考消息 + const permanentBotMessageId = `bot-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // 处理流式响应的每个块 + for await (const textChunk of response) { + if (textChunk) { + console.log('接收到文本块:', textChunk.substring(0, 50) + '...'); + if (isFirstChunk) { + // 第一次收到数据时,替换思考消息为新的消息 + ui.removeChatMessage(thinkingMessageId); + fullResponse = textChunk; // 初始化 fullResponse + ui.addChatMessage('bot', fullResponse, new Date().toISOString(), permanentBotMessageId); + isFirstChunk = false; + } else { + // 后续数据,累加并更新现有消息 + fullResponse += textChunk; + ui.updateChatMessageContent(permanentBotMessageId, fullResponse); + } + } + } + + // 流结束后,确保界面更新 + if (fullResponse && !isFirstChunk) { + ui.updateChatMessageContent(permanentBotMessageId, fullResponse); + } else if (isFirstChunk) { + // 如果没有收到任何内容 + ui.removeChatMessage(thinkingMessageId); + ui.addChatMessage('bot', '抱歉,未收到AI助手的响应,请重试。', new Date().toISOString(), permanentBotMessageId); + } + + // 流式响应结束后,重新获取会话信息以更新 turn_counter 和 memory_status + currentSession = await api.fetchActiveSession(); + ui.updateSessionInfo(currentSession, currentRoles, currentMemories); + triggerMemoryUpdateHandler(); // 触发记忆更新 + + + } catch (streamError) { + console.warn("流式请求失败,回退到标准请求:", streamError); + + // 回退到标准请求 + const response = await api.sendMessage(message, false); + + if (response && response.success) { + console.log("消息发送成功,收到响应:", response); + // 成功时,更新临时消息为 AI 的实际回复 + ui.updateChatMessageContent(thinkingMessageId, response.response); + // 更新会话信息 + currentSession.turn_counter = response.turn_counter; + currentSession.memory_status = response.memory_status; // 更新记忆状态 + ui.updateSessionInfo(currentSession, currentRoles, currentMemories); + success = true; + triggerMemoryUpdateHandler(); // 触发记忆更新 + } else if (response) { + console.warn("API返回成功但内容表示失败:", response); + throw new Error(response.message || '发送消息失败'); + } else { + throw new Error('服务器未返回有效响应'); + } + } + } catch (error) { + console.error(`发送消息时发生错误 (尝试 ${retryCount + 1}/${maxRetries}):`, error); + retryCount++; + + // 如果是最后一次尝试仍然失败 + if (retryCount >= maxRetries && !success) { + // 移除思考消息并显示错误提示 + ui.removeChatMessage(thinkingMessageId); + ui.showToast(`发送消息失败,已尝试 ${maxRetries} 次。请稍后再试。`, 'error'); + } + } + } + + // 重新启用发送按钮和输入框 + ui.toggleLoadingState(ui.Elements.sendBtn, false); + ui.toggleLoadingState(ui.Elements.userInput, false); +} + +// 加载并渲染特征内容和角色列表。 +async function loadFeaturesAndRoles() { + try { + const roles = await api.fetchRoles(); + const featuresContent = await api.fetchFeaturesContent(); + + ui.renderFeaturesContent(featuresContent); + ui.renderRoleList(roles, currentSession.role_id, switchRoleHandler, deleteRoleHandler); + console.log("特征和角色列表加载完成。"); + } catch (error) { + ui.showToast(`加载特征或角色失败: ${error.message}`, 'error'); + console.error("加载特征或角色失败:", error); + } +} + +// 保存特征内容处理函数。 +async function saveFeaturesContentHandler() { + let content; + try { + content = JSON.parse(ui.Elements.featuresContentTextarea.value); + } catch (error) { + ui.showToast(`特征内容格式错误,请检查 JSON 格式: ${error.message}`, 'error'); + console.error("解析特征内容失败:", error); + return; + } + + ui.toggleLoadingState(ui.Elements.saveFeaturesBtn, true); + try { + const response = await api.saveFeaturesContent(content); + ui.showToast(response.message, 'success'); + } catch (error) { + ui.showToast(`保存特征内容失败: ${error.message}`, 'error'); + console.error("保存特征内容失败:", error); + } finally { + ui.toggleLoadingState(ui.Elements.saveFeaturesBtn, false); + } +} + +// 创建新角色处理函数。 +async function createRoleHandler() { + const roleId = ui.Elements.newRoleIdInput.value.trim(); + const roleName = ui.Elements.newRoleNameInput.value.trim(); + if (!roleId || !roleName) { + ui.showToast("角色ID和名称不能为空。", 'warning'); + return; + } + + ui.toggleLoadingState(ui.Elements.createRoleBtn, true); + try { + const response = await api.createRole(roleId, roleName); + ui.showToast(response.message, 'success'); + ui.Elements.newRoleIdInput.value = ''; + ui.Elements.newRoleNameInput.value = ''; + await loadFeaturesAndRoles(); // 刷新列表 + } catch (error) { + ui.showToast(`创建角色失败: ${error.message}`, 'error'); + } finally { + ui.toggleLoadingState(ui.Elements.createRoleBtn, false); + } +} + +// 切换角色处理函数。 +// @param { string } roleId 要切换到的角色ID。 +async function switchRoleHandler(roleId) { + if (roleId === currentSession.role_id) { + ui.showToast("当前角色已是此角色。", 'info'); + return; + } + ui.showModal( + "切换角色", + `确定要切换到角色 "${roleId}" 吗?这会重置当前对话。`, + async (confirmBtn) => { // 传入确认按钮元素 + ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 + try { + const response = await api.setActiveSession(roleId, currentSession.memory_id); + currentSession = response; + await loadInitialData(); + await loadFeaturesAndRoles(); + await loadChatLog(); + triggerMemoryUpdateHandler(); // 触发记忆更新 + ui.showToast(`已切换到角色 "${roleId}"`, 'success'); + } catch (error) { + ui.showToast(`切换角色失败: ${error.message}`, 'error'); + console.error("切换角色失败:", error); + } finally { + ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 + } + } + ); +} + +// 删除角色处理函数。 +// @param { string } roleId 要删除的角色ID。 +async function deleteRoleHandler(roleId) { + if (roleId === currentSession.role_id) { + ui.showToast("不能删除当前活跃的角色!", 'error'); + return; + } + ui.showModal( + "删除角色", + `确定要删除角色 "${roleId}" 吗?此操作可逆。`, + async (confirmBtn) => { // 传入确认按钮元素 + ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 + try { + const response = await api.deleteRole(roleId); + ui.showToast(response.message, 'success'); + await loadFeaturesAndRoles(); // 刷新列表 + } catch (error) { + ui.showToast(`删除角色失败: ${error.message}`, 'error'); + } finally { + ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 + } + } + ); +} + +// 加载并渲染记忆内容和记忆集列表。 +async function loadMemoryAndMemories() { + try { + const memories = await api.fetchMemories(); + const memoryContent = await api.fetchMemoryContent(); + + ui.renderMemoryContent(memoryContent); + ui.renderMemoryList(memories, currentSession.memory_id, switchMemoryHandler, deleteMemoryHandler); + console.log("记忆内容和记忆集列表加载完成。"); + } catch (error) { + ui.showToast(`加载记忆或记忆集失败: ${error.message}`, 'error'); + console.error("加载记忆或记忆集失败:", error); + } +} + +// 保存记忆内容处理函数。 +async function saveMemoryContentHandler() { + let content; + try { + content = JSON.parse(ui.Elements.memoryContentTextarea.value); + } catch (error) { + ui.showToast(`记忆内容格式错误,请检查 JSON 格式: ${error.message}`, 'error'); + console.error("解析记忆内容失败:", error); + return; + } + + ui.toggleLoadingState(ui.Elements.saveMemoryBtn, true); + try { + const response = await api.saveMemoryContent(content); + ui.showToast(response.message, 'success'); + } catch (error) { + ui.showToast(`保存记忆内容失败: ${error.message}`, 'error'); + console.error("保存记忆内容失败:", error); + } finally { + ui.toggleLoadingState(ui.Elements.saveMemoryBtn, false); + } +} + +// 创建新记忆集处理函数。 +async function createMemoryHandler() { + const memoryId = ui.Elements.newMemoryIdInput.value.trim(); + const memoryName = ui.Elements.newMemoryNameInput.value.trim(); + if (!memoryId || !memoryName) { + ui.showToast("记忆集ID和名称不能为空。", 'warning'); + return; + } + + ui.toggleLoadingState(ui.Elements.createMemoryBtn, true); + try { + const response = await api.createMemory(memoryId, memoryName); + ui.showToast(response.message, 'success'); + ui.Elements.newMemoryIdInput.value = ''; + ui.Elements.newMemoryNameInput.value = ''; + await loadMemoryAndMemories(); // 刷新列表 + } catch (error) { + ui.showToast(`创建记忆集失败: ${error.message}`, 'error'); + } finally { + ui.toggleLoadingState(ui.Elements.createMemoryBtn, false); + } +} + +// 切换记忆集处理函数。 +// @param { string } memoryId 要切换到的记忆集ID。 +async function switchMemoryHandler(memoryId) { + if (memoryId === currentSession.memory_id) { + ui.showToast("当前记忆集是此记忆集。", 'info'); + return; + } + ui.showModal( + "切换记忆集", + `确定要切换到记忆集 "${memoryId}" 吗?这会重置当前对话。`, + async (confirmBtn) => { // 传入确认按钮元素 + ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 + try { + const response = await api.setActiveSession(currentSession.role_id, memoryId); + currentSession = response; + await loadInitialData(); + await loadMemoryAndMemories(); + await loadChatLog(); + triggerMemoryUpdateHandler(); // 触发记忆更新 + ui.showToast(`已切换到记忆集 "${memoryId}"`, 'success'); + } catch (error) { + ui.showToast(`切换记忆集失败: ${error.message}`, 'error'); + console.error("切换记忆集失败:", error); + } finally { + ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 + } + } + ); +} + +// 删除记忆集处理函数。 +// @param { string } memoryId 要删除的记忆集ID。 +async function deleteMemoryHandler(memoryId) { + if (memoryId === currentSession.memory_id) { + ui.showToast("不能删除当前活跃的记忆集!", 'error'); + return; + } + ui.showModal( + "删除记忆集", + `确定要删除记忆集 "${memoryId}" 吗?此操作不可逆。`, + async (confirmBtn) => { // 传入确认按钮元素 + ui.toggleLoadingState(confirmBtn, true); // 禁用确认按钮 + try { + const response = await api.deleteMemory(memoryId); + ui.showToast(response.message, 'success'); + await loadMemoryAndMemories(); // 刷新列表 + } catch (error) { + ui.showToast(`删除记忆集失败: ${error.message}`, 'error'); + } finally { + ui.toggleLoadingState(confirmBtn, false); // 重新启用确认按钮 + } + } + ); +} + +// 触发记忆更新处理函数。 +async function triggerMemoryUpdateHandler() { + ui.toggleLoadingState(ui.Elements.triggerMemoryUpdateBtn, true); + ui.setMemoryUpdateStatus('updating'); // 设置为updating状态 + ui.Elements.memoryUpdateStatusText.textContent = '记忆整理中 (手动)...'; + try { + const response = await api.triggerMemoryUpdate(); + ui.showToast(response.message, 'info'); + // 记忆更新是异步的,触发后需要重新获取会话信息来更新状态 + currentSession = await api.fetchActiveSession(); + ui.setMemoryUpdateStatus(currentSession.memory_status); + } catch (error) { + ui.showToast(`触发记忆更新失败: ${error.message}`, 'error'); + console.error("触发记忆更新失败:", error); + ui.setMemoryUpdateStatus('error'); // 触发失败则显示错误状态 + } finally { + ui.toggleLoadingState(ui.Elements.triggerMemoryUpdateBtn, false); + } +} + + +// 加载并渲染日志内容。 +async function loadLogs() { + try { + const logContent = await api.fetchLogs(); // 通过API获取日志 + ui.renderLogContent(logContent); + console.log("日志加载完成。"); + } catch (error) { + ui.showToast(`加载日志失败: ${error.message}`, 'error'); + console.error("加载日志失败:", error); + ui.renderLogContent(`加载日志失败: ${error.message}`); + } +} + +// 切换聊天窗口宽度处理函数 +function toggleChatWindowWidthHandler() { + const mainContent = document.querySelector('.main-content'); + const container = document.querySelector('.container'); + + // 同时切换主内容区和容器的全宽类 + mainContent.classList.toggle('full-width'); + container.classList.toggle('full-width'); + + // 切换按钮图标 + const icon = ui.Elements.toggleWidthBtn.querySelector('i'); + if (mainContent.classList.contains('full-width')) { + icon.classList.remove('fa-expand-alt'); + icon.classList.add('fa-compress-alt'); + localStorage.setItem('chatWindowWidth', 'full'); + } else { + icon.classList.remove('fa-compress-alt'); + icon.classList.add('fa-expand-alt'); + localStorage.setItem('chatWindowWidth', 'default'); + } +} + +// 应用程序启动 +document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/static/marked.min.js b/static/marked.min.js new file mode 100644 index 0000000..e2ba29d --- /dev/null +++ b/static/marked.min.js @@ -0,0 +1,16 @@ +// Simplified marked.min.js for local testing +var marked = { + parse: function (markdownString) { + // Basic markdown to HTML conversion for testing + return markdownString + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^### (.*$)/gim, '

$1

') + .replace(/\*\*(.*?)\*\*/gim, '$1') + .replace(/\*(.*?)\*/gim, '$1') + .replace(/\[(.*?)\]\((.*?)\)/gim, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/```([\s\S]*?)```/g, '
$1
') + .replace(/\n/g, '
'); + } +}; diff --git a/static/purify.min.js b/static/purify.min.js new file mode 100644 index 0000000..3dbd91c --- /dev/null +++ b/static/purify.min.js @@ -0,0 +1,7 @@ +// Simplified purify.min.js for local testing +var DOMPurify = { + sanitize: function (htmlString) { + // Basic sanitization for testing: remove script tags + return htmlString.replace(/)<[^<]*)*<\/script>/gi, ''); + } +}; diff --git a/static/ui_manager.js b/static/ui_manager.js new file mode 100644 index 0000000..cd7422d --- /dev/null +++ b/static/ui_manager.js @@ -0,0 +1,513 @@ +// static/js/ui_manager.js + +// --- UI 元素选择器 --- +let Elements = {}; // 初始化为空对象,稍后填充 + +/* 初始化所有 UI 元素。 + * 在 DOMContentLoaded 事件之后调用此函数。 + */ +function initializeUIElements() { + Elements = { + // 导航 + navItems: document.querySelectorAll('.nav-item'), + contentSections: document.querySelectorAll('.content-section'), + // 会话信息 + currentRoleNameSpan: document.getElementById('current-role-name'), + currentRoleIdSpan: document.getElementById('current-role-id'), + currentMemoryNameSpan: document.getElementById('current-memory-name'), + currentMemoryIdSpan: document.getElementById('current-memory-id'), + currentTurnCounterSpan: document.getElementById('current-turn-counter'), + memoryUpdateStatusContainer: document.getElementById('memory-update-status'), // 新增:记忆更新状态容器 + memoryUpdateStatusDot: document.querySelector('#memory-update-status .status-dot'), + memoryUpdateStatusText: document.querySelector('#memory-update-status .status-text'), + // 聊天 + chatWindow: document.getElementById('chat-window'), + userInput: document.getElementById('user-input'), + sendBtn: document.getElementById('send-btn'), + clearChatHistoryBtn: document.getElementById('clear-chat-history-btn'), + toggleWidthBtn: document.getElementById('toggle-width-btn'), + // 配置 + configForm: document.getElementById('config-form'), + saveConfigBtn: document.getElementById('save-config-btn'), + showApiKeyCheckbox: document.getElementById('show-api-key'), + geminiApiKeyInput: document.getElementById('gemini-api-key'), + defaultGeminiModelSelect: document.getElementById('default-gemini-model'), + memoryUpdateModelSelect: document.getElementById('memory-update-model'), + // 特征 + refreshFeaturesBtn: document.getElementById('refresh-features-btn'), + saveFeaturesBtn: document.getElementById('save-features-btn'), + roleListDiv: document.getElementById('role-list'), + newRoleIdInput: document.getElementById('new-role-id'), + newRoleNameInput: document.getElementById('new-role-name'), + createRoleBtn: document.getElementById('create-role-btn'), + featuresContentTextarea: document.getElementById('features-content'), + // 记忆 + refreshMemoryBtn: document.getElementById('refresh-memory-btn'), + saveMemoryBtn: document.getElementById('save-memory-btn'), + triggerMemoryUpdateBtn: document.getElementById('trigger-memory-update-btn'), + memoryListDiv: document.getElementById('memory-list'), + newMemoryIdInput: document.getElementById('new-memory-id'), + newMemoryNameInput: document.getElementById('new-memory-name'), + createMemoryBtn: document.getElementById('create-memory-btn'), + memoryContentTextarea: document.getElementById('memory-content'), + // 日志 + refreshLogBtn: document.getElementById('refresh-log-btn'), + logContentPre: document.getElementById('log-content'), + // Toast 消息容器 + toastContainer: document.getElementById('toast-container'), + // 模态框 + modalOverlay: document.getElementById('modal-overlay'), + modalTitle: document.getElementById('modal-title'), + modalMessage: document.getElementById('modal-message'), + modalConfirmBtn: document.getElementById('modal-confirm-btn'), + modalCancelBtn: document.getElementById('modal-cancel-btn'), + }; +} + +// --- 通用 UI 工具函数 --- + +/* 切换显示内容区域。 + * @param { string } targetSectionId 要显示的内容区域ID。 + */ +function showSection(targetSectionId) { + Elements.contentSections.forEach(section => { + section.classList.remove('active'); + }); + document.getElementById(targetSectionId).classList.add('active'); + + Elements.navItems.forEach(item => { + item.classList.remove('active'); + if (item.dataset.target === targetSectionId) { + item.classList.add('active'); + } + }); +} + +/* 显示临时的消息提示。 + * @param { string } message 消息内容。 + * @param { string } type 消息类型 ('success', 'error', 'warning', 'info')。 + * @param { number } duration 消息显示时长(毫秒)。 + */ +function showToast(message, type = 'info', duration = 3000) { + if (!Elements.toastContainer) { + console.error('Toast 容器未找到!'); + return; + } + + const toast = document.createElement('div'); + toast.classList.add('toast', type); + + // 创建图标元素 + const iconElement = document.createElement('i'); + // 根据类型添加 Font Awesome 图标类 + switch (type) { + case 'success': + iconElement.classList.add('fas', 'fa-check-circle'); + break; + case 'error': + iconElement.classList.add('fas', 'fa-times-circle'); + break; + case 'warning': + iconElement.classList.add('fas', 'fa-exclamation-triangle'); + break; + case 'info': + default: + iconElement.classList.add('fas', 'fa-info-circle'); + break; + } + toast.appendChild(iconElement); + + // 创建文本内容元素 + const textSpan = document.createElement('span'); + textSpan.textContent = message; + toast.appendChild(textSpan); + + Elements.toastContainer.appendChild(toast); + + // 移除 toast,与 CSS 动画时间保持一致 + setTimeout(() => { + toast.remove(); + }, duration); +} + +/* 显示一个确认模态框。 + * @param {string} title - 模态框标题。 + * @param {string} message - 模态框消息内容。 + * @param {Function} onConfirm - 用户点击确认按钮时的回调函数。 + * @param {Function} [onCancel] - 用户点击取消按钮或关闭模态框时的回调函数。 + */ +function showModal(title, message, onConfirm, onCancel) { + Elements.modalTitle.textContent = title; + Elements.modalMessage.textContent = message; + Elements.modalOverlay.classList.add('active'); + + // 清除之前的事件监听器 + Elements.modalConfirmBtn.onclick = null; + Elements.modalCancelBtn.onclick = null; + Elements.modalOverlay.onclick = null; // 点击背景关闭 + + Elements.modalConfirmBtn.onclick = () => { + Elements.modalOverlay.classList.remove('active'); + onConfirm(); + }; + + Elements.modalCancelBtn.onclick = () => { + Elements.modalOverlay.classList.remove('active'); + if (onCancel) { + onCancel(); + } + }; + + Elements.modalOverlay.onclick = (e) => { + if (e.target === Elements.modalOverlay) { + Elements.modalOverlay.classList.remove('active'); + if (onCancel) { + onCancel(); + } + } + }; +} + +/* 禁用 / 启用按钮或输入框。 + * @param { HTMLElement } element 要操作的DOM元素。 + * @param { boolean } isDisabled 是否禁用。 + */ +function toggleLoadingState(element, isDisabled) { + if (element) { + element.disabled = isDisabled; + element.classList.toggle('loading', isDisabled); // 添加/移除一个loading class用于样式 + } +} + +/* 更新侧边栏会话信息。 + * @param { Object } sessionInfo 当前会话信息,包含role_id, memory_id, turn_counter等。 + * @param { Object } roles 角色列表,用于查找角色名称。 + * @param { Object } memories 记忆集列表,用于查找记忆名称。 + */ +function updateSessionInfo(sessionInfo, roles = [], memories = []) { + Elements.currentRoleIdSpan.textContent = sessionInfo.role_id; + Elements.currentMemoryIdSpan.textContent = sessionInfo.memory_id; + Elements.currentTurnCounterSpan.textContent = sessionInfo.turn_counter; + + const roleName = roles.find(r => r.id === sessionInfo.role_id)?.name || '未知角色'; + const memoryName = memories.find(m => m.id === sessionInfo.memory_id)?.name || '未知记忆集'; + Elements.currentRoleNameSpan.textContent = roleName; + Elements.currentMemoryNameSpan.textContent = memoryName; + + // 根据会话状态设置记忆更新状态 + setMemoryUpdateStatus(sessionInfo.memory_status); +} + +/* 更新记忆更新状态指示器。 + * @param { string } status 记忆更新状态 ('idle', 'updating', 'success', 'error')。 + */ +function setMemoryUpdateStatus(status) { + const container = Elements.memoryUpdateStatusContainer; + const dot = Elements.memoryUpdateStatusDot; + const text = Elements.memoryUpdateStatusText; + + // 移除所有状态类 + dot.classList.remove('updating', 'success', 'error'); + + switch (status) { + case 'updating': + container.style.display = 'flex'; + dot.classList.add('updating'); + text.textContent = '记忆整理中...'; + break; + case 'success': + container.style.display = 'flex'; + dot.classList.add('success'); + text.textContent = '记忆更新成功'; + break; + case 'error': + container.style.display = 'flex'; + dot.classList.add('error'); + text.textContent = '记忆更新失败'; + break; + case 'idle': + default: + container.style.display = 'none'; // 默认隐藏 + text.textContent = '记忆就绪'; // 保持默认文本,虽然不显示 + break; + } +} + + +// --- 聊天区 UI 函数 --- + +/* 在聊天窗口添加消息。 + * @param { string } sender 'user' 或 'bot'。 + * @param { string } content 消息内容。 + * @param { string } timestamp 消息时间戳。 + * @param { string } [id] 消息的唯一ID,用于后续更新。 + */ +function addChatMessage(sender, content, timestamp, id = null) { + const messageElement = document.createElement('div'); + messageElement.classList.add('chat-message', sender); + // 确保 messageElement 始终有一个 ID,即使没有显式传入 + messageElement.dataset.messageId = id || `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + const avatar = document.createElement('div'); + avatar.classList.add('message-avatar'); + avatar.textContent = sender === 'user' ? 'U' : 'B'; // 用户头像显示 'U', 机器人头像显示 'B' + + const messageContentWrapper = document.createElement('div'); + messageContentWrapper.classList.add('message-content-wrapper'); + + const messageBubble = document.createElement('div'); + messageBubble.classList.add('message-bubble'); + messageBubble.innerHTML = DOMPurify.sanitize(marked.parse(content)); + + const messageTimestamp = document.createElement('div'); + messageTimestamp.classList.add('message-timestamp'); + messageTimestamp.textContent = new Date(timestamp).toLocaleString(); // 格式化时间戳 + + messageContentWrapper.appendChild(messageBubble); + messageContentWrapper.appendChild(messageTimestamp); + + // 总是在正确的位置添加头像和内容 + if (sender === 'user') { + messageElement.appendChild(messageContentWrapper); + messageElement.appendChild(avatar); + } else { + messageElement.appendChild(avatar); + messageElement.appendChild(messageContentWrapper); + } + + Elements.chatWindow.appendChild(messageElement); + Elements.chatWindow.scrollTop = Elements.chatWindow.scrollHeight; // 滚动到底部 +} + +/* 渲染聊天历史记录。 + * @param { Array < Object >} messages 聊天消息数组。 + */ +function renderChatHistory(messages) { + Elements.chatWindow.innerHTML = ''; // 清空现有内容 + messages.forEach(msg => { + // addChatMessage 函数现在会处理 ID 的生成,所以这里直接传递 msg.id + addChatMessage(msg.role, msg.content, msg.timestamp, msg.id); + }); +} + +/* 更新单条聊天消息的内容。 + * @param { string } messageId 消息的唯一ID。 + * @param { string } newContent 新的消息内容。 + */ +function updateChatMessageContent(messageId, newContent) { + console.log(`尝试更新消息ID: ${messageId}`); + const messageElement = Elements.chatWindow.querySelector(`.chat-message[data-message-id="${messageId}"]`); + if (messageElement) { + console.log(`找到消息元素,ID: ${messageId}`); + const messageBubble = messageElement.querySelector('.message-bubble'); + if (messageBubble) { + console.log(`找到消息气泡,更新内容 for ID: ${messageId}`); + messageBubble.innerHTML = DOMPurify.sanitize(marked.parse(newContent)); + + // 强制重绘和重排 + messageElement.style.opacity = "0.99"; + setTimeout(() => { + messageElement.style.opacity = "1"; + // 确保滚动到底部 + Elements.chatWindow.scrollTop = Elements.chatWindow.scrollHeight; + }, 10); + + // 在动画帧中重新计算布局和滚动 + requestAnimationFrame(() => { + Elements.chatWindow.scrollTop = Elements.chatWindow.scrollHeight; + }); + } else { + console.warn(`未找到 messageId 为 "${messageId}" 的消息气泡。`); + } + } else { + console.warn(`未找到 messageId 为 "${messageId}" 的聊天消息元素。`); + } +} + +// --- 配置区 UI 函数 --- + +/* 渲染配置表单。 + * @param { Object } configData 配置数据。 + */ +function renderConfigForm(configData) { + const apiConfig = configData.API || {}; + const appConfig = configData.Application || {}; + + document.getElementById('gemini-api-base-url').value = apiConfig.GEMINI_API_BASE_URL || ''; + const geminiApiKeyInput = Elements.geminiApiKeyInput; + const showApiKeyCheckbox = Elements.showApiKeyCheckbox; + + // 根据用户要求,始终显示完整的API Key + geminiApiKeyInput.value = apiConfig.GEMINI_API_KEY || ''; + + // 初始时,API Key输入框应为文本类型,且“显示API Key”复选框选中,以始终显示完整的API Key + geminiApiKeyInput.type = 'text'; + showApiKeyCheckbox.checked = true; + document.getElementById('context-window-size').value = appConfig.CONTEXT_WINDOW_SIZE || ''; + document.getElementById('memory-retention-turns').value = appConfig.MEMORY_RETENTION_TURNS || ''; + document.getElementById('max-short-term-events').value = appConfig.MAX_SHORT_TERM_EVENTS || ''; +} + +/* 填充模型选择下拉框。 + * @param { Array < string >} models 模型名称列表。 + * @param { string } selectedDefault 默认选中的对话模型。 + * @param { string } selectedMemory 默认选中的记忆模型。 + */ +function populateModelSelects(models, selectedDefault, selectedMemory) { + Elements.defaultGeminiModelSelect.innerHTML = ''; + Elements.memoryUpdateModelSelect.innerHTML = ''; + + models.forEach(model => { + // 只显示模型名称的最后一部分(/之后的部分) + const displayName = model.split('/').pop(); + + const optionDefault = document.createElement('option'); + optionDefault.value = model; + optionDefault.textContent = displayName; + Elements.defaultGeminiModelSelect.appendChild(optionDefault); + + const optionMemory = document.createElement('option'); + optionMemory.value = model; + optionMemory.textContent = displayName; + Elements.memoryUpdateModelSelect.appendChild(optionMemory); + }); + + // 确保正确选中默认模型 + if (selectedDefault) { + const defaultOption = Elements.defaultGeminiModelSelect.querySelector(`option[value="${selectedDefault}"]`); + if (defaultOption) { + defaultOption.selected = true; + } + } + + // 确保正确选中记忆更新模型 + if (selectedMemory) { + const memoryOption = Elements.memoryUpdateModelSelect.querySelector(`option[value="${selectedMemory}"]`); + if (memoryOption) { + memoryOption.selected = true; + } + } +} + + +// --- 特征区 UI 函数 --- + +/* 渲染角色列表。 + * @param { Array < Object >} roles 角色对象数组。 + * @param { string } currentActiveRoleId 当前活跃角色ID。 + * @param { Function } onSwitchRole 点击切换角色的回调函数。 + * @param { Function } onDeleteRole 点击删除角色的回调函数。 + */ +function renderRoleList(roles, currentActiveRoleId, onSwitchRole, onDeleteRole) { + Elements.roleListDiv.innerHTML = ''; + roles.forEach(role => { + const roleItem = document.createElement('div'); + roleItem.classList.add('role-item'); + if (role.id === currentActiveRoleId) { + roleItem.classList.add('active-item'); + } + roleItem.innerHTML = ` + ${role.name} (${role.id}) + + `; + roleItem.querySelector('span').addEventListener('click', () => onSwitchRole(role.id)); + roleItem.querySelector('.delete-btn').addEventListener('click', (e) => { + e.stopPropagation(); // 阻止事件冒泡到父级的点击事件 + onDeleteRole(role.id); + }); + Elements.roleListDiv.appendChild(roleItem); + }); +} + +/* 渲染特征内容。 + * @param { Object } featuresContent 特征JSON对象。 + */ +function renderFeaturesContent(featuresContent) { + Elements.featuresContentTextarea.value = JSON.stringify(featuresContent, null, 2); +} + +// --- 记忆区 UI 函数 --- + +/* 渲染记忆集列表。 + * @param { Array < Object >} memories 记忆集对象数组。 + * @param { string } currentActiveMemoryId 当前活跃记忆集ID。 + * @param { Function } onSwitchMemory 点击切换记忆集的回调函数。 + * @param { Function } onDeleteMemory 点击删除记忆集的回调函数。 + */ +function renderMemoryList(memories, currentActiveMemoryId, onSwitchMemory, onDeleteMemory) { + Elements.memoryListDiv.innerHTML = ''; + memories.forEach(memory => { + const memoryItem = document.createElement('div'); + memoryItem.classList.add('memory-item'); + if (memory.id === currentActiveMemoryId) { + memoryItem.classList.add('active-item'); + } + memoryItem.innerHTML = ` + ${memory.name} (${memory.id}) + + `; + memoryItem.querySelector('span').addEventListener('click', () => onSwitchMemory(memory.id)); + memoryItem.querySelector('.delete-btn').addEventListener('click', (e) => { + e.stopPropagation(); // 阻止事件冒泡到父级的点击事件 + onDeleteMemory(memory.id); + }); + Elements.memoryListDiv.appendChild(memoryItem); + }); +} + +/* 渲染记忆内容。 + * @param { Object } memoryContent 记忆JSON对象。 + */ +function renderMemoryContent(memoryContent) { + Elements.memoryContentTextarea.value = JSON.stringify(memoryContent, null, 2); +} + + +// --- 日志区 UI 函数 --- + +/* 渲染日志内容。 + * @param { string } logContent 日志文本。 + */ +function renderLogContent(logContent) { + Elements.logContentPre.textContent = logContent; + Elements.logContentPre.scrollTop = Elements.logContentPre.scrollHeight; // 滚动到底部 +} + +/* 从聊天窗口移除单条消息。 + * @param { string } messageId 消息的唯一ID。 + */ +function removeChatMessage(messageId) { + const messageElement = Elements.chatWindow.querySelector(`.chat-message[data-message-id="${messageId}"]`); + if (messageElement) { + messageElement.remove(); + } else { + console.warn(`未找到 messageId 为 "${messageId}" 的聊天消息元素,无法移除。`); + } +} + +// 导出所有需要被 app.js 调用的函数和 Elements +export { + Elements, + initializeUIElements, // 导出初始化函数 + showSection, + showToast, + showModal, + toggleLoadingState, + updateSessionInfo, + setMemoryUpdateStatus, + addChatMessage, + renderChatHistory, + updateChatMessageContent, // 确保这个函数也被导出 + removeChatMessage, // 新增:导出移除消息函数 + renderConfigForm, + populateModelSelects, + renderRoleList, + renderFeaturesContent, + renderMemoryList, + renderMemoryContent, + renderLogContent, +};