From c555ca89b718afad73a2cfa0d7651b6789f23ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=B7=B2=E6=B3=A8=E9=94=80?= Date: Fri, 6 Jun 2025 16:53:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20static/js/ui=5Fmanager.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/{ => js}/ui_manager.js | 1026 ++++++++++++++++----------------- 1 file changed, 513 insertions(+), 513 deletions(-) rename static/{ => js}/ui_manager.js (97%) 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, +};