Files
gemini_boy/static/js/app.js

600 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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