my_meeting/frontend/assets/app.js

990 lines
28 KiB
JavaScript
Raw Normal View History

2026-05-09 03:23:57 +00:00
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const state = {
meetingId: null,
meetings: [],
templateName: "template1.md",
2026-05-09 08:32:17 +00:00
templates: [],
2026-05-09 03:23:57 +00:00
processing: false,
2026-05-09 08:32:17 +00:00
resultEditMode: false,
rightEditMode: false,
selectedTreeKey: "",
rightResource: null,
2026-05-09 03:23:57 +00:00
};
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();
2026-05-09 08:32:17 +00:00
const sidebar = $("#sidebar");
const resultPanel = $("#result-panel");
const templatePanel = $("#template-panel");
2026-05-09 03:23:57 +00:00
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: {
2026-05-09 08:32:17 +00:00
sidebarWidth: Math.round($("#sidebar").getBoundingClientRect().width),
resultWidth: Math.round($("#result-panel").getBoundingClientRect().width),
templateWidth: Math.round($("#template-panel").getBoundingClientRect().width),
2026-05-09 03:23:57 +00:00
},
});
}
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;
}
2026-05-09 08:32:17 +00:00
function isMarkdownFile(name = "") {
return name.toLowerCase().endsWith(".md");
}
2026-05-09 03:23:57 +00:00
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}`;
2026-05-09 08:32:17 +00:00
meta.textContent =
`ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 原始文件:` +
`${meeting.original_filename || meeting.transcript_filename || "未知"}`;
2026-05-09 03:23:57 +00:00
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";
}
2026-05-09 09:08:32 +00:00
function setRightStatus(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 resetRightStream() {
$("#side-status-stream").textContent = "";
}
function updateRightStream(text) {
const lines = text.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim() !== "");
$("#side-status-stream").textContent = lines.slice(-2).join("\n");
}
2026-05-09 03:23:57 +00:00
function resetProcessingStream() {
$("#stream-box").style.display = "none";
$("#stream-title").textContent = "";
$("#stream-content").textContent = "";
2026-05-09 09:08:32 +00:00
setRightStatus(false, "空闲");
resetRightStream();
2026-05-09 03:23:57 +00:00
}
2026-05-09 08:32:17 +00:00
function showResultEmpty() {
state.resultEditMode = false;
$("#btn-toggle-result-edit").disabled = true;
2026-05-09 09:08:32 +00:00
$("#btn-toggle-result-edit").textContent = "编辑";
2026-05-09 08:32:17 +00:00
$("#result-editor").style.display = "none";
$("#result-md").style.display = "none";
2026-05-09 03:23:57 +00:00
$("#processing-indicator").hidden = true;
2026-05-09 08:32:17 +00:00
$("#result-empty").hidden = false;
2026-05-09 03:23:57 +00:00
}
2026-05-09 08:32:17 +00:00
function setResultEditMode(editMode) {
state.resultEditMode = editMode;
$("#result-editor").style.display = editMode ? "block" : "none";
$("#result-md").style.display = editMode ? "none" : "block";
2026-05-09 09:08:32 +00:00
$("#btn-toggle-result-edit").textContent = editMode ? "保存" : "编辑";
2026-05-09 03:23:57 +00:00
}
2026-05-09 08:32:17 +00:00
function showResult(markdown) {
2026-05-09 03:23:57 +00:00
resetProcessingStream();
$("#processing-indicator").hidden = true;
2026-05-09 08:32:17 +00:00
$("#result-empty").hidden = true;
$("#result-editor").value = markdown;
$("#result-md").innerHTML = marked.parse(markdown || "");
$("#result-md").scrollTop = 0;
$("#btn-toggle-result-edit").disabled = !state.meetingId;
setResultEditMode(false);
2026-05-09 03:23:57 +00:00
}
2026-05-09 08:32:17 +00:00
function showProcessingView() {
2026-05-09 03:23:57 +00:00
$("#result-empty").hidden = true;
2026-05-09 08:32:17 +00:00
$("#result-editor").style.display = "none";
2026-05-09 03:23:57 +00:00
$("#result-md").style.display = "none";
2026-05-09 08:32:17 +00:00
$("#processing-indicator").hidden = false;
$("#btn-toggle-result-edit").disabled = true;
2026-05-09 03:23:57 +00:00
}
2026-05-09 08:32:17 +00:00
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);
});
2026-05-09 03:23:57 +00:00
}
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);
$("#btn-process").disabled = !state.meetingId || state.processing;
2026-05-09 08:32:17 +00:00
setSelectedTreeKey(state.selectedTreeKey);
2026-05-09 03:23:57 +00:00
}
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`;
2026-05-09 08:32:17 +00:00
if (node.id) {
row.dataset.meetingId = node.id;
}
2026-05-09 03:23:57 +00:00
if (node.type === "folder") {
const arrow = document.createElement("span");
arrow.className = `arrow ${node.children?.length ? "" : "none"}`;
2026-05-09 08:32:17 +00:00
arrow.textContent = "▸";
2026-05-09 03:23:57 +00:00
row.appendChild(arrow);
const icon = document.createElement("span");
icon.className = "icon";
2026-05-09 08:32:17 +00:00
icon.textContent = node.id ? "📁" : "🗂";
2026-05-09 03:23:57 +00:00
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) {
2026-05-09 08:32:17 +00:00
row.dataset.nodeKey = `meeting:${node.id}`;
2026-05-09 03:23:57 +00:00
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);
2026-05-09 08:32:17 +00:00
} else {
row.dataset.nodeKey = `folder:${node.name}`;
2026-05-09 03:23:57 +00:00
}
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);
2026-05-09 08:32:17 +00:00
} else {
setSelectedTreeKey(row.dataset.nodeKey);
2026-05-09 03:23:57 +00:00
}
});
} else {
2026-05-09 08:32:17 +00:00
row.dataset.nodeKey = `file:${node.path}`;
2026-05-09 03:23:57 +00:00
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")) {
2026-05-09 08:32:17 +00:00
icon.textContent = "🧩";
} else if (node.name.endsWith(".yaml") || node.name.endsWith(".yml")) {
icon.textContent = "⚙️";
2026-05-09 03:23:57 +00:00
} else {
icon.textContent = "📄";
}
row.appendChild(icon);
const label = document.createElement("span");
label.className = "label";
label.textContent = node.name;
row.appendChild(label);
2026-05-09 08:52:09 +00:00
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);
}
2026-05-09 03:23:57 +00:00
row.addEventListener("click", async () => {
2026-05-09 08:32:17 +00:00
await openTreeResource(node.path);
2026-05-09 03:23:57 +00:00
});
}
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));
2026-05-09 08:32:17 +00:00
setSelectedTreeKey(state.selectedTreeKey);
2026-05-09 03:23:57 +00:00
}
async function loadTree() {
const tree = await api("/api/tree");
buildTree(tree);
}
2026-05-09 08:32:17 +00:00
async function loadMeetingSummary(meetingId) {
const result = await api(`/api/meetings/${meetingId}/file/meeting_summary.md`);
showResult(result.content);
}
2026-05-09 03:23:57 +00:00
async function selectMeeting(meetingId) {
if (state.processing) {
return;
}
await setCurrentMeeting(meetingId);
2026-05-09 08:32:17 +00:00
setSelectedTreeKey(`meeting:${meetingId}`);
2026-05-09 03:23:57 +00:00
try {
2026-05-09 08:32:17 +00:00
await loadMeetingSummary(meetingId);
2026-05-09 03:23:57 +00:00
} catch {
2026-05-09 08:32:17 +00:00
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;
}
2026-05-09 08:52:09 +00:00
function updateGuideButton(resource) {
const button = $("#btn-reparse-guide");
if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) {
button.disabled = true;
2026-05-09 09:08:32 +00:00
button.textContent = "解析";
2026-05-09 08:52:09 +00:00
return;
}
const hasGuide = resource.type === "template-guide" || Boolean(resource.hasGuide);
button.disabled = false;
2026-05-09 09:08:32 +00:00
button.textContent = hasGuide ? "重解析" : "解析";
2026-05-09 08:52:09 +00:00
}
2026-05-09 08:32:17 +00:00
function applyRightResource(resource) {
state.rightResource = resource;
$("#editor-resource-label").textContent = `当前资源:${resource.label}`;
$("#side-editor").value = resource.content || "";
$("#btn-toggle-side-edit").disabled = !resource.editable;
2026-05-09 09:08:32 +00:00
$("#btn-toggle-side-edit").textContent = resource.editable ? "编辑" : "只读";
2026-05-09 08:52:09 +00:00
updateGuideButton(resource);
2026-05-09 08:32:17 +00:00
state.rightEditMode = false;
renderSidePreview(resource);
2026-05-09 08:52:09 +00:00
if (resource.type === "template" || resource.type === "template-guide") {
syncTemplateSelection(resource.templateName || resource.name);
2026-05-09 03:23:57 +00:00
}
}
2026-05-09 08:32:17 +00:00
function setRightEditMode(editMode) {
state.rightEditMode = editMode;
const resource = state.rightResource;
if (!resource || !resource.editable) {
renderSidePreview(resource);
return;
}
2026-05-09 09:08:32 +00:00
$("#btn-toggle-side-edit").textContent = editMode ? "保存" : "编辑";
2026-05-09 08:32:17 +00:00
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,
2026-05-09 08:52:09 +00:00
templateName: name,
2026-05-09 08:32:17 +00:00
label: `模板 / ${name}`,
content: data.content,
2026-05-09 08:52:09 +00:00
hasGuide: Boolean(data.has_guide),
editable: true,
treeKey,
});
}
async function openTemplateGuide(name, treeKey = `file:template_guides/${name}`) {
2026-05-09 09:08:32 +00:00
const data = await api(`/api/templates/${encodeURIComponent(name)}/guide`);
2026-05-09 08:52:09 +00:00
await openRightResource({
type: "template-guide",
name,
2026-05-09 09:08:32 +00:00
templateName: name,
label: `模板说明 / ${name}`,
2026-05-09 08:52:09 +00:00
content: data.content,
hasGuide: true,
2026-05-09 08:32:17 +00:00
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) {
2026-05-09 03:23:57 +00:00
if (state.processing) {
return;
}
2026-05-09 08:32:17 +00:00
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;
}
2026-05-09 08:52:09 +00:00
if (group === "template_guides") {
await openTemplateGuide(parts.slice(1).join("/"), treeKey);
return;
}
2026-05-09 08:32:17 +00:00
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);
}
2026-05-09 03:23:57 +00:00
}
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)) {
2026-05-09 08:32:17 +00:00
showResultEmpty();
2026-05-09 03:23:57 +00:00
return;
}
if (!isDeleteMeeting && state.meetingId === meetingId) {
try {
2026-05-09 08:32:17 +00:00
await loadMeetingSummary(meetingId);
2026-05-09 03:23:57 +00:00
} catch {
2026-05-09 08:32:17 +00:00
showResultEmpty();
2026-05-09 03:23:57 +00:00
}
}
}
2026-05-09 08:52:09 +00:00
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();
}
2026-05-09 03:23:57 +00:00
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;
}
2026-05-09 08:32:17 +00:00
2026-05-09 03:23:57 +00:00
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");
2026-05-09 08:32:17 +00:00
state.templates = templateData;
2026-05-09 03:23:57 +00:00
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;
}
2026-05-09 08:32:17 +00:00
if (state.templateName) {
syncTemplateSelection(state.templateName);
}
2026-05-09 03:23:57 +00:00
const meetingsData = await api("/api/meetings");
state.meetings = meetingsData.meetings || [];
state.meetingId = meetingsData.active_meeting_id || null;
await loadTree();
2026-05-09 08:32:17 +00:00
renderMeetingStatus(meetingById(state.meetingId));
2026-05-09 03:23:57 +00:00
$("#btn-process").disabled = !state.meetingId || state.processing;
2026-05-09 08:32:17 +00:00
if (state.meetingId) {
2026-05-09 03:23:57 +00:00
try {
2026-05-09 08:32:17 +00:00
await loadMeetingSummary(state.meetingId);
2026-05-09 03:23:57 +00:00
} catch {
2026-05-09 08:32:17 +00:00
showResultEmpty();
2026-05-09 03:23:57 +00:00
}
} else {
2026-05-09 08:32:17 +00:00
showResultEmpty();
}
if (
state.rightResource &&
state.rightResource.type === "template" &&
templateData.some((item) => item.name === state.rightResource.name)
) {
await openTemplate(state.rightResource.name, state.rightResource.treeKey);
return;
}
2026-05-09 08:52:09 +00:00
if (
state.rightResource &&
state.rightResource.type === "template-guide" &&
templateData.some((item) => item.name === state.rightResource.templateName)
) {
await openTemplateGuide(state.rightResource.name, state.rightResource.treeKey);
return;
}
2026-05-09 08:32:17 +00:00
if (!state.rightResource && state.templateName) {
await openTemplate(state.templateName);
2026-05-09 03:23:57 +00:00
}
}
$("#btn-process").addEventListener("click", () => {
if (!state.meetingId || state.processing) {
return;
}
state.processing = true;
$("#btn-process").disabled = true;
2026-05-09 09:08:32 +00:00
$("#btn-process").textContent = "处理中";
2026-05-09 03:23:57 +00:00
showProcessingView();
$("#stream-box").style.display = "block";
$("#stream-title").textContent = "第一阶段:结构化主题...";
$("#stream-content").textContent = "";
2026-05-09 09:08:32 +00:00
setRightStatus(true, "工作中");
resetRightStream();
2026-05-09 03:23:57 +00:00
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 = "";
2026-05-09 09:08:32 +00:00
resetRightStream();
2026-05-09 03:23:57 +00:00
}
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");
2026-05-09 09:08:32 +00:00
updateRightStream(streamAcc);
2026-05-09 03:23:57 +00:00
if (data.stage === 2 && data.chunk_type === "content") {
resultAcc += data.text || "";
}
return;
}
if (payload.type === "done") {
source.close();
state.processing = false;
$("#btn-process").disabled = false;
2026-05-09 09:08:32 +00:00
$("#btn-process").textContent = "处理";
2026-05-09 03:23:57 +00:00
showResult(payload.data?.result || resultAcc || "");
await refresh();
toast("会议处理完成");
return;
}
if (payload.type === "error") {
source.close();
state.processing = false;
$("#btn-process").disabled = false;
2026-05-09 09:08:32 +00:00
$("#btn-process").textContent = "处理";
2026-05-09 03:23:57 +00:00
resetProcessingStream();
$("#processing-indicator").hidden = true;
toast(`处理失败:${payload.data}`, "err");
}
};
source.onerror = () => {
source.close();
state.processing = false;
$("#btn-process").disabled = false;
2026-05-09 09:08:32 +00:00
$("#btn-process").textContent = "处理";
2026-05-09 03:23:57 +00:00
resetProcessingStream();
$("#processing-indicator").hidden = true;
toast("处理连接中断", "err");
};
});
2026-05-09 08:32:17 +00:00
$("#btn-toggle-result-edit").addEventListener("click", async () => {
if (!state.meetingId) {
2026-05-09 03:23:57 +00:00
return;
}
2026-05-09 08:32:17 +00:00
if (!state.resultEditMode) {
setResultEditMode(true);
return;
}
2026-05-09 03:23:57 +00:00
2026-05-09 08:32:17 +00:00
const content = $("#result-editor").value;
2026-05-09 09:08:32 +00:00
$("#btn-toggle-result-edit").disabled = true;
2026-05-09 08:32:17 +00:00
await api(`/api/meetings/${state.meetingId}/summary`, {
2026-05-09 03:23:57 +00:00
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
2026-05-09 08:32:17 +00:00
showResult(content);
2026-05-09 09:08:32 +00:00
$("#btn-toggle-result-edit").disabled = false;
toast("结果已保存");
2026-05-09 03:23:57 +00:00
});
2026-05-09 08:32:17 +00:00
$("#btn-toggle-side-edit").addEventListener("click", async () => {
const resource = state.rightResource;
if (!resource || !resource.editable) {
return;
}
2026-05-09 03:23:57 +00:00
2026-05-09 08:32:17 +00:00
if (!state.rightEditMode) {
setRightEditMode(true);
2026-05-09 03:23:57 +00:00
return;
}
2026-05-09 08:32:17 +00:00
const content = $("#side-editor").value;
2026-05-09 09:08:32 +00:00
$("#btn-toggle-side-edit").disabled = true;
2026-05-09 08:32:17 +00:00
if (resource.type === "template") {
await api(`/api/templates/${encodeURIComponent(resource.name)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
2026-05-09 08:52:09 +00:00
} 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 }),
});
2026-05-09 08:32:17 +00:00
} 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);
2026-05-09 09:08:32 +00:00
$("#btn-toggle-side-edit").disabled = false;
2026-05-09 08:32:17 +00:00
toast("资源已保存");
});
$("#tpl-select").addEventListener("change", async (event) => {
syncTemplateSelection(event.target.value);
await openTemplate(event.target.value);
});
2026-05-09 08:52:09 +00:00
$("#btn-reparse-guide").addEventListener("click", async () => {
const resource = state.rightResource;
if (!resource || (resource.type !== "template" && resource.type !== "template-guide")) {
return;
}
const templateName = resource.templateName || resource.name;
2026-05-09 09:08:32 +00:00
$("#btn-reparse-guide").disabled = true;
setRightStatus(true, "工作中");
resetRightStream();
2026-05-09 08:52:09 +00:00
const result = await api(`/api/templates/${encodeURIComponent(templateName)}/guide/reparse`, {
method: "POST",
});
2026-05-09 09:08:32 +00:00
setRightStatus(false, "空闲");
$("#btn-reparse-guide").disabled = false;
toast(`说明已更新:${templateName}`);
2026-05-09 08:52:09 +00:00
await refresh();
await openTemplateGuide(result.name);
});
2026-05-09 03:23:57 +00:00
$("#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 () => {
2026-05-09 08:32:17 +00:00
$("#sidebar").dataset.minWidth = "260";
$("#result-panel").dataset.minWidth = "360";
$("#template-panel").dataset.minWidth = "360";
2026-05-09 03:23:57 +00:00
applySavedLayout();
initResize("gutter-1", "sidebar", "result-panel");
initResize("gutter-2", "result-panel", "template-panel");
2026-05-09 09:08:32 +00:00
setRightStatus(false, "空闲");
resetRightStream();
2026-05-09 03:23:57 +00:00
try {
await refresh();
} catch (error) {
toast(error.message, "err");
}
});