// AI助手主入口模块

import {initMobileViewport, initMobileInputHandling} from './mobile-utils.js';
import {initModal} from './modal.js';
import {updateDomReferences as updateDomReferencesUtil, getCurrentSessionId, scrollToBottom} from './dom-utils.js';
import {highlightCodeBlocks} from './highlight.js';
import {initConnection, setConnectionStatus, getIsConnected, getConnection, getIsLoading} from './connection.js';
import {sendMessage, onModelChange} from './chat-handler.js';
import {customAlert} from './modal.js';
import {
    createNewSession,
    deleteSession,
    switchSession,
    refreshSessionList,
    toggleSidebar,
    getPendingSessionElement    
} from './session.js';
import {initMessageActions} from './message-actions.js';

// 初始化移动端工具
initMobileViewport();
initMobileInputHandling();

// 初始化模态框
initModal();

// 初始化消息操作
initMessageActions();

// DOM 元素引用
let chatArea, messageInput, sendBtn, statusEl, modelSelect, sidebarToggle;
const sessionList = document.getElementById('sessionList');
const btnNewSession = document.getElementById('btnNewSession');
const sessionSidebar = document.getElementById('sessionSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');

// 更新DOM引用
function updateDomRefs() {
    const refs = updateDomReferencesUtil();
    chatArea = refs.chatArea;
    messageInput = refs.messageInput;
    sendBtn = refs.sendBtn;
    statusEl = refs.statusEl;
    modelSelect = refs.modelSelect;
    sidebarToggle = refs.sidebarToggle;
    bindChatEvents(refs);

    if (statusEl) {
        setConnectionStatus(getIsConnected(), statusEl, () => updateInputState(refs));
    }

    // 异步高亮代码块
    highlightCodeBlocks(chatArea).catch(err => {
        console.warn('代码高亮失败:', err);
    });
    scrollToBottom(chatArea);
}

// 更新输入状态
function updateInputState(domRefs) {
    const connection = getConnection();
    const isConnected = connection && connection.state === signalR.HubConnectionState.Connected;
    const isLoading = getIsLoading();

    // 如果传入了 domRefs,使用传入的引用;否则使用闭包变量
    const input = domRefs?.messageInput || messageInput;
    const btn = domRefs?.sendBtn || sendBtn;
    const select = domRefs?.modelSelect || modelSelect;

    if (!input || !btn) return;
    const canInput = isConnected && !isLoading;
    input.disabled = !canInput;
    btn.disabled = !canInput || input.value.trim() === '';
    if (select) {
        select.disabled = isLoading;
    }
}

// 绑定聊天事件
function bindChatEvents(domRefs) {
    // 如果传入了 domRefs,使用传入的引用;否则使用闭包变量
    const input = domRefs?.messageInput || messageInput;
    const btn = domRefs?.sendBtn || sendBtn;
    const select = domRefs?.modelSelect || modelSelect;
    const toggle = domRefs?.sidebarToggle || sidebarToggle;
    const area = domRefs?.chatArea || chatArea;

    if (input) {
        input.onkeydown = async e => {
            if ((e.ctrlKey && e.key === 'Enter') || (e.altKey && e.key === 's')) {
                e.preventDefault();
                await sendMessage(input, select, area, () => updateInputState(domRefs));
            }
        };
        input.oninput = () => updateInputState(domRefs);
    }
    if (btn) {
        btn.onclick = async () => {
            await sendMessage(input, select, area, () => updateInputState(domRefs));
        };
    }
    if (select) {
        select.onchange = async () => {
            await onModelChange(select, getCurrentSessionId, customAlert);
        };
    }
    if (toggle) {
        toggle.onclick = () => toggleSidebar();
    }
    // 使用传入的 domRefs 更新输入状态
    updateInputState(domRefs);
}

