网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
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();
    }
});
loading