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(/```([\s\S]*?)```/g, '$1
')
+ .replace(/\n/g, '