const $ = (selector) => document.querySelector(selector); const $$ = (selector) => document.querySelectorAll(selector); const state = { meetingId: null, meetings: [], templateName: "template1.md", templates: [], processing: false, guideBusy: false, resultEditMode: false, rightEditMode: false, processGuideEditMode: false, selectedTreeKey: "", rightResource: null, settingsDraft: null, }; const STORAGE_KEY = "meeting-workspace-preferences"; function loadPreferences() { try { const raw = window.localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } function savePreferences(partial) { const next = { ...loadPreferences(), ...partial }; window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } function applySavedLayout() { const prefs = loadPreferences(); const sidebar = $("#sidebar"); const resultPanel = $("#result-panel"); const templatePanel = $("#template-panel"); if (prefs.layout?.sidebarWidth) { sidebar.style.flexBasis = `${prefs.layout.sidebarWidth}px`; sidebar.style.flexGrow = "0"; } if (prefs.layout?.templateWidth) { templatePanel.style.flexBasis = `${prefs.layout.templateWidth}px`; templatePanel.style.flexGrow = "0"; } if (prefs.layout?.resultWidth) { resultPanel.style.flexBasis = `${prefs.layout.resultWidth}px`; resultPanel.style.flexGrow = "0"; } if (prefs.templateName) { state.templateName = prefs.templateName; } } function persistLayout() { savePreferences({ layout: { sidebarWidth: Math.round($("#sidebar").getBoundingClientRect().width), resultWidth: Math.round($("#result-panel").getBoundingClientRect().width), templateWidth: Math.round($("#template-panel").getBoundingClientRect().width), }, }); } function toast(message, type = "ok") { const el = $("#toast"); el.textContent = message; el.className = `toast ${type} show`; clearTimeout(el._timer); el._timer = setTimeout(() => el.classList.remove("show"), 2400); } function openModal(id) { document.getElementById(id).classList.add("show"); } function closeModal(id) { document.getElementById(id).classList.remove("show"); } function cloneSettingsDraft(draft) { return { active_api_profile_name: draft.active_api_profile_name || "", api_profiles: (draft.api_profiles || []).map((item) => ({ name: item.name, api_base_url: item.api_base_url, api_key: item.api_key, model_name: item.model_name, max_tokens: item.max_tokens, })), }; } async function api(url, options) { const res = await fetch(url, options || {}); if (!res.ok) { const detail = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(detail.detail || "Request failed"); } return res.json(); } function meetingById(meetingId) { return state.meetings.find((item) => item.id === meetingId) || null; } function patchMeeting(meetingId, partial) { state.meetings = state.meetings.map((item) => ( item.id === meetingId ? { ...item, ...partial } : item )); } function templateMetaByName(name) { return state.templates.find((item) => item.name === name) || null; } function isMarkdownFile(name = "") { return name.toLowerCase().endsWith(".md"); } function setStatus(side, isBusy, text = "空闲") { const light = $(`#${side}-status-light`); const textEl = $(`#${side}-status-text`); light.classList.toggle("idle", !isBusy); light.classList.toggle("busy", isBusy); textEl.textContent = text; } function renderMeetingStatus(meeting) { const name = $("#sidebar-meeting-name"); const meta = $("#sidebar-meeting-meta"); const tip = $("#selected-meeting-tip"); const summaryBadge = $("#badge-summary"); const topicsBadge = $("#badge-topics"); if (!meeting) { name.textContent = "当前会议:未选择"; meta.textContent = "请从左侧选择一个会议开始处理。"; tip.textContent = "未选择会议"; summaryBadge.textContent = "未生成总结"; topicsBadge.textContent = "未生成主题 JSON"; summaryBadge.className = "badge muted"; topicsBadge.className = "badge muted"; return; } name.textContent = `当前会议:${meeting.name}`; meta.textContent = `ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 原始文件:` + `${meeting.original_filename || meeting.transcript_filename || "未知"}`; tip.textContent = `当前处理会议:${meeting.name} (${meeting.id})`; summaryBadge.textContent = meeting.has_summary ? "已生成总结" : "未生成总结"; topicsBadge.textContent = meeting.has_topics ? "已生成主题 JSON" : "未生成主题 JSON"; summaryBadge.className = meeting.has_summary ? "badge" : "badge muted"; topicsBadge.className = meeting.has_topics ? "badge" : "badge muted"; } function refreshActionButtons() { const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy; const currentMeeting = meetingById(state.meetingId); $("#btn-process").disabled = !canProcess; $("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : currentMeeting?.has_summary ? "重总结" : "总结"; const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy; $("#btn-toggle-result-edit").disabled = !canEditResult; const resource = state.rightResource; const canEditSide = Boolean(resource?.editable) && !state.processing && !state.guideBusy; $("#btn-toggle-side-edit").disabled = !canEditSide; updateGuideButton(resource); } function setProcessGuideEditMode(editMode) { state.processGuideEditMode = editMode; const editor = $("#process-guide-editor"); const button = $("#btn-process-guide-edit"); editor.readOnly = !editMode; editor.classList.toggle("is-editing", editMode); button.textContent = editMode ? "保存" : "编辑"; } function syncProcessGuideConfirmLabel() { const meeting = meetingById(state.meetingId); $("#btn-process-guide-confirm").textContent = meeting?.has_summary ? "重总结" : "确认"; } function renderSettingsKeyOptions() { const draft = state.settingsDraft; const select = $("#cfg-key-select"); select.innerHTML = ""; (draft?.api_profiles || []).forEach((item) => { const option = document.createElement("option"); option.value = item.name; option.textContent = item.model_name || item.name; select.appendChild(option); }); if (draft?.active_api_profile_name) { select.value = draft.active_api_profile_name; } const current = draft?.api_profiles?.find((item) => item.name === draft.active_api_profile_name); $("#cfg-current-model").value = current?.model_name || ""; $("#cfg-max-tokens").value = current?.max_tokens || 64000; $("#btn-key-delete").disabled = (draft?.api_profiles?.length || 0) <= 1; } function loadSettingsDraft(cfg) { state.settingsDraft = cloneSettingsDraft(cfg); renderSettingsKeyOptions(); } async function persistSettingsDraft() { const payload = cloneSettingsDraft(state.settingsDraft || {}); if (!payload.api_profiles.length) { throw new Error("至少保留一个模型配置"); } await api("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } function showReparseGuidePrompt() { $("#reparse-guide-notes").value = ""; $("#reparse-guide-prompt").hidden = false; $("#reparse-guide-notes").focus(); } function hideReparseGuidePrompt() { $("#reparse-guide-prompt").hidden = true; } function resetReparseModalProgress() { $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; $("#reparse-stream-content").textContent = ""; } function openStandaloneReparseModal() { $("#reparse-modal-title").textContent = `模板说明补充 · ${state.templateName}`; $("#reparse-modal-subtitle").textContent = "可为空;会和系统解析模板一起用于重新解析"; $("#reparse-modal-notes").value = ""; $("#reparse-modal-form").hidden = false; $("#reparse-modal-progress").hidden = true; $("#btn-reparse-modal-cancel").disabled = false; $("#btn-reparse-modal-confirm").disabled = false; resetReparseModalProgress(); openModal("modal-reparse-guide"); $("#reparse-modal-notes").focus(); } function setStandaloneReparseProcessing(active) { $("#reparse-modal-form").hidden = active; $("#reparse-modal-progress").hidden = !active; $("#btn-reparse-modal-cancel").disabled = active; $("#btn-reparse-modal-confirm").disabled = active; } function pushStandaloneReparseLine(text) { const box = $("#reparse-stream-content"); const lines = box.textContent ? box.textContent.split("\n") : []; lines.push(text); box.textContent = lines.slice(-6).join("\n"); } async function streamTemplateGuideReparse(templateName, userNotes = "", handlers = {}) { const params = new URLSearchParams(); if (userNotes.trim()) { params.set("user_notes", userNotes.trim()); } const suffix = params.toString() ? `?${params.toString()}` : ""; const source = new EventSource( `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream${suffix}`, ); try { const result = await new Promise((resolve, reject) => { let contentAcc = ""; source.onmessage = (event) => { if (!event.data) { return; } const payload = JSON.parse(event.data); if (payload.type === "status") { handlers.onStatus?.(payload.data); return; } if (payload.type === "chunk") { const chunk = payload.data?.text || ""; if (chunk) { contentAcc += chunk; handlers.onChunk?.(chunk, contentAcc, payload.data); } return; } if (payload.type === "done") { resolve(payload.data || { name: templateName, content: contentAcc }); return; } if (payload.type === "error") { reject(new Error(payload.data || "解析失败")); } }; source.onerror = () => { reject(new Error("解析连接中断")); }; }); state.templates = state.templates.map((item) => ( item.name === templateName ? { ...item, has_guide: true } : item )); if (state.rightResource?.name === templateName && state.rightResource.type === "template") { state.rightResource.hasGuide = true; } await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); return result; } finally { source.close(); } } async function runStandaloneGuideReparseFlow() { const templateName = state.templateName; const userNotes = $("#reparse-modal-notes").value || ""; state.guideBusy = true; setStatus("right", true, "解析中"); setStandaloneReparseProcessing(true); resetReparseModalProgress(); pushStandaloneReparseLine(`模板:${templateName}`); pushStandaloneReparseLine("已提交重解析请求,正在准备说明..."); refreshActionButtons(); try { const params = new URLSearchParams(); if (userNotes.trim()) { params.set("user_notes", userNotes.trim()); } const source = new EventSource( `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream?${params.toString()}`, ); const result = await new Promise((resolve, reject) => { let contentAcc = ""; source.onmessage = (event) => { if (!event.data) { return; } const payload = JSON.parse(event.data); if (payload.type === "status") { $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; pushStandaloneReparseLine("已连接模型,开始解析..."); return; } if (payload.type === "chunk") { const chunk = payload.data?.text || ""; if (chunk) { contentAcc += chunk; $("#reparse-stream-content").textContent = contentAcc.replace(/\r\n/g, "\n").split("\n").slice(-8).join("\n"); } return; } if (payload.type === "done") { source.close(); resolve(payload.data || { name: templateName, content: contentAcc }); return; } if (payload.type === "error") { source.close(); reject(new Error(payload.data || "解析失败")); } }; source.onerror = () => { source.close(); reject(new Error("解析连接中断")); }; }); state.templates = state.templates.map((item) => ( item.name === templateName ? { ...item, has_guide: true } : item )); if (state.rightResource?.name === templateName && state.rightResource.type === "template") { state.rightResource.hasGuide = true; } await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); closeModal("modal-reparse-guide"); await openProcessGuideModal(); $("#process-guide-editor").value = result.content || ""; } finally { state.guideBusy = false; setStatus("right", false, "空闲"); setStandaloneReparseProcessing(false); refreshActionButtons(); } } async function refreshRightResourceAfterGuideReparse(templateName, content) { const resource = state.rightResource; if (!resource) { return; } const isTargetGuide = resource.type === "template-guide" && resource.templateName === templateName; const isTargetTemplate = resource.type === "template" && resource.name === templateName; if (!isTargetGuide && !isTargetTemplate) { return; } if (isTargetGuide) { resource.content = content; $("#side-editor").value = content; if (!state.rightEditMode) { renderSidePreview(resource); } return; } const treeKey = resource.treeKey || `file:template_guides/${templateName}`; await openTemplateGuide(templateName, treeKey); } function resetProcessingStream() { $("#stream-box").style.display = "none"; $("#stream-title").textContent = ""; $("#stream-content").textContent = ""; setStatus("left", false, "空闲"); } function showResultEmpty() { state.resultEditMode = false; $("#btn-toggle-result-edit").textContent = "编辑"; $("#result-editor").style.display = "none"; $("#result-md").style.display = "none"; $("#processing-indicator").hidden = true; $("#result-empty").hidden = false; refreshActionButtons(); } function setResultEditMode(editMode) { state.resultEditMode = editMode; $("#result-editor").style.display = editMode ? "block" : "none"; $("#result-md").style.display = editMode ? "none" : "block"; $("#btn-toggle-result-edit").textContent = editMode ? "保存" : "编辑"; } function showResult(markdown) { resetProcessingStream(); $("#processing-indicator").hidden = true; $("#result-empty").hidden = true; $("#result-editor").value = markdown; $("#result-md").innerHTML = marked.parse(markdown || ""); $("#result-md").scrollTop = 0; setResultEditMode(false); refreshActionButtons(); } function showProcessingView() { $("#result-empty").hidden = true; $("#result-editor").style.display = "none"; $("#result-md").style.display = "none"; $("#processing-indicator").hidden = false; } function setSelectedTreeKey(key) { state.selectedTreeKey = key; $$(".tree-row").forEach((row) => { row.classList.toggle("selected", row.dataset.nodeKey === key); row.classList.toggle("active-meeting", row.dataset.meetingId === state.meetingId); }); } async function setCurrentMeeting(meetingId) { const data = await api("/api/current-meeting", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ meeting_id: meetingId }), }); state.meetingId = data.active_meeting_id; renderMeetingStatus(data.meeting); setSelectedTreeKey(state.selectedTreeKey); refreshActionButtons(); } function renderNode(node, parent, depth) { const wrapper = document.createElement("div"); wrapper.className = "tree-node"; const row = document.createElement("div"); row.className = "tree-row"; row.style.paddingLeft = `${10 + depth * 18}px`; if (node.id) { row.dataset.meetingId = node.id; } if (node.type === "folder") { const arrow = document.createElement("span"); arrow.className = `arrow ${node.children?.length ? "" : "none"}`; arrow.textContent = "▸"; row.appendChild(arrow); const icon = document.createElement("span"); icon.className = "icon"; icon.textContent = node.id ? "📁" : "🗂"; row.appendChild(icon); const label = document.createElement("span"); label.className = "label"; label.textContent = node.name; row.appendChild(label); if (node.active) { const pin = document.createElement("span"); pin.className = "meeting-pin"; pin.textContent = "当前"; row.appendChild(pin); } if (node.id) { row.dataset.nodeKey = `meeting:${node.id}`; const del = document.createElement("span"); del.className = "del-btn"; del.textContent = "删除"; del.addEventListener("click", async (event) => { event.stopPropagation(); await deleteMeetingNode(node.id, node.delete_mode || "meeting"); }); row.appendChild(del); } else { row.dataset.nodeKey = `folder:${node.name}`; } row.addEventListener("click", async () => { const children = wrapper.querySelector(":scope > .tree-children"); if (children) { const isOpen = !children.classList.contains("open"); children.classList.toggle("open", isOpen); arrow.classList.toggle("expanded", isOpen); } if (node.id) { await selectMeeting(node.id); } else { setSelectedTreeKey(row.dataset.nodeKey); } }); } else { row.dataset.nodeKey = `file:${node.path}`; const spacer = document.createElement("span"); spacer.className = "arrow none"; row.appendChild(spacer); const icon = document.createElement("span"); icon.className = "icon"; if (node.name.endsWith(".md")) { icon.textContent = "📝"; } else if (node.name.endsWith(".json")) { icon.textContent = "🧩"; } else if (node.name.endsWith(".yaml") || node.name.endsWith(".yml")) { icon.textContent = "⚙️"; } else { icon.textContent = "📄"; } row.appendChild(icon); const label = document.createElement("span"); label.className = "label"; label.textContent = node.name; row.appendChild(label); if (node.delete_mode === "template") { const del = document.createElement("span"); del.className = "del-btn"; del.textContent = "删除"; del.addEventListener("click", async (event) => { event.stopPropagation(); await deleteTemplateNode(node.name); }); row.appendChild(del); } row.addEventListener("click", async () => { await openTreeResource(node.path); }); } wrapper.appendChild(row); if (node.children?.length) { const children = document.createElement("div"); children.className = `tree-children ${node.active ? "open" : ""}`; if (node.active) { row.querySelector(".arrow")?.classList.add("expanded"); } node.children.forEach((child) => renderNode(child, children, depth + 1)); wrapper.appendChild(children); } parent.appendChild(wrapper); } function buildTree(tree) { const root = $("#file-tree"); root.innerHTML = ""; (tree.children || []).forEach((child) => renderNode(child, root, 0)); setSelectedTreeKey(state.selectedTreeKey); } async function loadTree() { const tree = await api("/api/tree"); buildTree(tree); } async function loadMeetingSummary(meetingId) { const result = await api(`/api/meetings/${meetingId}/file/meeting_summary.md`); showResult(result.content); } async function selectMeeting(meetingId) { if (state.processing || state.guideBusy) { return; } await setCurrentMeeting(meetingId); setSelectedTreeKey(`meeting:${meetingId}`); try { await loadMeetingSummary(meetingId); } catch { showResultEmpty(); } } function renderSidePreview(resource) { $("#side-editor").style.display = "none"; $("#side-preview").style.display = "none"; $("#side-plain-preview").style.display = "none"; if (!resource) { $("#side-plain-preview").style.display = "block"; $("#side-plain-preview").textContent = "请选择一个资源"; return; } if (isMarkdownFile(resource.name)) { $("#side-preview").style.display = "block"; $("#side-preview").innerHTML = marked.parse(resource.content || ""); return; } $("#side-plain-preview").style.display = "block"; $("#side-plain-preview").textContent = resource.content || ""; } function syncTemplateSelection(name) { state.templateName = name; savePreferences({ templateName: name }); $("#tpl-select").value = name; } function updateGuideButton(resource) { const button = $("#btn-reparse-guide"); const isTemplateResource = resource && (resource.type === "template" || resource.type === "template-guide"); if (!isTemplateResource) { button.disabled = true; button.textContent = "解析"; return; } const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide); button.textContent = state.guideBusy ? (hasGuide ? "重解析中" : "解析中") : hasGuide ? "重解析" : "解析"; button.disabled = state.guideBusy || state.processing; } function applyRightResource(resource) { state.rightResource = resource; $("#editor-resource-label").textContent = `当前资源:${resource.label}`; $("#side-editor").value = resource.content || ""; $("#btn-toggle-side-edit").textContent = resource.editable ? "编辑" : "只读"; state.rightEditMode = false; renderSidePreview(resource); if (resource.type === "template" || resource.type === "template-guide") { syncTemplateSelection(resource.templateName || resource.name); } refreshActionButtons(); } function setRightEditMode(editMode) { state.rightEditMode = editMode; const resource = state.rightResource; if (!resource || !resource.editable) { renderSidePreview(resource); return; } $("#btn-toggle-side-edit").textContent = editMode ? "保存" : "编辑"; if (editMode) { $("#side-preview").style.display = "none"; $("#side-plain-preview").style.display = "none"; $("#side-editor").style.display = "block"; } else { resource.content = $("#side-editor").value; renderSidePreview(resource); } } async function openRightResource(resource) { applyRightResource(resource); if (resource.treeKey) { setSelectedTreeKey(resource.treeKey); } } async function openTemplate(name, treeKey = `file:templates/${name}`) { const data = await api(`/api/templates/${encodeURIComponent(name)}`); await openRightResource({ type: "template", name, templateName: name, label: `模板 / ${name}`, content: data.content, hasGuide: Boolean(data.has_guide), editable: true, treeKey, }); } async function openTemplateGuide(name, treeKey = `file:template_guides/${name}`) { const data = await api(`/api/templates/${encodeURIComponent(name)}/guide`); await openRightResource({ type: "template-guide", name, templateName: name, label: `模板说明 / ${name}`, content: data.content, hasGuide: true, editable: true, treeKey, }); } async function openPrompt(name, treeKey = `file:prompts/${name}`) { const data = await api(`/api/prompts/${encodeURIComponent(name)}`); await openRightResource({ type: "prompt", name, label: `提示词 / ${name}`, content: data.content, editable: true, treeKey, }); } async function openMeetingFile(meetingId, filename, treeKey = `file:meetings/${meetingId}/${filename}`) { const data = await api(`/api/meetings/${meetingId}/file/${encodeURIComponent(filename)}`); await openRightResource({ type: "meeting-file", meetingId, name: filename, label: `会议原文 / ${meetingById(meetingId)?.name || meetingId} / ${filename}`, content: data.content, editable: false, treeKey, }); } async function openResultFile(meetingId, filename, treeKey) { const data = await api(`/api/meetings/${meetingId}/file/${encodeURIComponent(filename)}`); await openRightResource({ type: "result-file", meetingId, name: filename, label: `处理结果 / ${meetingById(meetingId)?.name || meetingId} / ${filename}`, content: data.content, editable: false, treeKey, }); } async function openTreeResource(path) { if (state.processing || state.guideBusy) { return; } const parts = path.split("/"); const group = parts[0]; const treeKey = `file:${path}`; if (group === "templates") { await openTemplate(parts.slice(1).join("/"), treeKey); return; } if (group === "prompts") { await openPrompt(parts.slice(1).join("/"), treeKey); return; } if (group === "template_guides") { await openTemplateGuide(parts.slice(1).join("/"), treeKey); return; } const meetingId = parts[1]; const filename = parts.slice(2).join("/"); await setCurrentMeeting(meetingId); if (group === "results_md" && filename === "meeting_summary.md") { setSelectedTreeKey(treeKey); await loadMeetingSummary(meetingId); return; } if (group === "meetings") { await openMeetingFile(meetingId, filename, treeKey); return; } if (group === "results_json" || group === "results_md") { await openResultFile(meetingId, filename, treeKey); } } async function deleteMeetingNode(meetingId, deleteMode) { const isDeleteMeeting = deleteMode === "meeting"; const confirmMessage = isDeleteMeeting ? "确定删除该会议原文及其全部处理结果吗?" : "确定只删除该会议的处理结果吗?原文将保留。"; if (!window.confirm(confirmMessage)) { return; } const endpoint = isDeleteMeeting ? `/api/meetings/${meetingId}` : `/api/meetings/${meetingId}/results`; await api(endpoint, { method: "DELETE" }); toast(isDeleteMeeting ? "会议及处理结果已删除" : "处理结果已删除,原文已保留"); await refresh(); if (isDeleteMeeting && !meetingById(state.meetingId)) { showResultEmpty(); return; } if (!isDeleteMeeting && state.meetingId === meetingId) { try { await loadMeetingSummary(meetingId); } catch { showResultEmpty(); } } } async function deleteTemplateNode(name) { if (!window.confirm(`确定删除模板 ${name} 及其对应使用说明吗?`)) { return; } await api(`/api/templates/${encodeURIComponent(name)}`, { method: "DELETE" }); toast(`模板已删除:${name}`); if ( state.rightResource && (state.rightResource.templateName === name || state.rightResource.name === name) ) { state.rightResource = null; } if (state.templateName === name) { state.templateName = ""; savePreferences({ templateName: "" }); } await refresh(); } function initResize(gutterId, leftId, rightId) { const gutter = document.getElementById(gutterId); const left = document.getElementById(leftId); const right = document.getElementById(rightId); let dragging = false; let startX = 0; let startLeftW = 0; let startRightW = 0; gutter.addEventListener("mousedown", (event) => { if (window.innerWidth <= 1100) { return; } event.preventDefault(); dragging = true; gutter.classList.add("dragging"); startX = event.clientX; startLeftW = left.getBoundingClientRect().width; startRightW = right.getBoundingClientRect().width; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (event) => { if (!dragging) { return; } const dx = event.clientX - startX; const minLeft = Number(left.dataset.minWidth || 220); const minRight = Number(right.dataset.minWidth || 320); let nextLeft = Math.max(minLeft, startLeftW + dx); let nextRight = startRightW - dx; if (nextRight < minRight) { nextRight = minRight; nextLeft = startLeftW + startRightW - minRight; } if (leftId === "sidebar") { left.style.flexBasis = `${nextLeft}px`; left.style.flexGrow = "0"; } else { left.style.flexBasis = "0%"; left.style.flexGrow = "1"; } right.style.flexBasis = `${nextRight}px`; right.style.flexGrow = "0"; }); document.addEventListener("mouseup", () => { if (!dragging) { return; } dragging = false; gutter.classList.remove("dragging"); document.body.style.cursor = ""; document.body.style.userSelect = ""; persistLayout(); }); } async function refresh() { const templateData = await api("/api/templates"); state.templates = templateData; const select = $("#tpl-select"); select.innerHTML = ""; templateData.forEach((item) => { const option = document.createElement("option"); option.value = item.name; option.textContent = item.name; select.appendChild(option); }); if (!templateData.some((item) => item.name === state.templateName) && templateData[0]) { state.templateName = templateData[0].name; } if (state.templateName) { syncTemplateSelection(state.templateName); } const meetingsData = await api("/api/meetings"); state.meetings = meetingsData.meetings || []; state.meetingId = meetingsData.active_meeting_id || null; await loadTree(); renderMeetingStatus(meetingById(state.meetingId)); if (state.meetingId) { try { await loadMeetingSummary(state.meetingId); } catch { showResultEmpty(); } } else { showResultEmpty(); } if ( state.rightResource && state.rightResource.type === "template" && templateData.some((item) => item.name === state.rightResource.name) ) { await openTemplate(state.rightResource.name, state.rightResource.treeKey); } else if ( state.rightResource && state.rightResource.type === "template-guide" && templateData.some((item) => item.name === state.rightResource.templateName) ) { await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey); } else if (!state.rightResource && state.templateName) { await openTemplate(state.templateName); } refreshActionButtons(); } async function ensureTemplateGuideBeforeProcess() { const templateMeta = templateMetaByName(state.templateName); if (templateMeta?.has_guide) { return; } toast("当前模板还没有解析说明,先为你解析模板。"); state.guideBusy = true; setStatus("left", true, "解析中"); refreshActionButtons(); try { const result = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide/reparse`, { method: "POST", }); state.templates = state.templates.map((item) => ( item.name === state.templateName ? { ...item, has_guide: true } : item )); if ( state.rightResource && state.rightResource.templateName === result.name && state.rightResource.type === "template" ) { state.rightResource.hasGuide = true; refreshActionButtons(); } } finally { state.guideBusy = false; setStatus("left", false, "空闲"); refreshActionButtons(); } } async function loadTemplateGuideForProcess() { const data = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`); $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; $("#process-guide-editor").value = data.content || ""; if (!$("#process-extra-notes").dataset.keepValue) { $("#process-extra-notes").value = ""; } setProcessGuideEditMode(false); syncProcessGuideConfirmLabel(); } async function saveProcessGuideFromModal() { const content = $("#process-guide-editor").value; await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); state.templates = state.templates.map((item) => ( item.name === state.templateName ? { ...item, has_guide: true } : item )); if ( state.rightResource && state.rightResource.templateName === state.templateName && state.rightResource.type === "template-guide" ) { state.rightResource.content = content; } if ( state.rightResource && state.rightResource.name === state.templateName && state.rightResource.type === "template" ) { state.rightResource.hasGuide = true; } setProcessGuideEditMode(false); refreshActionButtons(); toast("说明已保存"); } async function openProcessGuideModal() { openModal("modal-process-guide"); hideReparseGuidePrompt(); $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; $("#guide-modal-subtitle").textContent = "正在准备模板说明..."; $("#process-guide-editor").value = "正在加载模板说明,请稍等..."; $("#process-guide-editor").readOnly = true; $("#process-guide-editor").classList.remove("is-editing"); $("#process-extra-notes").dataset.keepValue = "1"; $("#btn-process-guide-edit").disabled = true; $("#btn-process-guide-reparse").disabled = true; $("#btn-process-guide-confirm").disabled = true; try { await ensureTemplateGuideBeforeProcess(); await loadTemplateGuideForProcess(); } catch (error) { closeModal("modal-process-guide"); throw error; } finally { $("#btn-process-guide-edit").disabled = false; $("#btn-process-guide-reparse").disabled = false; $("#btn-process-guide-confirm").disabled = false; delete $("#process-extra-notes").dataset.keepValue; } } async function reparseTemplateGuideFromModal(userNotes = "") { const templateName = state.templateName; state.guideBusy = true; setStatus("right", true, "解析中"); $("#btn-process-guide-edit").disabled = true; $("#btn-process-guide-reparse").disabled = true; $("#btn-process-guide-confirm").disabled = true; $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; refreshActionButtons(); try { if (state.processGuideEditMode) { await saveProcessGuideFromModal(); } const result = await runTemplateGuideReparse(templateName, userNotes); $("#process-guide-editor").value = result.content || ""; $("#guide-modal-title").textContent = `模板说明 · ${templateName}`; $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; setProcessGuideEditMode(false); toast(`说明已更新:${templateName}`); } finally { state.guideBusy = false; setStatus("right", false, "空闲"); $("#btn-process-guide-edit").disabled = false; $("#btn-process-guide-reparse").disabled = false; $("#btn-process-guide-confirm").disabled = false; refreshActionButtons(); } } async function runTemplateGuideReparse(templateName, userNotes = "") { return streamTemplateGuideReparse(templateName, userNotes, { onStatus() { $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; }, onChunk(_chunk, contentAcc) { $("#process-guide-editor").value = contentAcc; }, }); } function startMeetingProcess(userNotes = "") { state.processing = true; setStatus("left", true, "总结中"); refreshActionButtons(); showProcessingView(); $("#stream-box").style.display = "block"; $("#stream-title").textContent = "第一阶段:结构化主题..."; $("#stream-content").textContent = ""; const params = new URLSearchParams({ template_name: state.templateName }); if (userNotes.trim()) { params.set("user_notes", userNotes.trim()); } const source = new EventSource(`/api/meetings/${state.meetingId}/process?${params.toString()}`); let resultAcc = ""; let streamAcc = ""; source.onmessage = async (event) => { if (!event.data) { return; } const payload = JSON.parse(event.data); if (payload.type === "status") { if (payload.data === "preprocessing") { $("#stream-title").textContent = "第一阶段:结构化主题..."; } else if (payload.data === "preprocessing_done") { $("#stream-title").textContent = "主题提取完成,开始生成会议总结..."; } else if (payload.data === "summarizing") { $("#stream-title").textContent = "第二阶段:生成会议总结..."; streamAcc = ""; $("#stream-content").textContent = ""; } return; } if (payload.type === "chunk") { const { data } = payload; streamAcc += data.text || ""; $("#stream-content").textContent = streamAcc.replace(/\r\n/g, "\n").split("\n").slice(-4).join("\n"); if (data.stage === 2 && data.chunk_type === "content") { resultAcc += data.text || ""; } return; } if (payload.type === "done") { source.close(); state.processing = false; patchMeeting(state.meetingId, { has_summary: true }); renderMeetingStatus(meetingById(state.meetingId)); showResult(payload.data?.result || resultAcc || ""); toast("会议总结完成"); refreshActionButtons(); return; } if (payload.type === "error") { source.close(); state.processing = false; resetProcessingStream(); $("#processing-indicator").hidden = true; refreshActionButtons(); toast(`处理失败:${payload.data}`, "err"); } }; source.onerror = () => { source.close(); state.processing = false; resetProcessingStream(); $("#processing-indicator").hidden = true; refreshActionButtons(); toast("处理连接中断", "err"); }; } $("#btn-process").addEventListener("click", async () => { if (!state.meetingId || state.processing || state.guideBusy) { return; } try { await openProcessGuideModal(); } catch (error) { state.processing = false; state.guideBusy = false; setStatus("left", false, "空闲"); refreshActionButtons(); toast(error.message, "err"); } }); $("#btn-close-process-guide").addEventListener("click", () => { hideReparseGuidePrompt(); closeModal("modal-process-guide"); }); $("#btn-process-guide-edit").addEventListener("click", async () => { if (state.processing || state.guideBusy) { return; } if (!state.processGuideEditMode) { setProcessGuideEditMode(true); return; } $("#btn-process-guide-edit").disabled = true; try { await saveProcessGuideFromModal(); } catch (error) { toast(error.message, "err"); } finally { $("#btn-process-guide-edit").disabled = false; } }); $("#btn-process-guide-reparse").addEventListener("click", async () => { if (state.processing || state.guideBusy) { return; } $("#btn-process-guide-reparse").disabled = true; try { if (state.processGuideEditMode) { await saveProcessGuideFromModal(); } hideReparseGuidePrompt(); closeModal("modal-process-guide"); openStandaloneReparseModal(); } catch (error) { toast(error.message, "err"); } finally { $("#btn-process-guide-reparse").disabled = false; } }); $("#btn-reparse-guide-cancel").addEventListener("click", () => { hideReparseGuidePrompt(); }); $("#btn-reparse-guide-confirm").addEventListener("click", async () => { if (state.processing || state.guideBusy) { return; } $("#btn-reparse-guide-confirm").disabled = true; try { const userNotes = $("#reparse-guide-notes").value || ""; await reparseTemplateGuideFromModal(userNotes); hideReparseGuidePrompt(); } catch (error) { toast(error.message, "err"); } finally { $("#btn-reparse-guide-confirm").disabled = false; } }); $("#btn-close-reparse-guide").addEventListener("click", () => { if (state.guideBusy) { return; } closeModal("modal-reparse-guide"); }); $("#btn-reparse-modal-cancel").addEventListener("click", () => { if (state.guideBusy) { return; } closeModal("modal-reparse-guide"); }); $("#btn-reparse-modal-confirm").addEventListener("click", async () => { if (state.processing || state.guideBusy) { return; } try { await runStandaloneGuideReparseFlow(); } catch (error) { toast(error.message, "err"); } }); $("#btn-process-guide-confirm").addEventListener("click", async () => { if (!state.meetingId || state.processing || state.guideBusy) { return; } $("#btn-process-guide-confirm").disabled = true; try { if (state.processGuideEditMode) { await saveProcessGuideFromModal(); } const userNotes = $("#process-extra-notes").value || ""; closeModal("modal-process-guide"); startMeetingProcess(userNotes); } catch (error) { toast(error.message, "err"); } finally { $("#btn-process-guide-confirm").disabled = false; } }); $("#btn-toggle-result-edit").addEventListener("click", async () => { if (!state.meetingId || state.processing || state.guideBusy) { return; } if (!state.resultEditMode) { setResultEditMode(true); return; } const content = $("#result-editor").value; $("#btn-toggle-result-edit").disabled = true; await api(`/api/meetings/${state.meetingId}/summary`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); patchMeeting(state.meetingId, { has_summary: true }); renderMeetingStatus(meetingById(state.meetingId)); showResult(content); toast("结果已保存"); }); $("#btn-toggle-side-edit").addEventListener("click", async () => { const resource = state.rightResource; if (!resource || !resource.editable || state.processing || state.guideBusy) { return; } if (!state.rightEditMode) { setRightEditMode(true); return; } const content = $("#side-editor").value; $("#btn-toggle-side-edit").disabled = true; if (resource.type === "template") { await api(`/api/templates/${encodeURIComponent(resource.name)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); } else if (resource.type === "template-guide") { await api(`/api/templates/${encodeURIComponent(resource.templateName)}/guide`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); } else if (resource.type === "prompt") { await api(`/api/prompts/${encodeURIComponent(resource.name)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); } resource.content = content; setRightEditMode(false); toast("资源已保存"); refreshActionButtons(); }); $("#tpl-select").addEventListener("change", async (event) => { syncTemplateSelection(event.target.value); await openTemplate(event.target.value); }); $("#btn-reparse-guide").addEventListener("click", async () => { const resource = state.rightResource; if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) { return; } if (state.processing || state.guideBusy) { return; } const templateName = resource.templateName || resource.name; syncTemplateSelection(templateName); openStandaloneReparseModal(); }); renderMeetingStatus = function(meeting) { const name = $("#sidebar-meeting-name"); const meta = $("#sidebar-meeting-meta"); const tip = $("#selected-meeting-tip"); const summaryBadge = $("#badge-summary"); const topicsBadge = $("#badge-topics"); if (!meeting) { name.textContent = "当前会议:未选择"; meta.textContent = "请从左侧选择一个会议开始处理。"; tip.textContent = "未选择会议"; summaryBadge.textContent = "未生成总结"; topicsBadge.textContent = "未生成主题 JSON"; summaryBadge.className = "badge muted"; topicsBadge.className = "badge muted"; return; } name.textContent = `当前会议:${meeting.name}`; meta.textContent = `ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 原始文件:` + `${meeting.original_filename || meeting.transcript_filename || "未知"}`; tip.textContent = `当前处理会议:${meeting.name} (${meeting.id})`; summaryBadge.textContent = meeting.has_summary ? "已生成总结" : "未生成总结"; topicsBadge.textContent = meeting.has_topics ? "已生成主题 JSON" : "未生成主题 JSON"; summaryBadge.className = meeting.has_summary ? "badge" : "badge muted"; topicsBadge.className = meeting.has_topics ? "badge" : "badge muted"; }; refreshActionButtons = function() { const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy; const currentMeeting = meetingById(state.meetingId); $("#btn-process").disabled = !canProcess; $("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : currentMeeting?.has_summary ? "重总结" : "总结"; const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy; $("#btn-toggle-result-edit").disabled = !canEditResult; const resource = state.rightResource; const canEditSide = Boolean(resource?.editable) && !state.processing && !state.guideBusy; $("#btn-toggle-side-edit").disabled = !canEditSide; updateGuideButton(resource); }; setProcessGuideEditMode = function(editMode) { state.processGuideEditMode = editMode; const editor = $("#process-guide-editor"); const button = $("#btn-process-guide-edit"); editor.readOnly = !editMode; editor.classList.toggle("is-editing", editMode); button.textContent = editMode ? "保存" : "编辑"; }; syncProcessGuideConfirmLabel = function() { const meeting = meetingById(state.meetingId); $("#btn-process-guide-confirm").textContent = meeting?.has_summary ? "重总结" : "确认"; }; persistSettingsDraft = async function() { const payload = cloneSettingsDraft(state.settingsDraft || {}); if (!payload.api_profiles.length) { throw new Error("至少保留一个模型配置"); } await api("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); }; resetReparseModalProgress = function() { $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; $("#reparse-stream-content").textContent = ""; }; openStandaloneReparseModal = function() { $("#reparse-modal-title").textContent = `模板说明补充 · ${state.templateName}`; $("#reparse-modal-subtitle").textContent = "可为空;会和系统解析模板一起用于重新解析"; $("#reparse-modal-notes").value = ""; $("#reparse-modal-form").hidden = false; $("#reparse-modal-progress").hidden = true; $("#btn-reparse-modal-cancel").disabled = false; $("#btn-reparse-modal-confirm").disabled = false; resetReparseModalProgress(); openModal("modal-reparse-guide"); $("#reparse-modal-notes").focus(); }; runStandaloneGuideReparseFlow = async function() { const templateName = state.templateName; const userNotes = $("#reparse-modal-notes").value || ""; state.guideBusy = true; setStatus("right", true, "解析中"); setStandaloneReparseProcessing(true); resetReparseModalProgress(); pushStandaloneReparseLine(`模板:${templateName}`); pushStandaloneReparseLine("已提交重解析请求,正在准备说明..."); refreshActionButtons(); try { const params = new URLSearchParams(); if (userNotes.trim()) { params.set("user_notes", userNotes.trim()); } const source = new EventSource( `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream?${params.toString()}`, ); const result = await new Promise((resolve, reject) => { let contentAcc = ""; source.onmessage = (event) => { if (!event.data) { return; } const payload = JSON.parse(event.data); if (payload.type === "status") { $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; pushStandaloneReparseLine("已连接模型,开始解析..."); return; } if (payload.type === "chunk") { const chunk = payload.data?.text || ""; if (chunk) { contentAcc += chunk; $("#reparse-stream-content").textContent = contentAcc .replace(/\r\n/g, "\n") .split("\n") .slice(-6) .join("\n"); } return; } if (payload.type === "done") { source.close(); resolve(payload.data || { name: templateName, content: contentAcc }); return; } if (payload.type === "error") { source.close(); reject(new Error(payload.data || "解析失败")); } }; source.onerror = () => { source.close(); reject(new Error("解析连接中断")); }; }); state.templates = state.templates.map((item) => ( item.name === templateName ? { ...item, has_guide: true } : item )); if (state.rightResource?.name === templateName && state.rightResource.type === "template") { state.rightResource.hasGuide = true; } await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); closeModal("modal-reparse-guide"); await openProcessGuideModal(); $("#process-guide-editor").value = result.content || ""; } finally { state.guideBusy = false; setStatus("right", false, "空闲"); setStandaloneReparseProcessing(false); refreshActionButtons(); } }; resetProcessingStream = function() { $("#stream-box").style.display = "none"; $("#stream-title").textContent = ""; $("#stream-content").textContent = ""; setStatus("left", false, "空闲"); }; ensureTemplateGuideBeforeProcess = async function() { const templateMeta = templateMetaByName(state.templateName); if (templateMeta?.has_guide) { return; } toast("当前模板还没有解析说明,先为你解析模板。"); state.guideBusy = true; setStatus("left", true, "解析中"); refreshActionButtons(); try { const result = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide/reparse`, { method: "POST", }); state.templates = state.templates.map((item) => ( item.name === state.templateName ? { ...item, has_guide: true } : item )); if ( state.rightResource && state.rightResource.templateName === result.name && state.rightResource.type === "template" ) { state.rightResource.hasGuide = true; refreshActionButtons(); } } finally { state.guideBusy = false; setStatus("left", false, "空闲"); refreshActionButtons(); } }; loadTemplateGuideForProcess = async function() { const data = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`); $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; $("#process-guide-editor").value = data.content || ""; if (!$("#process-extra-notes").dataset.keepValue) { $("#process-extra-notes").value = ""; } setProcessGuideEditMode(false); syncProcessGuideConfirmLabel(); }; saveProcessGuideFromModal = async function() { const content = $("#process-guide-editor").value; await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); state.templates = state.templates.map((item) => ( item.name === state.templateName ? { ...item, has_guide: true } : item )); if ( state.rightResource && state.rightResource.templateName === state.templateName && state.rightResource.type === "template-guide" ) { state.rightResource.content = content; } if ( state.rightResource && state.rightResource.name === state.templateName && state.rightResource.type === "template" ) { state.rightResource.hasGuide = true; } setProcessGuideEditMode(false); refreshActionButtons(); toast("说明已保存"); }; openProcessGuideModal = async function() { openModal("modal-process-guide"); hideReparseGuidePrompt(); $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; $("#guide-modal-subtitle").textContent = "正在准备模板说明..."; $("#process-guide-editor").value = "正在加载模板说明,请稍等..."; $("#process-guide-editor").readOnly = true; $("#process-guide-editor").classList.remove("is-editing"); $("#process-extra-notes").dataset.keepValue = "1"; $("#btn-process-guide-edit").disabled = true; $("#btn-process-guide-reparse").disabled = true; $("#btn-process-guide-confirm").disabled = true; try { await ensureTemplateGuideBeforeProcess(); await loadTemplateGuideForProcess(); } catch (error) { closeModal("modal-process-guide"); throw error; } finally { $("#btn-process-guide-edit").disabled = false; $("#btn-process-guide-reparse").disabled = false; $("#btn-process-guide-confirm").disabled = false; delete $("#process-extra-notes").dataset.keepValue; } }; reparseTemplateGuideFromModal = async function(userNotes = "") { const templateName = state.templateName; state.guideBusy = true; setStatus("right", true, "解析中"); $("#btn-process-guide-edit").disabled = true; $("#btn-process-guide-reparse").disabled = true; $("#btn-process-guide-confirm").disabled = true; $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; refreshActionButtons(); try { if (state.processGuideEditMode) { await saveProcessGuideFromModal(); } const result = await runTemplateGuideReparse(templateName, userNotes); $("#process-guide-editor").value = result.content || ""; $("#guide-modal-title").textContent = `模板说明 · ${templateName}`; $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; setProcessGuideEditMode(false); toast(`说明已更新:${templateName}`); } finally { state.guideBusy = false; setStatus("right", false, "空闲"); $("#btn-process-guide-edit").disabled = false; $("#btn-process-guide-reparse").disabled = false; $("#btn-process-guide-confirm").disabled = false; refreshActionButtons(); } }; runTemplateGuideReparse = async function(templateName, userNotes = "") { return streamTemplateGuideReparse(templateName, userNotes, { onStatus() { $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; }, onChunk(_chunk, contentAcc) { $("#process-guide-editor").value = contentAcc; }, }); }; startMeetingProcess = function(userNotes = "") { state.processing = true; setStatus("left", true, "总结中"); refreshActionButtons(); showProcessingView(); $("#stream-box").style.display = "block"; $("#stream-title").textContent = "第一阶段:结构化主题..."; $("#stream-content").textContent = ""; const params = new URLSearchParams({ template_name: state.templateName }); if (userNotes.trim()) { params.set("user_notes", userNotes.trim()); } const source = new EventSource(`/api/meetings/${state.meetingId}/process?${params.toString()}`); let resultAcc = ""; let streamAcc = ""; source.onmessage = async (event) => { if (!event.data) { return; } const payload = JSON.parse(event.data); if (payload.type === "status") { if (payload.data === "preprocessing") { $("#stream-title").textContent = "第一阶段:结构化主题..."; } else if (payload.data === "preprocessing_done") { $("#stream-title").textContent = "主题提取完成,开始生成会议总结..."; } else if (payload.data === "summarizing") { $("#stream-title").textContent = "第二阶段:生成会议总结..."; streamAcc = ""; $("#stream-content").textContent = ""; } return; } if (payload.type === "chunk") { const { data } = payload; streamAcc += data.text || ""; $("#stream-content").textContent = streamAcc.replace(/\r\n/g, "\n").split("\n").slice(-4).join("\n"); if (data.stage === 2 && data.chunk_type === "content") { resultAcc += data.text || ""; } return; } if (payload.type === "done") { source.close(); state.processing = false; patchMeeting(state.meetingId, { has_summary: true }); renderMeetingStatus(meetingById(state.meetingId)); showResult(payload.data?.result || resultAcc || ""); toast("会议总结完成"); refreshActionButtons(); return; } if (payload.type === "error") { source.close(); state.processing = false; resetProcessingStream(); $("#processing-indicator").hidden = true; refreshActionButtons(); toast(`处理失败:${payload.data}`, "err"); } }; source.onerror = () => { source.close(); state.processing = false; resetProcessingStream(); $("#processing-indicator").hidden = true; refreshActionButtons(); toast("处理连接中断", "err"); }; }; $("#btn-import").addEventListener("click", () => { $("#import-name").value = ""; $("#import-file").value = ""; openModal("modal-import"); }); $("#btn-import-template").addEventListener("click", () => { $("#import-template-name").value = ""; $("#import-template-file").value = ""; openModal("modal-import-template"); }); $("#btn-confirm-import").addEventListener("click", async () => { const name = $("#import-name").value.trim(); const file = $("#import-file").files[0]; if (!name) { toast("请填写会议名称", "err"); return; } if (!file) { toast("请选择会议文件", "err"); return; } const formData = new FormData(); formData.append("name", name); formData.append("file", file); const result = await fetch("/api/meetings/import", { method: "POST", body: formData }); if (!result.ok) { const detail = await result.json().catch(() => ({ detail: "Import failed" })); toast(`导入失败:${detail.detail}`, "err"); return; } const payload = await result.json(); closeModal("modal-import"); toast(`已导入会议:${name}`); await refresh(); await selectMeeting(payload.id); }); $("#btn-confirm-import-template").addEventListener("click", async () => { const name = $("#import-template-name").value.trim(); const file = $("#import-template-file").files[0]; if (!name) { toast("请填写模板名称", "err"); return; } if (!file) { toast("请选择模板文件", "err"); return; } const formData = new FormData(); formData.append("name", name); formData.append("file", file); const result = await fetch("/api/templates/import", { method: "POST", body: formData }); if (!result.ok) { const detail = await result.json().catch(() => ({ detail: "Import failed" })); toast(`导入失败:${detail.detail}`, "err"); return; } const payload = await result.json(); closeModal("modal-import-template"); toast(`已导入模板:${payload.name}`); await refresh(); syncTemplateSelection(payload.name); await openTemplate(payload.name); }); $("#btn-settings").addEventListener("click", async () => { try { const cfg = await api("/api/settings"); loadSettingsDraft(cfg); } finally { openModal("modal-settings"); } }); $("#cfg-key-select").addEventListener("change", async (event) => { if (!state.settingsDraft) { return; } state.settingsDraft.active_api_profile_name = event.target.value; renderSettingsKeyOptions(); try { await persistSettingsDraft(); toast(`已切换模型:${$("#cfg-current-model").value}`); } catch (error) { toast(error.message, "err"); } }); $("#btn-save-settings").addEventListener("click", async () => { if (!state.settingsDraft) { return; } const currentName = $("#cfg-key-select").value; const maxTokens = Number($("#cfg-max-tokens").value || 64000); if (!Number.isFinite(maxTokens) || maxTokens < 1) { toast("max_tokens 必须大于 0", "err"); return; } state.settingsDraft.api_profiles = state.settingsDraft.api_profiles.map((item) => ( item.name === currentName ? { ...item, max_tokens: Math.floor(maxTokens) } : item )); try { await persistSettingsDraft(); renderSettingsKeyOptions(); toast(`已保存模型配置:${$("#cfg-current-model").value}`); } catch (error) { toast(error.message, "err"); } }); $("#btn-key-add").addEventListener("click", () => { $("#cfg-add-model-name").value = ""; $("#cfg-add-base-url").value = ""; $("#cfg-add-api-key").value = ""; $("#cfg-add-max-tokens").value = "64000"; openModal("modal-add-model"); }); $("#btn-confirm-add-model").addEventListener("click", async () => { if (!state.settingsDraft) { return; } const modelName = $("#cfg-add-model-name").value.trim(); const apiBaseUrl = $("#cfg-add-base-url").value.trim(); const apiKey = $("#cfg-add-api-key").value.trim(); const maxTokens = Number($("#cfg-add-max-tokens").value || 64000); if (!modelName || !apiBaseUrl || !apiKey || !Number.isFinite(maxTokens) || maxTokens < 1) { toast("请完整填写模型配置", "err"); return; } if (state.settingsDraft.api_profiles.some((item) => item.name === modelName)) { toast("模型名称已存在", "err"); return; } state.settingsDraft.api_profiles.push({ name: modelName, model_name: modelName, api_base_url: apiBaseUrl, api_key: apiKey, max_tokens: Math.floor(maxTokens), }); state.settingsDraft.active_api_profile_name = modelName; renderSettingsKeyOptions(); try { await persistSettingsDraft(); closeModal("modal-add-model"); toast(`已新增模型:${modelName}`); } catch (error) { toast(error.message, "err"); } }); $("#btn-key-delete").addEventListener("click", async () => { if (!state.settingsDraft || state.settingsDraft.api_profiles.length <= 1) { return; } const targetName = $("#cfg-key-select").value; state.settingsDraft.api_profiles = state.settingsDraft.api_profiles.filter((item) => item.name !== targetName); state.settingsDraft.active_api_profile_name = state.settingsDraft.api_profiles[0]?.name || ""; renderSettingsKeyOptions(); try { await persistSettingsDraft(); toast(`已删除模型:${targetName}`); } catch (error) { toast(error.message, "err"); } }); $$("[data-close]").forEach((button) => { button.addEventListener("click", () => closeModal(button.dataset.close)); }); ["modal-import", "modal-import-template", "modal-settings", "modal-add-model", "modal-process-guide", "modal-reparse-guide"].forEach((id) => { document.getElementById(id).addEventListener("click", (event) => { if (event.target.id === id) { if (id === "modal-reparse-guide" && state.guideBusy) { return; } closeModal(id); } }); }); document.addEventListener("DOMContentLoaded", async () => { $("#sidebar").dataset.minWidth = "260"; $("#result-panel").dataset.minWidth = "360"; $("#template-panel").dataset.minWidth = "520"; applySavedLayout(); initResize("gutter-1", "sidebar", "result-panel"); initResize("gutter-2", "result-panel", "template-panel"); setStatus("left", false, "空闲"); setStatus("right", false, "空闲"); try { await refresh(); } catch (error) { toast(error.message, "err"); } });