// 初始化连接
async function init() {
    updateDomRefs();

    // 创建一个包装函数,确保使用最新的 DOM 引用
    const refreshSessionListWrapper = () => {
        refreshSessionList(
            (sessionId) => switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState),
            updateDomReferencesUtil,
            bindChatEvents,
            setConnectionStatus,
            getIsConnected,
            updateInputState
        );
    };


    // 创建一个包装函数,使用最新的 DOM 引用
    const updateInputStateWrapper = () => {
        const refs = updateDomReferencesUtil();
        updateInputState(refs);
    };

    // 初始化连接(initConnection 会创建 connection,这里 connection 可能为 null)
    await initConnection(chatArea, statusEl, updateInputStateWrapper, refreshSessionListWrapper, getCurrentSessionId);

    // 连接初始化后,重新获取 connection 并设置事件处理器
    const connectionAfterInit = getConnection();
    if (connectionAfterInit) {
        // 重新设置 NewSessionCreated 事件处理器(覆盖 connection.js 中的简单处理器)
        connectionAfterInit.off('NewSessionCreated');
        connectionAfterInit.on('NewSessionCreated', (sessionId) => {
            console.log('✅ 新会话已创建,SessionId:', sessionId);
            const currentSessionIdEl = document.getElementById('currentSessionId');
            if (currentSessionIdEl) currentSessionIdEl.value = sessionId;

            const pendingSessionElement = getPendingSessionElement();
            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, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState);
                    });
                }
            }
        });

        // 重新设置 SessionNameGenerated 事件处理器
        connectionAfterInit.off('SessionNameGenerated');
        connectionAfterInit.on('SessionNameGenerated', async (data) => {
            let sessionId, sessionName;
            if (typeof data === 'object' && data !== null) {
                sessionId = data.sessionId;
                sessionName = data.sessionName;
            } else {
                sessionId = data;
            }

            console.log('✅ 会话名称已生成,SessionId:', sessionId, 'SessionName:', sessionName);
            // 动态导入 updateSessionTitle 函数
            const { updateSessionTitle } = await import('./session.js');
            
            // 创建 switchSession 的包装函数
            const switchSessionWrapper = (sessionId) => {
                return switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState);
            };
            
            // 尝试直接更新标题,传入 sessionName
            const updated = await updateSessionTitle(sessionId, switchSessionWrapper, refreshSessionListWrapper, sessionName);
            if (!updated) {
                // 如果直接更新失败,则刷新整个列表
                refreshSessionListWrapper();
            }
        });
    }

    // 初始绑定左侧列表事件
    document.querySelectorAll('.session-item').forEach(item => {
        const sid = item.dataset.sessionId;
        if (item.dataset.isNewSession === 'true' && !sid) {
            item.querySelector('.session-info').onclick = () => createNewSession(sessionList, sessionSidebar, sidebarOverlay, (sessionId) => switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState));
        } else {
            item.querySelector('.session-info').onclick = () => switchSession(sid, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState);
            const delBtn = item.querySelector('.btn-delete-session');
            if (delBtn) delBtn.onclick = (e) => {
                e.stopPropagation();
                deleteSession(sid, (sessionId) => switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState), refreshSessionListWrapper);
            };
        }
    });
}

if (sidebarOverlay) sidebarOverlay.onclick = () => toggleSidebar(true);
if (btnNewSession) btnNewSession.onclick = () => createNewSession(sessionList, sessionSidebar, sidebarOverlay, (sessionId) => switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, setConnectionStatus, getIsConnected(), updateInputState));

window.addEventListener('load', init);

window.addEventListener('beforeunload', async () => {
    const connection = getConnection();
    if (connection) {
        await connection.stop();
    }
});

⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

下面按模块和流程来说明这段代码的功能与工作方式,重点解释每个部分在应用中的作用与交互关系。

总体概述

  • 这是单页聊天应用的主入口脚本。它负责初始化页面相关的工具、建立/管理与后端(SignalR)连接、绑定 UI 事件、管理会话(session)列表以及协调各子模块(如消息发送、模态框、代码高亮等)。
  • 代码通过大量的工具模块(mobile-utils、modal、dom-utils、highlight、connection、chat-handler、session、message-actions 等)来实现拆分职责。

初始化阶段

  • 引入并立即初始化移动端适配/输入处理:initMobileViewport(), initMobileInputHandling()。
  • 初始化模态框(initModal)和消息相关操作(initMessageActions)。
  • 异步代码高亮与滚动(highlightCodeBlocks、scrollToBottom)用于在切换/加载聊天内容后美化并滚动到底部。

DOM 引用与更新

  • 定义一组闭包变量(chatArea, messageInput, sendBtn, statusEl, modelSelect, sidebarToggle)来缓存重要 DOM 节点。
  • updateDomRefs() 调用 updateDomReferencesUtil()(来自 dom-utils)来获取最新的 DOM 引用,并赋给闭包变量,然后调用 bindChatEvents(refs) 绑定事件。
  • 在 updateDomRefs 中也会根据当前连接状态设置连接指示(setConnectionStatus),并异步高亮聊天区代码块、将聊天区滚动到底部。

输入状态管理(updateInputState)

  • 负责启/禁用输入框、发送按钮和模型选择下拉(select),以防止在未连接或正在加载时发送请求。
  • 根据 getConnection() 返回的 connection.state(与 signalR.HubConnectionState.Connected 比较)和 getIsLoading() 状态来决定能否输入。
  • 接受可选参数 domRefs:如果提供就使用传入的引用(方便在切换会话/刷新 DOM 后使用最新节点),否则使用闭包中的引用。

