const config = {hubUrl: '/robotChat'};
// DOM 元素引用 (动态获取)
let chatArea, messageInput, sendBtn, statusEl, themeToggle, modelSelect, sidebarToggle;
const sessionList = document.getElementById('sessionList');
const btnNewSession = document.getElementById('btnNewSession');
const sessionSidebar = document.getElementById('sessionSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
let connection = null;
let isConnected = false;
let isLoading = false;
let currentAssistantMessage = null; // DOM 元素
let currentAssistantRaw = ''; // 原始 Markdown 累积
let pendingSessionElement = null; // 保存待更新的临时会话元素
let cancelBtn = null; // 取消按钮引用
// ============ 移动端视口高度处理 ============
(function initMobileViewport() {
// 设置 CSS 自定义属性来处理移动端视口高度
function setViewportHeight() {
// 获取真实视口高度(不包括地址栏等)
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
// 初始设置
setViewportHeight();
// 监听窗口大小变化和方向变化
window.addEventListener('resize', setViewportHeight);
window.addEventListener('orientationchange', () => {
setTimeout(setViewportHeight, 100);
});
// iOS Safari 特殊处理:监听滚动事件来调整视口
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
let lastScrollTop = 0;
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop !== lastScrollTop) {
setViewportHeight();
lastScrollTop = scrollTop;
}
}, { passive: true });
}
})();
// ============ 移动端输入框焦点处理 ============
(function initMobileInputHandling() {
if (window.innerWidth > 600) return; // 仅在移动端执行
// 监听输入框焦点,滚动到可视区域
document.addEventListener('focusin', (e) => {
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
setTimeout(() => {
// 使用 scrollIntoView 而不是依赖 fixed 定位
e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300); // 等待键盘弹出
}
});
// 监听输入框失焦
document.addEventListener('focusout', (e) => {
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
// 延迟执行,确保键盘收起后再滚动
setTimeout(() => {
// 滚动回聊天区域顶部(如果需要)
const chatArea = document.getElementById('chatArea');
if (chatArea && chatArea.scrollTop === 0) {
// 如果聊天区域在顶部,不需要额外滚动
return;
}
}, 100);
}
});
})();
// 获取当前 SessionId
function getCurrentSessionId() {
return document.getElementById('currentSessionId')?.value || '';
}
// ============ 模态框相关 (保持不变) ============
const modalOverlay = document.getElementById('modalOverlay');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalFooter = document.getElementById('modalFooter');
const modalClose = document.getElementById('modalClose');
const modalCancel = document.getElementById('modalCancel');
const modalConfirm = document.getElementById('modalConfirm');
let modalResolve = null;
function showModal(options = {}) {
return new Promise((resolve) => {
modalResolve = resolve;
const {
title = '提示',
message = '',
type = 'alert',
confirmText = '确定',
cancelText = '取消'
} = options;
modalTitle.textContent = title;
modalMessage.textContent = message;
modalConfirm.textContent = confirmText;
modalCancel.textContent = cancelText;
if (type === 'alert') {
modalFooter.classList.add('single-button');
modalCancel.style.display = 'none';
} else {
modalFooter.classList.remove('single-button');
modalCancel.style.display = 'block';
}
modalOverlay.classList.add('show');
setTimeout(() => modalConfirm.focus(), 100);
});
}
function hideModal(result = false) {
modalOverlay.classList.remove('show');
if (modalResolve) {
modalResolve(result);
modalResolve = null;
}
}
function customAlert(message, title = '提示') {
return showModal({title, message, type: 'alert'});
}
function customConfirm(message, title = '确认') {
return showModal({title, message, type: 'confirm'});
}
modalClose.addEventListener('click', () => hideModal(false));
modalCancel.addEventListener('click', () => hideModal(false));
modalConfirm.addEventListener('click', () => hideModal(true));
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) hideModal(false);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modalOverlay.classList.contains('show')) hideModal(false);
});
// ============ Highlight.js & Theme ============
function updateHighlightTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const lightTheme = document.getElementById('highlight-light-theme');
const darkTheme = document.getElementById('highlight-dark-theme');
if (currentTheme === 'dark') {
lightTheme.disabled = true;
darkTheme.disabled = false;
} else {
lightTheme.disabled = false;
darkTheme.disabled = true;
}
}
function highlightCodeBlocks(container) {
if (!container) return;
const codeBlocks = container.querySelectorAll('pre code');
codeBlocks.forEach(block => {
if (block.classList.contains('hljs')) {
block.classList.remove('hljs');
block.removeAttribute('data-highlighted');
}
hljs.highlightElement(block);
});
}
function toggleTheme() {
const cur = document.documentElement.getAttribute('data-theme');
const next = cur === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('chatTheme', next);
updateHighlightTheme();
highlightCodeBlocks(chatArea);
}
// 初始化主题
(function initTheme() {
const saved = localStorage.getItem('chatTheme');
let theme;
if (saved) {
theme = saved;
} else {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
} else {
theme = 'light';
}
}
document.documentElement.setAttribute('data-theme', theme);
updateHighlightTheme();
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('chatTheme')) {
const newTheme = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
updateHighlightTheme();
highlightCodeBlocks(chatArea);
}
});
}
})();
// ============ DOM 更新与事件绑定 ============
function updateDomReferences() {
chatArea = document.getElementById('chatArea');
messageInput = document.getElementById('messageInput');
sendBtn = document.getElementById('sendBtn');
statusEl = document.getElementById('status');
themeToggle = document.getElementById('themeToggle');
modelSelect = document.getElementById('modelSelect');
sidebarToggle = document.getElementById('sidebarToggle');
bindChatEvents();
// 恢复连接状态显示
if (statusEl) {
setConnectionStatus(isConnected);
}
highlightCodeBlocks(chatArea);
scrollToBottom();
}
async function onModelChange() {
if (!modelSelect) return;
const newModelValue = modelSelect.value;
console.log('📝 模型已切换至:', modelSelect.options[modelSelect.selectedIndex].text);
const sessionId = getCurrentSessionId();
if (sessionId) {
try {
const form = new FormData();
form.append("sessionId", sessionId);
form.append("modelType", newModelValue);
const response = await fetch('/AiChat/UpdateSessionModelType', {
method: 'PUT',
body: form
});
const result = await response.json();
if (!result.success) {
console.error('❌ 更新会话模型类型失败:', result.message);
await customAlert('❌ 更新会话模型类型失败');
} else {
console.log('✅ 已更新会话模型类型:', sessionId, newModelValue);
}
} catch (err) {
console.error('❌ 更新会话模型类型失败:', err);
}
}
}
function bindChatEvents() {
if (messageInput) {
messageInput.onkeydown = async e => {
if ((e.ctrlKey && e.key === 'Enter') || (e.altKey && e.key === 's')) {
e.preventDefault();
await sendMessage();
}
};
messageInput.oninput = updateInputState;
}
if (sendBtn) {
sendBtn.onclick = sendMessage;
}
if (themeToggle) {
themeToggle.onclick = toggleTheme;
}
if (modelSelect) {
modelSelect.onchange = onModelChange;
}
if (sidebarToggle) {
sidebarToggle.onclick = () => toggleSidebar();
}
// 更新输入状态
updateInputState();
}
// 复制功能委托
document.addEventListener('click', (e) => {
// 查找最近的 .btn-copy-markdown 元素
const btn = e.target.closest('.btn-copy-markdown');
if (btn) {
e.stopPropagation();
const rawContentEl = btn.nextElementSibling;
if (rawContentEl && rawContentEl.classList.contains('raw-markdown')) {
const text = rawContentEl.textContent;
navigator.clipboard.writeText(text).then(() => {
const originalHTML = btn.innerHTML;
btn.innerHTML = '✅'; // 简单的反馈
setTimeout(() => btn.innerHTML = originalHTML, 2000);
}).catch(async err => {
console.error('复制失败:', err);
await customAlert('复制失败');
});
}
}
// 删除消息
const delBtn = e.target.closest('.btn-delete-message');
if (delBtn) {
e.stopPropagation();
const messageEl = delBtn.closest('.message');
if (!messageEl) return;
// 如果正在生成中(assistant且有cancel-btn),不允许删除?或者允许但要小心
// 这里简单处理:确认后尝试删除
customConfirm('确定要删除这条消息吗?').then(async (confirmed) => {
if (!confirmed) return;
const messageId = messageEl.dataset.messageId;
if (!messageId) {
// 如果没有ID(新发送的消息),提示刷新
// 或者如果是 assistant 正在生成的,可能也没有ID
await customAlert('无法删除未同步的消息,请刷新页面后重试。');
return;
}
try {
const res = await fetch(`/AiChat/DeleteMessage?messageId=${messageId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result.success) {
messageEl.remove();
} else {
await customAlert(result.msg || '删除失败');
}
} catch (err) {
console.error('删除消息失败:', err);
await customAlert('删除失败: ' + err.message);
}
});
}
});
;
// ============ 聊天逻辑 ============
function scrollToBottom() {
if (chatArea) {
setTimeout(() => {
chatArea.scrollTop = chatArea.scrollHeight;
}, 100);
}
}
function renderMarkdown(md) {
try {
return DOMPurify.sanitize(marked.parse(md));
} catch (e) {
return DOMPurify.sanitize(md);
}
}
function addMessage(role, markdownContent) {
if (!chatArea) return null;
const messageEl = document.createElement('div');
messageEl.className = `message ${role}`;
const contentEl = document.createElement('div');
contentEl.className = 'message-content markdown-body';
contentEl.innerHTML = renderMarkdown(markdownContent || '');
// 添加操作栏 (复制 & 删除)
const actionRow = document.createElement('div');
actionRow.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'btn-copy-markdown';
copyBtn.title = '复制原始内容';
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
const rawContent = document.createElement('div');
rawContent.className = 'raw-markdown';
rawContent.style.display = 'none';
rawContent.textContent = markdownContent || '';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-delete-message';
deleteBtn.title = '删除消息';
deleteBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
actionRow.appendChild(copyBtn);
actionRow.appendChild(rawContent);
actionRow.appendChild(deleteBtn);
contentEl.appendChild(actionRow);
const timestamp = document.createElement('div');
timestamp.className = 'timestamp';
timestamp.textContent = new Date().toLocaleString();
contentEl.appendChild(timestamp);
const avatarEl = document.createElement('div');
avatarEl.className = 'avatar';
avatarEl.textContent = role === 'user' ? '👤' : (role === 'assistant' ? '🤖' : '⚠');
messageEl.appendChild(contentEl);
messageEl.appendChild(avatarEl);
chatArea.appendChild(messageEl);
highlightCodeBlocks(contentEl);
scrollToBottom();
return contentEl;
}
function setConnectionStatus(connected) {
isConnected = connected;
if (statusEl) {
statusEl.textContent = connected ? '已连接' : '未连接';
statusEl.className = `status ${connected ? 'connected' : 'disconnected'}`;
}
updateInputState();
}
function updateInputState() {
if (!messageInput || !sendBtn) return;
const canInput = isConnected && !isLoading;
messageInput.disabled = !canInput;
sendBtn.disabled = !canInput || messageInput.value.trim() === '';
if (modelSelect) {
modelSelect.disabled = isLoading;
}
}
async function initConnection() {
connection = new signalR
.HubConnectionBuilder()
.withUrl(config.hubUrl)
.withAutomaticReconnect([0, 0, 1000, 3000, 5000, 10000])
.build();
connection.on('NewSessionCreated', (sessionId) => {
console.log('✅ 新会话已创建,SessionId:', sessionId);
// 更新隐藏域
const currentSessionIdEl = document.getElementById('currentSessionId');
if (currentSessionIdEl) currentSessionIdEl.value = sessionId;
if (pendingSessionElement) {
pendingSessionElement.dataset.isNewSession = 'false';
pendingSessionElement.dataset.sessionId = sessionId;
pendingSessionElement.classList.remove('pending');
pendingSessionElement.classList.add('active');
const sessionNameEl = pendingSessionElement.querySelector('.session-name');
if (sessionNameEl) {
sessionNameEl.textContent = '生成标题中...';
sessionNameEl.style.fontStyle = 'italic';
}
// 更新事件监听
const sessionInfo = pendingSessionElement.querySelector('.session-info');
if (sessionInfo) {
const newSessionInfo = sessionInfo.cloneNode(true);
sessionInfo.parentNode.replaceChild(newSessionInfo, sessionInfo);
newSessionInfo.addEventListener('click', async () => {
await switchSession(sessionId);
});
}
}
});
connection.on('SessionNameGenerated', (sessionId) => {
console.log('✅ 会话名称已生成,SessionId:', sessionId);
refreshSessionList();
pendingSessionElement = null;
});
connection.on('StreamDelta', (delta) => {
if (currentAssistantMessage) {
// 判断是否在底部 (在内容更新前)
const threshold = 100;
const isAtBottom = chatArea ? (chatArea.scrollHeight - chatArea.scrollTop - chatArea.clientHeight < threshold) : true;
currentAssistantRaw += delta;
// 保存现有的操作栏和 timestamp,避免被 innerHTML 覆盖
const actionRow = currentAssistantMessage.querySelector('.message-actions');
const ts = currentAssistantMessage.querySelector('.timestamp');
// rawEl 在 actionRow 内部
let rawEl = actionRow ? actionRow.querySelector('.raw-markdown') : null;
// 重新渲染 Markdown 内容
const html = renderMarkdown(currentAssistantRaw);
currentAssistantMessage.innerHTML = html;
// 更新 raw markdown 内容
if (rawEl) {
rawEl.textContent = currentAssistantRaw;
}
// 恢复操作栏
if (actionRow) {
currentAssistantMessage.appendChild(actionRow);
}
// 恢复或新建 timestamp
if (ts) {
currentAssistantMessage.appendChild(ts);
} else {
let newTs = document.createElement('div');
newTs.className = 'timestamp';
newTs.textContent = new Date().toLocaleString();
currentAssistantMessage.appendChild(newTs);
}
highlightCodeBlocks(currentAssistantMessage);
if (isAtBottom) {
scrollToBottom();
}
}
});
connection.on('StreamCompleted', () => {
isLoading = false;
if (cancelBtn) {
cancelBtn.remove();
cancelBtn = null;
}
if (currentAssistantMessage) {
highlightCodeBlocks(currentAssistantMessage);
}
currentAssistantMessage = null;
currentAssistantRaw = '';
updateInputState();
refreshSessionList();
});
connection.on('StreamCancelled', () => {
isLoading = false;
if (cancelBtn) {
cancelBtn.remove();
cancelBtn = null;
}
currentAssistantMessage = null;
currentAssistantRaw = '';
updateInputState();
addMessage('assistant', '已取消生成');
});
connection.on('SystemError', (errorData) => {
isLoading = false;
if (cancelBtn) {
cancelBtn.remove();
cancelBtn = null;
}
currentAssistantMessage = null;
currentAssistantRaw = '';
addMessage('error', (errorData && errorData.content) || '发生错误');
updateInputState();
});
connection.onreconnecting(err => setConnectionStatus(false));
connection.onreconnected(id => setConnectionStatus(true));
connection.onclose(err => {
setConnectionStatus(false);
setTimeout(() => initConnection(), 5000);
});
try {
await connection.start();
setConnectionStatus(true);
console.log('📡 SignalR 连接已建立');
} catch (err) {
console.error('❌ 连接失败:', err);
setConnectionStatus(false);
setTimeout(() => initConnection(), 5000);
}
}
async function sendMessage() {
const content = messageInput.value.trim();
if (!content || !isConnected) return;
let selectedModel = 1;
if (modelSelect) {
selectedModel = parseInt(modelSelect.value) || 1;
}
addMessage('user', content);
messageInput.value = '';
isLoading = true;
updateInputState();
const responseEl = addMessage('assistant', '');
currentAssistantMessage = responseEl; // 这里的 responseEl 是 .message-content
currentAssistantRaw = '';
const currentSessionId = getCurrentSessionId();
// 预置加载动画 - 插入到 contentEl 末尾,但在按钮之前
const loader = document.createElement('div');
loader.className = 'loading-dots';
loader.innerHTML = '<span></span><span></span><span></span>';
responseEl.appendChild(loader);
// 获取操作栏容器
let actionRow = responseEl.querySelector('.message-actions');
if (!actionRow) {
actionRow = document.createElement('div');
actionRow.className = 'message-actions';
responseEl.appendChild(actionRow);
}
cancelBtn = document.createElement('button');
cancelBtn.className = 'cancel-btn';
cancelBtn.title = '取消生成';
cancelBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect></svg>';
cancelBtn.onclick = async () => {
try {
const response = await fetch(`/AiChat/CancelMessage?sessionId=${currentSessionId}&connectionId=${connection.connectionId}`, {method: 'POST'});
if (!response.ok) {
console.error('❌ 取消请求失败');
}
} catch (err) {
console.error('❌ 取消请求失败:', err);
}
};
// 插入取消按钮到最前面
actionRow.insertBefore(cancelBtn, actionRow.firstChild);
responseEl.appendChild(actionRow);
try {
await connection.invoke('SendStreamMessageToChatGpt', content, selectedModel, currentSessionId || '');
loader.remove();
} catch (err) {
console.error('❌ 发送失败:', err);
isLoading = false;
currentAssistantMessage = null;
updateInputState();
loader.remove();
if (cancelBtn) {
cancelBtn.remove();
cancelBtn = null;
}
// 如果 actionRow 空了,也可以移除它,但这通常发生在 StreamCompleted 之后
addMessage('error', `错误: ${err.message}`);
}
}
// ============ 会话管理 ============
async function createNewSession() {
// 检查是否已存在待创建的会话
const existingPending = document.querySelector('.session-item.pending');
if (existingPending) {
await customAlert('已有待创建的新会话,请先发送消息完成创建', '提示');
// 激活现有的待创建会话
document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
existingPending.classList.add('active');
return;
}
try {
await switchSession(''); // 切换到空会话
// 移除所有会话选中状态
document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
// 在列表顶部添加临时会话
const tempSessionId = 'temp_' + Date.now();
const tempSession = document.createElement('div');
tempSession.className = 'session-item pending active';
tempSession.dataset.sessionId = tempSessionId;
tempSession.dataset.isNewSession = 'true';
tempSession.innerHTML = `
<div class="session-info">
<div class="session-name">新建会话...</div>
<div class="session-meta">
<span class="session-time">${new Date().toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<span class="session-count">0 条</span>
</div>
</div>
`;
tempSession.querySelector('.session-info').addEventListener('click', async () => {
// 再次点击临时会话,如果它还是临时的,就当做新建会话处理
if (tempSession.dataset.isNewSession === 'true') {
// 仅仅是切回视图,不重新创建(因为ID还没生成)
// 但这里逻辑有点复杂,简化为:如果点击了pending会话,且当前不在该会话(虽然ID不对),则恢复空状态
if (getCurrentSessionId() !== '') {
await switchSession('');
document.querySelectorAll('.session-item').forEach(i => i.classList.remove('active'));
tempSession.classList.add('active');
}
}
});
if (sessionList) {
sessionList.insertBefore(tempSession, sessionList.firstChild);
}
pendingSessionElement = tempSession;
if (window.innerWidth <= 600) {
sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
} catch (err) {
console.error('❌ 创建会话异常:', err);
}
}
async function deleteSession(sessionId) {
if (!await customConfirm('确定要删除这个会话吗?')) {
return;
}
try {
const res = await fetch(`/AiChat/DeleteSession?sessionId=${sessionId}`, {method: 'DELETE'});
const result = await res.json();
if (result.success) {
// 如果删除的是当前会话,刷新列表并尝试切换到第一个,或新建
const isCurrent = (sessionId === getCurrentSessionId());
await refreshSessionList();
if (isCurrent) {
const first = document.querySelector('.session-item');
if (first) {
await switchSession(first.dataset.sessionId);
} else {
await createNewSession();
}
}
} else {
await customAlert(result["msg"] || '删除失败');
}
} catch (err) {
await customAlert('删除失败: ' + err.message);
}
}
async function switchSession(sessionId) {
// 如果切换到当前会话,直接返回
const currentSessionId = getCurrentSessionId();
if (sessionId && sessionId === currentSessionId) {
console.log('⚠️ 已经在当前会话中,无需切换');
// 关闭移动端侧边栏
if (window.innerWidth <= 600) {
sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
return;
}
console.log('🔄 切换会话:', sessionId);
try {
// 请求分部视图
const res = await fetch(`/AiChat/ChatMessages?sessionId=${sessionId || ''}`);
if (!res.ok) {
await customAlert('网络错误');
return;
}
const html = await res.text();
const chatMain = document.getElementById('chatMain');
if (chatMain) {
chatMain.innerHTML = html;
// 更新所有 DOM 引用并重新绑定事件
updateDomReferences();
}
// 更新左侧列表选中状态
if (sessionId) {
document.querySelectorAll('.session-item').forEach(item => {
item.classList.toggle('active', item.dataset.sessionId === sessionId);
});
}
} catch (err) {
console.error('切换会话失败:', err);
await customAlert('切换会话失败');
}
if (window.innerWidth <= 600) {
sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
}
}
async function refreshSessionList() {
try {
const response = await fetch('/AiChat/Sessions');
const result = await response.json();
if (result.success && result.data) {
renderSessionList(result.data);
}
} catch (err) {
console.error('刷新会话列表失败:', err);
}
}
function renderSessionList(sessions) {
if (!sessionList) return;
const pending = pendingSessionElement; // 保留 pending 引用
sessionList.innerHTML = '';
if (pending) sessionList.appendChild(pending);
// 如果 pending 已经有真实 ID,检查是否在 sessions 中
if (pending && pending.dataset.sessionId && !pending.dataset.sessionId.startsWith('temp_')) {
const realId = pending.dataset.sessionId;
const matched = sessions.find(s => s.sessionId === realId);
if (matched) {
// 更新 pending 元素内容
const nameEl = pending.querySelector('.session-name');
const timeEl = pending.querySelector('.session-time');
const countEl = pending.querySelector('.session-count');
if (nameEl) {
nameEl.textContent = matched['sessionName'];
nameEl.title = matched['sessionName'];
nameEl.style.fontStyle = 'normal';
}
if (timeEl) timeEl.textContent = new Date(matched['lastUpdateTime']).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
if (countEl) countEl.textContent = `${matched['messageCount']} 条`;
pending.classList.remove('pending');
// 添加删除按钮
if (!pending.querySelector('.btn-delete-session')) {
const btn = document.createElement('button');
btn.className = 'btn-delete-session';
btn.textContent = '🗑️';
btn.title = '删除会话';
btn.onclick = async (e) => {
e.stopPropagation();
await deleteSession(matched.sessionId);
};
pending.appendChild(btn);
}
// 从 sessions 中过滤掉
sessions = sessions.filter(s => s.sessionId !== realId);
}
}
sessions.forEach(session => {
const div = document.createElement('div');
div.className = `session-item ${session.sessionId === getCurrentSessionId() ? 'active' : ''}`;
div.dataset.sessionId = session.sessionId;
div.innerHTML = `
<div class="session-info">
<div class="session-name" title="${session['sessionName']}">${session['sessionName']}</div>
<div class="session-meta">
<span class="session-time">${new Date(session['lastUpdateTime']).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<span class="session-count">${session['messageCount']} 条</span>
</div>
</div>
<button class="btn-delete-session" title="删除会话">🗑️</button>
`;
div.querySelector('.session-info').onclick = () => switchSession(session.sessionId);
div.querySelector('.btn-delete-session').onclick = async (e) => {
e.stopPropagation();
await deleteSession(session.sessionId);
};
sessionList.appendChild(div);
});
}
function toggleSidebar(forceClose = false) {
const isMobile = window.innerWidth <= 600;
if (forceClose) {
if (isMobile) {
sessionSidebar.classList.remove('show');
if (sidebarOverlay) sidebarOverlay.classList.remove('show');
} else {
sessionSidebar.classList.add('collapsed');
}
return;
}
if (isMobile) {
sessionSidebar.classList.toggle('show');
if (sidebarOverlay) sidebarOverlay.classList.toggle('show');
} else {
sessionSidebar.classList.toggle('collapsed');
sessionSidebar.classList.toggle('show');
}
}
if (sidebarOverlay) sidebarOverlay.onclick = () => toggleSidebar(true);
if (btnNewSession) btnNewSession.onclick = createNewSession;
window.addEventListener('load', async () => {
updateDomReferences();
await initConnection();
// 初始绑定左侧列表事件(如果有服务器渲染的列表)
document.querySelectorAll('.session-item').forEach(item => {
const sid = item.dataset.sessionId;
if (item.dataset.isNewSession === 'true' && !sid) {
// 这是一个空的占位符(空列表时显示)
item.querySelector('.session-info').onclick = () => createNewSession();
} else {
item.querySelector('.session-info').onclick = () => switchSession(sid);
const delBtn = item.querySelector('.btn-delete-session');
if (delBtn) delBtn.onclick = (e) => {
e.stopPropagation();
deleteSession(sid);
};
}
});
});
window.addEventListener('beforeunload', async () => {
if (connection) {
await connection.stop();
}
});