上传文件至 static
This commit is contained in:
265
static/api.js
Normal file
265
static/api.js
Normal file
@ -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);
|
599
static/app.js
Normal file
599
static/app.js
Normal 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);
|
16
static/marked.min.js
vendored
Normal file
16
static/marked.min.js
vendored
Normal file
@ -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, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
|
||||
.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href="$2">$1</a>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
};
|
7
static/purify.min.js
vendored
Normal file
7
static/purify.min.js
vendored
Normal file
@ -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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||
}
|
||||
};
|
513
static/ui_manager.js
Normal file
513
static/ui_manager.js
Normal file
@ -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 = '<option value="">请选择</option>';
|
||||
Elements.memoryUpdateModelSelect.innerHTML = '<option value="">请选择</option>';
|
||||
|
||||
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 = `
|
||||
<span>${role.name} (${role.id})</span>
|
||||
<button class="delete-btn" data-role-id="${role.id}" title="删除角色">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
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 = `
|
||||
<span>${memory.name} (${memory.id})</span>
|
||||
<button class="delete-btn" data-memory-id="${memory.id}" title="删除记忆集">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
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,
|
||||
};
|
Reference in New Issue
Block a user