// 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)。
- NewSessionCreated:
- 这些操作保证了当后端异步创建/命名会话时,前端能以动态、即时的方式更新列表与 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 等)。
评论加载中...