事件绑定(bindChatEvents)

  • 为输入框绑定键盘事件:Ctrl+Enter 或 Alt+S 触发 sendMessage。
  • 为输入框绑定 input 事件,以实时更新发送按钮状态(通过 updateInputState)。
  • 为发送按钮绑定点击事件,调用 sendMessage。
  • modelSelect 的 onchange 调用 onModelChange,传入 getCurrentSessionId 与 customAlert。
  • sidebarToggle 绑定侧边栏切换。
  • 绑定完毕后调用 updateInputState(domRefs) 以同步控件状态。

连接初始化与事件处理(init)

  • init() 首先更新 DOM 引用(updateDomRefs)。
  • 构造两个包装函数:
    • refreshSessionListWrapper:包装 refreshSessionList,传入一系列回调(switchSession 等),保证在刷新列表时能使用最新的 DOM 和逻辑。
    • updateInputStateWrapper:在需要时重新读取最新 DOM 引用并调用 updateInputState。
  • 调用 initConnection(chatArea, statusEl, updateInputStateWrapper, refreshSessionListWrapper, getCurrentSessionId)。initConnection 会建立与后端(SignalR)的连接,可能创建 connection 对象或延迟创建。
  • initConnection 完成后,通过 getConnection() 取到 connection 并重新设置/覆盖两个重要的 SignalR 事件处理器:
    • NewSessionCreated:
      • 当后端发来新会话 ID 时,更新当前会话 ID 的隐藏元素。
      • 查找一个“候选的 pending session 元素”,把它标记为已创建(移除 pending、添加 active),设置 data-sessionId,并把会话名字置为“生成标题中...”(斜体);
      • 重新为该会话项的 .session-info 绑定点击事件,点击时切换到该会话(调用 switchSession)。
    • SessionNameGenerated:
      • 当服务端生成会话标题时,接收 data(可能为对象或直接为 sessionId)。
      • 动态 import('./session.js') 获取 updateSessionTitle(按需加载)。
      • 构造 switchSessionWrapper 并尝试用 updateSessionTitle 更新会话标题;如果失败则刷新会话列表(refreshSessionListWrapper)。
  • 这些操作保证了当后端异步创建/命名会话时,前端能以动态、即时的方式更新列表与 UI。

左侧会话列表的初始绑定

  • 在 init() 的后面,遍历 document.querySelectorAll('.session-item'):
    • 如果该项是“新会话占位”(data-is-new-session === 'true' 且无 sessionId),为其 .session-info 绑定 createNewSession 的调用(打开侧边栏创建新会话)。
    • 否则为其 .session-info 绑定 switchSession(传入 sessionId 与一系列更新/回调函数),并为删除按钮绑定 deleteSession(防止冒泡,并在删除后切换或刷新列表)。
  • 这些点击回调通常会调用 switchSession(sessionId, document.getElementById('chatMain'), updateDomReferencesUtil, bindChatEvents, ... ),保证切换会话时重新抓取 DOM 并重新绑定事件。

侧边栏和新会话按钮

  • sidebarOverlay 点击会触发 toggleSidebar(true)(用于在移动端关闭侧边栏)。
  • btnNewSession 点击会触发 createNewSession(...) 并在创建完成后切换到新会话。

页面卸载时的清理

  • 在 window.beforeunload 中,如果存在 connection,则 await connection.stop(),以优雅断开 SignalR 连接。

其他交互点与关键函数

  • sendMessage(input, select, area, cb)(来自 chat-handler.js):负责发送消息并在发送后通过回调(如 updateInputState)更新 UI。
  • onModelChange(select, getCurrentSessionId, customAlert):当模型切换时处理相关逻辑,可能提示用户或调整会话元数据。
  • updateDomReferencesUtil() 是中心函数,用来在 DOM 结构发生变化(如切换会话或加载新会话内容)时重新抓取需要的元素,保证事件处理器和状态检查使用到最新节点。
  • highlightCodeBlocks(chatArea):异步执行代码块高亮,若失败会打印警告。

总结(行为流程示例)

  • 页面加载(window load) -> init():
    • 更新 DOM 引用并绑定事件;
    • 建立/初始化后端连接并为后端事件(新会话创建/会话命名完成)注册处理;
    • 初始化左侧会话列表的点击与删除事件;
    • 点击发送/按快捷键触发 sendMessage;
    • 切换模型或会话时,使用一套传入 updateDomReferencesUtil 和 bindChatEvents 的包装回调,确保事件始终与当前 DOM 同步。
  • 在关闭页面前,会主动断开 SignalR 连接。

如果你需要,我可以进一步:

  • 逐行解释某个函数内部(例如 sendMessage、switchSession、refreshSessionList)的实现逻辑;
  • 给出可能的改进建议(例如减少重复传入大量回调、避免某些潜在的 race condition 等)。
评论加载中...