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