const state = { runtime: null, knowledgeBases: [], activeKnowledgeBaseId: null, conversationId: null, isSending: false, }; const elements = { kbList: document.getElementById("knowledge-base-list"), refreshKbs: document.getElementById("refresh-kbs"), importKb: document.getElementById("import-kb"), importPath: document.getElementById("import-path"), importSubmit: document.getElementById("import-submit"), importStatus: document.getElementById("import-status"), runtimeMode: document.getElementById("runtime-mode"), runtimeModel: document.getElementById("runtime-model"), threadTitle: document.getElementById("thread-title"), threadSubtitle: document.getElementById("thread-subtitle"), activeKbBadge: document.getElementById("active-kb-badge"), newThread: document.getElementById("new-thread"), messageList: document.getElementById("message-list"), composer: document.getElementById("composer"), sendMessage: document.getElementById("send-message"), }; function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } async function fetchJson(url, options) { const response = await fetch(url, options); if (!response.ok) { const text = await response.text(); throw new Error(text || `HTTP ${response.status}`); } return response.json(); } async function bootstrap() { const [runtime, kbPayload] = await Promise.all([ fetchJson("/api/runtime"), fetchJson("/api/knowledge-bases"), ]); state.runtime = runtime; state.knowledgeBases = kbPayload.knowledge_bases || []; state.activeKnowledgeBaseId = runtime.default_knowledge_base_id || kbPayload.default_knowledge_base_id || state.knowledgeBases[0]?.knowledge_base_id || null; renderRuntime(); renderKnowledgeBases(); updateThreadHeader(); } function renderRuntime() { elements.runtimeMode.textContent = state.runtime?.mode || "unknown"; elements.runtimeModel.textContent = state.runtime?.model_name || "offline"; } function renderKnowledgeBases() { const cards = state.knowledgeBases; elements.kbList.innerHTML = ""; if (!cards.length) { elements.kbList.innerHTML = '
还没有知识库,先从上面导入一份会议文本。
'; return; } for (const kb of cards) { const button = document.createElement("button"); button.type = "button"; button.className = `kb-item${kb.knowledge_base_id === state.activeKnowledgeBaseId ? " active" : ""}`; button.innerHTML = `
${escapeHtml(kb.title)}
最近会议:${escapeHtml(kb.last_meeting_date || "暂无")} · ${kb.meeting_count} 次导入
指标 ${kb.counts.metrics} · 待办 ${kb.counts.actions} · 风险 ${kb.counts.risks}
${escapeHtml((kb.latest_summary || []).join(" "))}
`; button.addEventListener("click", async () => { state.activeKnowledgeBaseId = kb.knowledge_base_id; state.conversationId = null; renderKnowledgeBases(); updateThreadHeader(); appendSystemMessage(`已切换到知识库“${kb.knowledge_base_id}”。`); await ensureConversation(); }); elements.kbList.appendChild(button); } } function updateThreadHeader() { const active = state.knowledgeBases.find((item) => item.knowledge_base_id === state.activeKnowledgeBaseId); if (!active) { elements.threadTitle.textContent = "选择一个知识库开始提问"; elements.threadSubtitle.textContent = "左侧选择知识库,右侧像 nanobot 一样按对话线程提问。"; elements.activeKbBadge.textContent = "未选择知识库"; return; } elements.threadTitle.textContent = active.title; elements.threadSubtitle.textContent = `最近会议:${active.last_meeting_date || "暂无"},待办 ${active.counts.actions},风险 ${active.counts.risks}`; elements.activeKbBadge.textContent = `知识库:${active.knowledge_base_id}`; elements.importKb.value = active.knowledge_base_id; } function clearHeroCard() { const hero = elements.messageList.querySelector(".hero-card"); if (hero) { hero.remove(); } } function appendMessage(role, text) { clearHeroCard(); const article = document.createElement("article"); article.className = `message ${role}`; article.innerHTML = `
${role === "user" ? "我" : "AI"}
${escapeHtml(text)}
`; elements.messageList.appendChild(article); elements.messageList.scrollTop = elements.messageList.scrollHeight; return article; } function appendSystemMessage(text) { appendMessage("assistant", text); } async function ensureConversation() { if (state.conversationId) { return state.conversationId; } const payload = await fetchJson("/api/conversations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ knowledge_base_id: state.activeKnowledgeBaseId }), }); state.conversationId = payload.conversation_id; return state.conversationId; } async function refreshKnowledgeBases() { const kbPayload = await fetchJson("/api/knowledge-bases"); state.knowledgeBases = kbPayload.knowledge_bases || []; if (!state.activeKnowledgeBaseId && state.knowledgeBases[0]) { state.activeKnowledgeBaseId = state.knowledgeBases[0].knowledge_base_id; } renderKnowledgeBases(); updateThreadHeader(); } async function importTranscript() { const knowledgeBaseId = elements.importKb.value.trim(); const filePath = elements.importPath.value.trim(); if (!knowledgeBaseId || !filePath) { elements.importStatus.textContent = "请输入知识库 ID 和文本路径。"; return; } elements.importStatus.textContent = "正在导入..."; try { const payload = await fetchJson("/api/knowledge-bases/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ knowledge_base_id: knowledgeBaseId, file_path: filePath, }), }); elements.importStatus.textContent = `导入完成:${payload.knowledge_base_id},meeting_id=${payload.meeting_id}`; state.activeKnowledgeBaseId = payload.knowledge_base_id; state.conversationId = null; await refreshKnowledgeBases(); appendSystemMessage(`已导入到知识库“${payload.knowledge_base_id}”,现在可以直接提问。`); } catch (error) { elements.importStatus.textContent = String(error.message || error); } } async function sendMessage() { const message = elements.composer.value.trim(); if (!message || state.isSending) { return; } state.isSending = true; elements.sendMessage.disabled = true; appendMessage("user", message); elements.composer.value = ""; const assistantCard = appendMessage("assistant", "正在思考..."); const body = assistantCard.querySelector(".message-body"); const activityNode = document.createElement("div"); activityNode.className = "message-activity"; body.appendChild(activityNode); try { const conversationId = await ensureConversation(); const response = await fetch(`/api/conversations/${conversationId}/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, knowledge_base_id: state.activeKnowledgeBaseId, }), }); if (!response.ok || !response.body) { throw new Error((await response.text()) || `HTTP ${response.status}`); } let finalText = ""; const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); const chunks = buffer.split("\n\n"); buffer = chunks.pop() || ""; for (const chunk of chunks) { const line = chunk.split("\n").find((item) => item.startsWith("data: ")); if (!line) { continue; } const payload = JSON.parse(line.slice(6)); if (payload.type === "meta" && payload.knowledge_base_id) { state.activeKnowledgeBaseId = payload.knowledge_base_id; updateThreadHeader(); renderKnowledgeBases(); } if (payload.type === "tool_call") { activityNode.textContent = `工具调用:${payload.tool_name}`; } if (payload.type === "tool_result") { activityNode.textContent = `工具已返回:${payload.tool_name}`; } if (payload.type === "final") { finalText = payload.final_response || ""; body.textContent = finalText; if (activityNode.textContent) { body.appendChild(activityNode); } } } } if (!finalText) { body.textContent = "这次没有拿到有效回答。"; } await refreshKnowledgeBases(); } catch (error) { body.textContent = `请求失败:${error.message || error}`; } finally { state.isSending = false; elements.sendMessage.disabled = false; elements.messageList.scrollTop = elements.messageList.scrollHeight; } } elements.refreshKbs.addEventListener("click", () => { refreshKnowledgeBases().catch((error) => appendSystemMessage(`刷新失败:${error.message || error}`)); }); elements.importSubmit.addEventListener("click", () => { importTranscript().catch((error) => { elements.importStatus.textContent = String(error.message || error); }); }); elements.sendMessage.addEventListener("click", () => { sendMessage().catch((error) => appendSystemMessage(`发送失败:${error.message || error}`)); }); elements.newThread.addEventListener("click", async () => { state.conversationId = null; await ensureConversation(); appendSystemMessage("已新建一个对话线程。"); }); elements.composer.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { event.preventDefault(); sendMessage().catch((error) => appendSystemMessage(`发送失败:${error.message || error}`)); } }); bootstrap().catch((error) => { appendSystemMessage(`初始化失败:${error.message || error}`); });