更新 static/js/ui_manager.js

This commit is contained in:
2025-06-06 16:53:09 +08:00
parent 8b929b58dd
commit c555ca89b7

513
static/js/ui_manager.js Normal file
View 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,
};