更新 static/js/app.js

This commit is contained in:
2025-06-06 16:52:48 +08:00
parent d7c238de03
commit 8b929b58dd

599
static/js/app.js Normal file
View File

@ -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);