// 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, };