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, selectedTreeKey: "", rightResource: 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"); } 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 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; $("#btn-process").disabled = !canProcess; $("#btn-process").textContent = state.processing ? "总结中" : state.guideBusy ? "等待" : "总结"; 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 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(); } } function startMeetingProcess() { state.processing = true; setStatus("left", true, "总结中"); refreshActionButtons(); showProcessingView(); $("#stream-box").style.display = "block"; $("#stream-title").textContent = "第一阶段:结构化主题..."; $("#stream-content").textContent = ""; const source = new EventSource( `/api/meetings/${state.meetingId}/process?template_name=${encodeURIComponent(state.templateName)}`, ); 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; 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 ensureTemplateGuideBeforeProcess(); startMeetingProcess(); } catch (error) { state.processing = false; state.guideBusy = false; setStatus("left", false, "空闲"); refreshActionButtons(); toast(error.message, "err"); } }); $("#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 }), }); 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; state.guideBusy = true; setStatus("right", true, "解析中"); refreshActionButtons(); try { const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, { method: "POST", }); state.templates = state.templates.map((item) => ( item.name === templateName ? { ...item, has_guide: true } : item )); toast(`说明已更新:${templateName}`); await openTemplateGuide(result.name); } finally { state.guideBusy = false; setStatus("right", false, "空闲"); refreshActionButtons(); } }); $("#btn-import").addEventListener("click", () => { $("#import-name").value = ""; $("#import-file").value = ""; openModal("modal-import"); }); $("#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-settings").addEventListener("click", async () => { try { const cfg = await api("/api/settings"); $("#cfg-url").value = cfg.api_base_url || ""; $("#cfg-key").value = cfg.api_key || ""; $("#cfg-model").value = cfg.model_name || ""; } finally { openModal("modal-settings"); } }); $("#btn-save-settings").addEventListener("click", async () => { const payload = { api_base_url: $("#cfg-url").value.trim(), api_key: $("#cfg-key").value.trim(), model_name: $("#cfg-model").value.trim(), }; await api("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); closeModal("modal-settings"); toast("配置已保存"); }); $$("[data-close]").forEach((button) => { button.addEventListener("click", () => closeModal(button.dataset.close)); }); ["modal-import", "modal-settings"].forEach((id) => { document.getElementById(id).addEventListener("click", (event) => { if (event.target.id === id) { 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"); } });