统一展示逻辑“
parent
74f65ae83a
commit
30f8d3cc9c
|
|
@ -5,9 +5,12 @@ const state = {
|
||||||
meetingId: null,
|
meetingId: null,
|
||||||
meetings: [],
|
meetings: [],
|
||||||
templateName: "template1.md",
|
templateName: "template1.md",
|
||||||
editMode: false,
|
templates: [],
|
||||||
processing: false,
|
processing: false,
|
||||||
activeTab: "template",
|
resultEditMode: false,
|
||||||
|
rightEditMode: false,
|
||||||
|
selectedTreeKey: "",
|
||||||
|
rightResource: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "meeting-workspace-preferences";
|
const STORAGE_KEY = "meeting-workspace-preferences";
|
||||||
|
|
@ -28,61 +31,37 @@ function savePreferences(partial) {
|
||||||
|
|
||||||
function applySavedLayout() {
|
function applySavedLayout() {
|
||||||
const prefs = loadPreferences();
|
const prefs = loadPreferences();
|
||||||
const sidebar = document.getElementById("sidebar");
|
const sidebar = $("#sidebar");
|
||||||
const resultPanel = document.getElementById("result-panel");
|
const resultPanel = $("#result-panel");
|
||||||
const templatePanel = document.getElementById("template-panel");
|
const templatePanel = $("#template-panel");
|
||||||
|
|
||||||
if (prefs.layout?.sidebarWidth) {
|
if (prefs.layout?.sidebarWidth) {
|
||||||
sidebar.style.flexBasis = `${prefs.layout.sidebarWidth}px`;
|
sidebar.style.flexBasis = `${prefs.layout.sidebarWidth}px`;
|
||||||
sidebar.style.flexGrow = "0";
|
sidebar.style.flexGrow = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.layout?.templateWidth) {
|
if (prefs.layout?.templateWidth) {
|
||||||
templatePanel.style.flexBasis = `${prefs.layout.templateWidth}px`;
|
templatePanel.style.flexBasis = `${prefs.layout.templateWidth}px`;
|
||||||
templatePanel.style.flexGrow = "0";
|
templatePanel.style.flexGrow = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.layout?.resultWidth) {
|
if (prefs.layout?.resultWidth) {
|
||||||
resultPanel.style.flexBasis = `${prefs.layout.resultWidth}px`;
|
resultPanel.style.flexBasis = `${prefs.layout.resultWidth}px`;
|
||||||
resultPanel.style.flexGrow = "0";
|
resultPanel.style.flexGrow = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.templateName) {
|
if (prefs.templateName) {
|
||||||
state.templateName = prefs.templateName;
|
state.templateName = prefs.templateName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.activeTab === "template" || prefs.activeTab === "original") {
|
|
||||||
state.activeTab = prefs.activeTab;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistLayout() {
|
function persistLayout() {
|
||||||
const sidebar = document.getElementById("sidebar");
|
|
||||||
const resultPanel = document.getElementById("result-panel");
|
|
||||||
const templatePanel = document.getElementById("template-panel");
|
|
||||||
|
|
||||||
savePreferences({
|
savePreferences({
|
||||||
layout: {
|
layout: {
|
||||||
sidebarWidth: Math.round(sidebar.getBoundingClientRect().width),
|
sidebarWidth: Math.round($("#sidebar").getBoundingClientRect().width),
|
||||||
resultWidth: Math.round(resultPanel.getBoundingClientRect().width),
|
resultWidth: Math.round($("#result-panel").getBoundingClientRect().width),
|
||||||
templateWidth: Math.round(templatePanel.getBoundingClientRect().width),
|
templateWidth: Math.round($("#template-panel").getBoundingClientRect().width),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateTab(tabName) {
|
|
||||||
state.activeTab = tabName;
|
|
||||||
$$("#panel-tabs .tab").forEach((item) => {
|
|
||||||
item.classList.toggle("active", item.dataset.tab === tabName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showTemplate = tabName === "template";
|
|
||||||
$("#template-tab").hidden = !showTemplate;
|
|
||||||
$("#original-tab").hidden = showTemplate;
|
|
||||||
|
|
||||||
savePreferences({ activeTab: tabName });
|
|
||||||
}
|
|
||||||
|
|
||||||
function toast(message, type = "ok") {
|
function toast(message, type = "ok") {
|
||||||
const el = $("#toast");
|
const el = $("#toast");
|
||||||
el.textContent = message;
|
el.textContent = message;
|
||||||
|
|
@ -112,6 +91,10 @@ function meetingById(meetingId) {
|
||||||
return state.meetings.find((item) => item.id === meetingId) || null;
|
return state.meetings.find((item) => item.id === meetingId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMarkdownFile(name = "") {
|
||||||
|
return name.toLowerCase().endsWith(".md");
|
||||||
|
}
|
||||||
|
|
||||||
function renderMeetingStatus(meeting) {
|
function renderMeetingStatus(meeting) {
|
||||||
const name = $("#sidebar-meeting-name");
|
const name = $("#sidebar-meeting-name");
|
||||||
const meta = $("#sidebar-meeting-meta");
|
const meta = $("#sidebar-meeting-meta");
|
||||||
|
|
@ -131,7 +114,9 @@ function renderMeetingStatus(meeting) {
|
||||||
}
|
}
|
||||||
|
|
||||||
name.textContent = `当前会议:${meeting.name}`;
|
name.textContent = `当前会议:${meeting.name}`;
|
||||||
meta.textContent = `ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 源文件:${meeting.original_filename || meeting.transcript_filename || "未知"}`;
|
meta.textContent =
|
||||||
|
`ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 原始文件:` +
|
||||||
|
`${meeting.original_filename || meeting.transcript_filename || "未知"}`;
|
||||||
tip.textContent = `当前处理会议:${meeting.name} (${meeting.id})`;
|
tip.textContent = `当前处理会议:${meeting.name} (${meeting.id})`;
|
||||||
summaryBadge.textContent = meeting.has_summary ? "已生成总结" : "未生成总结";
|
summaryBadge.textContent = meeting.has_summary ? "已生成总结" : "未生成总结";
|
||||||
topicsBadge.textContent = meeting.has_topics ? "已生成主题 JSON" : "未生成主题 JSON";
|
topicsBadge.textContent = meeting.has_topics ? "已生成主题 JSON" : "未生成主题 JSON";
|
||||||
|
|
@ -145,48 +130,48 @@ function resetProcessingStream() {
|
||||||
$("#stream-content").textContent = "";
|
$("#stream-content").textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showResultEmpty() {
|
||||||
|
state.resultEditMode = false;
|
||||||
|
$("#btn-toggle-result-edit").disabled = true;
|
||||||
|
$("#btn-toggle-result-edit").textContent = "编辑结果";
|
||||||
|
$("#result-editor").style.display = "none";
|
||||||
|
$("#result-md").style.display = "none";
|
||||||
|
$("#processing-indicator").hidden = true;
|
||||||
|
$("#result-empty").hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
function showResult(markdown) {
|
||||||
resetProcessingStream();
|
resetProcessingStream();
|
||||||
$("#processing-indicator").hidden = true;
|
$("#processing-indicator").hidden = true;
|
||||||
$("#result-empty").hidden = true;
|
$("#result-empty").hidden = true;
|
||||||
const result = $("#result-md");
|
$("#result-editor").value = markdown;
|
||||||
result.style.display = "block";
|
$("#result-md").innerHTML = marked.parse(markdown || "");
|
||||||
result.innerHTML = marked.parse(markdown);
|
$("#result-md").scrollTop = 0;
|
||||||
result.scrollTop = 0;
|
$("#btn-toggle-result-edit").disabled = !state.meetingId;
|
||||||
|
setResultEditMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showProcessingView() {
|
function showProcessingView() {
|
||||||
$("#result-empty").hidden = true;
|
$("#result-empty").hidden = true;
|
||||||
|
$("#result-editor").style.display = "none";
|
||||||
$("#result-md").style.display = "none";
|
$("#result-md").style.display = "none";
|
||||||
$("#processing-indicator").hidden = false;
|
$("#processing-indicator").hidden = false;
|
||||||
|
$("#btn-toggle-result-edit").disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEmpty() {
|
function setSelectedTreeKey(key) {
|
||||||
resetProcessingStream();
|
state.selectedTreeKey = key;
|
||||||
$("#processing-indicator").hidden = true;
|
$$(".tree-row").forEach((row) => {
|
||||||
$("#result-md").style.display = "none";
|
row.classList.toggle("selected", row.dataset.nodeKey === key);
|
||||||
$("#result-empty").hidden = false;
|
row.classList.toggle("active-meeting", row.dataset.meetingId === state.meetingId);
|
||||||
}
|
});
|
||||||
|
|
||||||
function showResultPanel() {
|
|
||||||
resetProcessingStream();
|
|
||||||
$("#result-empty").hidden = true;
|
|
||||||
$("#processing-indicator").hidden = true;
|
|
||||||
$("#result-md").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDisplayContent(filename, content) {
|
|
||||||
if (filename.endsWith(".json")) {
|
|
||||||
try {
|
|
||||||
return `\`\`\`json\n${JSON.stringify(JSON.parse(content), null, 2)}\n\`\`\``;
|
|
||||||
} catch {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filename.endsWith(".txt")) {
|
|
||||||
return `\`\`\`\n${content}\n\`\`\``;
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCurrentMeeting(meetingId) {
|
async function setCurrentMeeting(meetingId) {
|
||||||
|
|
@ -198,15 +183,7 @@ async function setCurrentMeeting(meetingId) {
|
||||||
state.meetingId = data.active_meeting_id;
|
state.meetingId = data.active_meeting_id;
|
||||||
renderMeetingStatus(data.meeting);
|
renderMeetingStatus(data.meeting);
|
||||||
$("#btn-process").disabled = !state.meetingId || state.processing;
|
$("#btn-process").disabled = !state.meetingId || state.processing;
|
||||||
highlightSelectedMeeting();
|
setSelectedTreeKey(state.selectedTreeKey);
|
||||||
}
|
|
||||||
|
|
||||||
function highlightSelectedMeeting() {
|
|
||||||
$$(".tree-row").forEach((row) => {
|
|
||||||
const isCurrent = row.dataset.meetingId === state.meetingId;
|
|
||||||
row.classList.toggle("selected", isCurrent);
|
|
||||||
row.classList.toggle("active-meeting", isCurrent);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNode(node, parent, depth) {
|
function renderNode(node, parent, depth) {
|
||||||
|
|
@ -217,15 +194,19 @@ function renderNode(node, parent, depth) {
|
||||||
row.className = "tree-row";
|
row.className = "tree-row";
|
||||||
row.style.paddingLeft = `${10 + depth * 18}px`;
|
row.style.paddingLeft = `${10 + depth * 18}px`;
|
||||||
|
|
||||||
|
if (node.id) {
|
||||||
|
row.dataset.meetingId = node.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (node.type === "folder") {
|
if (node.type === "folder") {
|
||||||
const arrow = document.createElement("span");
|
const arrow = document.createElement("span");
|
||||||
arrow.className = `arrow ${node.children?.length ? "" : "none"}`;
|
arrow.className = `arrow ${node.children?.length ? "" : "none"}`;
|
||||||
arrow.textContent = "▶";
|
arrow.textContent = "▸";
|
||||||
row.appendChild(arrow);
|
row.appendChild(arrow);
|
||||||
|
|
||||||
const icon = document.createElement("span");
|
const icon = document.createElement("span");
|
||||||
icon.className = "icon";
|
icon.className = "icon";
|
||||||
icon.textContent = node.id ? "🗂" : "📁";
|
icon.textContent = node.id ? "📁" : "🗂";
|
||||||
row.appendChild(icon);
|
row.appendChild(icon);
|
||||||
|
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
|
|
@ -241,7 +222,7 @@ function renderNode(node, parent, depth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.id) {
|
if (node.id) {
|
||||||
row.dataset.meetingId = node.id;
|
row.dataset.nodeKey = `meeting:${node.id}`;
|
||||||
const del = document.createElement("span");
|
const del = document.createElement("span");
|
||||||
del.className = "del-btn";
|
del.className = "del-btn";
|
||||||
del.textContent = "删除";
|
del.textContent = "删除";
|
||||||
|
|
@ -250,6 +231,8 @@ function renderNode(node, parent, depth) {
|
||||||
await deleteMeetingNode(node.id, node.delete_mode || "meeting");
|
await deleteMeetingNode(node.id, node.delete_mode || "meeting");
|
||||||
});
|
});
|
||||||
row.appendChild(del);
|
row.appendChild(del);
|
||||||
|
} else {
|
||||||
|
row.dataset.nodeKey = `folder:${node.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
row.addEventListener("click", async () => {
|
row.addEventListener("click", async () => {
|
||||||
|
|
@ -261,9 +244,13 @@ function renderNode(node, parent, depth) {
|
||||||
}
|
}
|
||||||
if (node.id) {
|
if (node.id) {
|
||||||
await selectMeeting(node.id);
|
await selectMeeting(node.id);
|
||||||
|
} else {
|
||||||
|
setSelectedTreeKey(row.dataset.nodeKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
row.dataset.nodeKey = `file:${node.path}`;
|
||||||
|
|
||||||
const spacer = document.createElement("span");
|
const spacer = document.createElement("span");
|
||||||
spacer.className = "arrow none";
|
spacer.className = "arrow none";
|
||||||
row.appendChild(spacer);
|
row.appendChild(spacer);
|
||||||
|
|
@ -273,7 +260,9 @@ function renderNode(node, parent, depth) {
|
||||||
if (node.name.endsWith(".md")) {
|
if (node.name.endsWith(".md")) {
|
||||||
icon.textContent = "📝";
|
icon.textContent = "📝";
|
||||||
} else if (node.name.endsWith(".json")) {
|
} else if (node.name.endsWith(".json")) {
|
||||||
icon.textContent = "🔎";
|
icon.textContent = "🧩";
|
||||||
|
} else if (node.name.endsWith(".yaml") || node.name.endsWith(".yml")) {
|
||||||
|
icon.textContent = "⚙️";
|
||||||
} else {
|
} else {
|
||||||
icon.textContent = "📄";
|
icon.textContent = "📄";
|
||||||
}
|
}
|
||||||
|
|
@ -285,14 +274,7 @@ function renderNode(node, parent, depth) {
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
|
|
||||||
row.addEventListener("click", async () => {
|
row.addEventListener("click", async () => {
|
||||||
if (!node.path) {
|
await openTreeResource(node.path);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parts = node.path.replace(/^(meetings|results_md|results_json)\//, "").split("/");
|
|
||||||
const meetingId = parts[0];
|
|
||||||
const filename = parts.slice(1).join("/");
|
|
||||||
await setCurrentMeeting(meetingId);
|
|
||||||
await viewMeetingFile(meetingId, filename);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,7 +297,7 @@ function buildTree(tree) {
|
||||||
const root = $("#file-tree");
|
const root = $("#file-tree");
|
||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
(tree.children || []).forEach((child) => renderNode(child, root, 0));
|
(tree.children || []).forEach((child) => renderNode(child, root, 0));
|
||||||
highlightSelectedMeeting();
|
setSelectedTreeKey(state.selectedTreeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTree() {
|
async function loadTree() {
|
||||||
|
|
@ -323,27 +305,176 @@ async function loadTree() {
|
||||||
buildTree(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) {
|
async function selectMeeting(meetingId) {
|
||||||
if (state.processing) {
|
if (state.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await setCurrentMeeting(meetingId);
|
await setCurrentMeeting(meetingId);
|
||||||
showResultPanel();
|
setSelectedTreeKey(`meeting:${meetingId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api(`/api/meetings/${meetingId}/file/meeting_summary.md`);
|
await loadMeetingSummary(meetingId);
|
||||||
showResult(result.content);
|
|
||||||
} catch {
|
} catch {
|
||||||
showEmpty();
|
showResultEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function viewMeetingFile(meetingId, filename) {
|
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 applyRightResource(resource) {
|
||||||
|
state.rightResource = resource;
|
||||||
|
$("#editor-resource-label").textContent = `当前资源:${resource.label}`;
|
||||||
|
$("#side-editor").value = resource.content || "";
|
||||||
|
$("#btn-toggle-side-edit").disabled = !resource.editable;
|
||||||
|
$("#btn-toggle-side-edit").textContent = resource.editable ? "编辑资源" : "只读资源";
|
||||||
|
state.rightEditMode = false;
|
||||||
|
renderSidePreview(resource);
|
||||||
|
|
||||||
|
if (resource.type === "template") {
|
||||||
|
syncTemplateSelection(resource.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
label: `模板 / ${name}`,
|
||||||
|
content: data.content,
|
||||||
|
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) {
|
if (state.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await api(`/api/meetings/${meetingId}/file/${encodeURIComponent(filename)}`);
|
|
||||||
showResult(toDisplayContent(filename, result.content));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async function deleteMeetingNode(meetingId, deleteMode) {
|
||||||
|
|
@ -365,74 +496,19 @@ async function deleteMeetingNode(meetingId, deleteMode) {
|
||||||
await refresh();
|
await refresh();
|
||||||
|
|
||||||
if (isDeleteMeeting && !meetingById(state.meetingId)) {
|
if (isDeleteMeeting && !meetingById(state.meetingId)) {
|
||||||
showEmpty();
|
showResultEmpty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDeleteMeeting && state.meetingId === meetingId) {
|
if (!isDeleteMeeting && state.meetingId === meetingId) {
|
||||||
try {
|
try {
|
||||||
const result = await api(`/api/meetings/${meetingId}/file/meeting_summary.md`);
|
await loadMeetingSummary(meetingId);
|
||||||
showResult(result.content);
|
|
||||||
} catch {
|
} catch {
|
||||||
showEmpty();
|
showResultEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTemplatePreview(content) {
|
|
||||||
const preview = $("#template-preview");
|
|
||||||
preview.style.display = "block";
|
|
||||||
preview.innerHTML = marked.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTemplate(name) {
|
|
||||||
state.templateName = name;
|
|
||||||
savePreferences({ templateName: name });
|
|
||||||
const data = await api(`/api/templates/${name}`);
|
|
||||||
state.editMode = false;
|
|
||||||
$("#template-editor").value = data.content;
|
|
||||||
$("#template-editor").style.display = "none";
|
|
||||||
$("#template-preview").style.display = "block";
|
|
||||||
updateTemplatePreview(data.content);
|
|
||||||
$("#btn-toggle-edit").textContent = "编辑模板";
|
|
||||||
$("#tpl-select").value = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOriginalContent(meetingId) {
|
|
||||||
const originalText = $("#original-content");
|
|
||||||
const originalMarkdown = $("#original-markdown");
|
|
||||||
|
|
||||||
if (!meetingId) {
|
|
||||||
originalMarkdown.hidden = true;
|
|
||||||
originalMarkdown.style.display = "none";
|
|
||||||
originalText.hidden = false;
|
|
||||||
originalText.textContent = "未选择会议";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = await api(`/api/meetings/${meetingId}/transcript`);
|
|
||||||
const filename = (data.filename || "").toLowerCase();
|
|
||||||
if (filename.endsWith(".md")) {
|
|
||||||
originalText.hidden = true;
|
|
||||||
originalMarkdown.hidden = false;
|
|
||||||
originalMarkdown.style.display = "block";
|
|
||||||
originalMarkdown.innerHTML = marked.parse(data.content);
|
|
||||||
originalMarkdown.scrollTop = 0;
|
|
||||||
} else {
|
|
||||||
originalMarkdown.hidden = true;
|
|
||||||
originalMarkdown.style.display = "none";
|
|
||||||
originalText.hidden = false;
|
|
||||||
originalText.textContent = data.content;
|
|
||||||
originalText.scrollTop = 0;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
originalMarkdown.hidden = true;
|
|
||||||
originalMarkdown.style.display = "none";
|
|
||||||
originalText.hidden = false;
|
|
||||||
originalText.textContent = "无法加载当前会议原文";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initResize(gutterId, leftId, rightId) {
|
function initResize(gutterId, leftId, rightId) {
|
||||||
const gutter = document.getElementById(gutterId);
|
const gutter = document.getElementById(gutterId);
|
||||||
const left = document.getElementById(leftId);
|
const left = document.getElementById(leftId);
|
||||||
|
|
@ -460,6 +536,7 @@ function initResize(gutterId, leftId, rightId) {
|
||||||
if (!dragging) {
|
if (!dragging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dx = event.clientX - startX;
|
const dx = event.clientX - startX;
|
||||||
const minLeft = Number(left.dataset.minWidth || 220);
|
const minLeft = Number(left.dataset.minWidth || 220);
|
||||||
const minRight = Number(right.dataset.minWidth || 320);
|
const minRight = Number(right.dataset.minWidth || 320);
|
||||||
|
|
@ -497,6 +574,8 @@ function initResize(gutterId, leftId, rightId) {
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const templateData = await api("/api/templates");
|
const templateData = await api("/api/templates");
|
||||||
|
state.templates = templateData;
|
||||||
|
|
||||||
const select = $("#tpl-select");
|
const select = $("#tpl-select");
|
||||||
select.innerHTML = "";
|
select.innerHTML = "";
|
||||||
templateData.forEach((item) => {
|
templateData.forEach((item) => {
|
||||||
|
|
@ -509,27 +588,39 @@ async function refresh() {
|
||||||
if (!templateData.some((item) => item.name === state.templateName) && templateData[0]) {
|
if (!templateData.some((item) => item.name === state.templateName) && templateData[0]) {
|
||||||
state.templateName = templateData[0].name;
|
state.templateName = templateData[0].name;
|
||||||
}
|
}
|
||||||
|
if (state.templateName) {
|
||||||
|
syncTemplateSelection(state.templateName);
|
||||||
|
}
|
||||||
|
|
||||||
const meetingsData = await api("/api/meetings");
|
const meetingsData = await api("/api/meetings");
|
||||||
state.meetings = meetingsData.meetings || [];
|
state.meetings = meetingsData.meetings || [];
|
||||||
state.meetingId = meetingsData.active_meeting_id || null;
|
state.meetingId = meetingsData.active_meeting_id || null;
|
||||||
|
|
||||||
await loadTree();
|
await loadTree();
|
||||||
await loadTemplate(state.templateName);
|
renderMeetingStatus(meetingById(state.meetingId));
|
||||||
|
|
||||||
const current = meetingById(state.meetingId);
|
|
||||||
renderMeetingStatus(current);
|
|
||||||
$("#btn-process").disabled = !state.meetingId || state.processing;
|
$("#btn-process").disabled = !state.meetingId || state.processing;
|
||||||
|
|
||||||
if (current) {
|
if (state.meetingId) {
|
||||||
try {
|
try {
|
||||||
const result = await api(`/api/meetings/${current.id}/file/meeting_summary.md`);
|
await loadMeetingSummary(state.meetingId);
|
||||||
showResult(result.content);
|
|
||||||
} catch {
|
} catch {
|
||||||
showEmpty();
|
showResultEmpty();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showEmpty();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.rightResource && state.templateName) {
|
||||||
|
await openTemplate(state.templateName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -613,43 +704,68 @@ $("#btn-process").addEventListener("click", () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn-toggle-edit").addEventListener("click", async () => {
|
$("#btn-toggle-result-edit").addEventListener("click", async () => {
|
||||||
state.editMode = !state.editMode;
|
if (!state.meetingId) {
|
||||||
if (state.editMode) {
|
|
||||||
$("#template-editor").style.display = "block";
|
|
||||||
$("#template-preview").style.display = "none";
|
|
||||||
$("#btn-toggle-edit").textContent = "保存模板";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = $("#template-editor").value;
|
if (!state.resultEditMode) {
|
||||||
$("#template-editor").style.display = "none";
|
setResultEditMode(true);
|
||||||
$("#template-preview").style.display = "block";
|
return;
|
||||||
$("#btn-toggle-edit").textContent = "编辑模板";
|
}
|
||||||
updateTemplatePreview(content);
|
|
||||||
|
|
||||||
await api(`/api/templates/${state.templateName}`, {
|
const content = $("#result-editor").value;
|
||||||
|
await api(`/api/meetings/${state.meetingId}/summary`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ content }),
|
body: JSON.stringify({ content }),
|
||||||
});
|
});
|
||||||
toast("模板已保存");
|
showResult(content);
|
||||||
|
await refresh();
|
||||||
|
toast("会议结果已保存");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn-toggle-side-edit").addEventListener("click", async () => {
|
||||||
|
const resource = state.rightResource;
|
||||||
|
if (!resource || !resource.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.rightEditMode) {
|
||||||
|
setRightEditMode(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = $("#side-editor").value;
|
||||||
|
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 === "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("资源已保存");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#tpl-select").addEventListener("change", async (event) => {
|
$("#tpl-select").addEventListener("change", async (event) => {
|
||||||
await loadTemplate(event.target.value);
|
syncTemplateSelection(event.target.value);
|
||||||
|
await openTemplate(event.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#panel-tabs").addEventListener("click", async (event) => {
|
$("#btn-open-template").addEventListener("click", async () => {
|
||||||
const tab = event.target.closest(".tab");
|
if (!state.templateName) {
|
||||||
if (!tab) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activateTab(tab.dataset.tab);
|
await openTemplate(state.templateName);
|
||||||
|
|
||||||
if (tab.dataset.tab === "original") {
|
|
||||||
await loadOriginalContent(state.meetingId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn-import").addEventListener("click", () => {
|
$("#btn-import").addEventListener("click", () => {
|
||||||
|
|
@ -727,9 +843,9 @@ $$("[data-close]").forEach((button) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
document.getElementById("sidebar").dataset.minWidth = "260";
|
$("#sidebar").dataset.minWidth = "260";
|
||||||
document.getElementById("result-panel").dataset.minWidth = "360";
|
$("#result-panel").dataset.minWidth = "360";
|
||||||
document.getElementById("template-panel").dataset.minWidth = "360";
|
$("#template-panel").dataset.minWidth = "360";
|
||||||
|
|
||||||
applySavedLayout();
|
applySavedLayout();
|
||||||
initResize("gutter-1", "sidebar", "result-panel");
|
initResize("gutter-1", "sidebar", "result-panel");
|
||||||
|
|
@ -737,10 +853,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refresh();
|
await refresh();
|
||||||
activateTab(state.activeTab);
|
|
||||||
if (state.activeTab === "original") {
|
|
||||||
await loadOriginalContent(state.meetingId);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(error.message, "err");
|
toast(error.message, "err");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,11 @@ textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
height: var(--app-h);
|
height: var(--app-h);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|
@ -176,8 +181,8 @@ textarea {
|
||||||
|
|
||||||
.panel-scroll,
|
.panel-scroll,
|
||||||
.tree,
|
.tree,
|
||||||
#template-tab,
|
#result-editor,
|
||||||
#original-tab {
|
#side-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -185,10 +190,11 @@ textarea {
|
||||||
.tree,
|
.tree,
|
||||||
#result-body,
|
#result-body,
|
||||||
#template-body,
|
#template-body,
|
||||||
#template-editor,
|
#result-editor,
|
||||||
#template-preview,
|
#result-md,
|
||||||
#original-content,
|
#side-editor,
|
||||||
#result-md {
|
#side-preview,
|
||||||
|
#side-plain-preview {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #8bb8f1 rgba(232, 242, 255, 0.7);
|
scrollbar-color: #8bb8f1 rgba(232, 242, 255, 0.7);
|
||||||
|
|
@ -197,10 +203,11 @@ textarea {
|
||||||
.tree::-webkit-scrollbar,
|
.tree::-webkit-scrollbar,
|
||||||
#result-body::-webkit-scrollbar,
|
#result-body::-webkit-scrollbar,
|
||||||
#template-body::-webkit-scrollbar,
|
#template-body::-webkit-scrollbar,
|
||||||
#template-editor::-webkit-scrollbar,
|
#result-editor::-webkit-scrollbar,
|
||||||
#template-preview::-webkit-scrollbar,
|
#result-md::-webkit-scrollbar,
|
||||||
#original-content::-webkit-scrollbar,
|
#side-editor::-webkit-scrollbar,
|
||||||
#result-md::-webkit-scrollbar {
|
#side-preview::-webkit-scrollbar,
|
||||||
|
#side-plain-preview::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -208,10 +215,11 @@ textarea {
|
||||||
.tree::-webkit-scrollbar-track,
|
.tree::-webkit-scrollbar-track,
|
||||||
#result-body::-webkit-scrollbar-track,
|
#result-body::-webkit-scrollbar-track,
|
||||||
#template-body::-webkit-scrollbar-track,
|
#template-body::-webkit-scrollbar-track,
|
||||||
#template-editor::-webkit-scrollbar-track,
|
#result-editor::-webkit-scrollbar-track,
|
||||||
#template-preview::-webkit-scrollbar-track,
|
#result-md::-webkit-scrollbar-track,
|
||||||
#original-content::-webkit-scrollbar-track,
|
#side-editor::-webkit-scrollbar-track,
|
||||||
#result-md::-webkit-scrollbar-track {
|
#side-preview::-webkit-scrollbar-track,
|
||||||
|
#side-plain-preview::-webkit-scrollbar-track {
|
||||||
background: rgba(232, 242, 255, 0.8);
|
background: rgba(232, 242, 255, 0.8);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
@ -219,10 +227,11 @@ textarea {
|
||||||
.tree::-webkit-scrollbar-thumb,
|
.tree::-webkit-scrollbar-thumb,
|
||||||
#result-body::-webkit-scrollbar-thumb,
|
#result-body::-webkit-scrollbar-thumb,
|
||||||
#template-body::-webkit-scrollbar-thumb,
|
#template-body::-webkit-scrollbar-thumb,
|
||||||
#template-editor::-webkit-scrollbar-thumb,
|
#result-editor::-webkit-scrollbar-thumb,
|
||||||
#template-preview::-webkit-scrollbar-thumb,
|
#result-md::-webkit-scrollbar-thumb,
|
||||||
#original-content::-webkit-scrollbar-thumb,
|
#side-editor::-webkit-scrollbar-thumb,
|
||||||
#result-md::-webkit-scrollbar-thumb {
|
#side-preview::-webkit-scrollbar-thumb,
|
||||||
|
#side-plain-preview::-webkit-scrollbar-thumb {
|
||||||
background: linear-gradient(180deg, #8bb8f1, #4f93eb);
|
background: linear-gradient(180deg, #8bb8f1, #4f93eb);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 2px solid rgba(232, 242, 255, 0.8);
|
border: 2px solid rgba(232, 242, 255, 0.8);
|
||||||
|
|
@ -484,8 +493,9 @@ select.btn {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-content,
|
.stream-content,
|
||||||
#original-content,
|
#result-editor,
|
||||||
#template-editor {
|
#side-editor,
|
||||||
|
#side-plain-preview {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -523,59 +533,39 @@ select.btn {
|
||||||
}
|
}
|
||||||
|
|
||||||
#result-md,
|
#result-md,
|
||||||
#template-preview,
|
#side-preview,
|
||||||
#original-markdown,
|
#side-plain-preview,
|
||||||
#original-content {
|
#result-editor,
|
||||||
|
#side-editor {
|
||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#result-md,
|
#result-md,
|
||||||
#template-preview,
|
#side-preview,
|
||||||
#original-markdown {
|
#side-plain-preview,
|
||||||
|
#result-editor,
|
||||||
|
#side-editor {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#template-editor {
|
#result-editor,
|
||||||
|
#side-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 18px 20px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#original-content {
|
#side-plain-preview {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-tabs {
|
|
||||||
display: inline-flex;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(232, 242, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
background: rgba(47, 128, 237, 0.15);
|
|
||||||
color: var(--accent-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-content {
|
.md-content {
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<aside class="panel" id="sidebar">
|
<aside class="panel" id="sidebar">
|
||||||
<div class="panel-header sidebar-header">
|
<div class="panel-header sidebar-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>会议资源</span>
|
<span>资源管理器</span>
|
||||||
<small id="sidebar-meeting-name">当前会议:未选择</small>
|
<small id="sidebar-meeting-name">当前会议:未选择</small>
|
||||||
<small id="sidebar-meeting-meta">请从左侧选择一个会议开始处理。</small>
|
<small id="sidebar-meeting-meta">请从左侧选择一个会议开始处理。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,10 +43,13 @@
|
||||||
<section class="panel panel-main" id="result-panel">
|
<section class="panel panel-main" id="result-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>处理结果</span>
|
<span>会议结果</span>
|
||||||
<small id="selected-meeting-tip">未选择会议</small>
|
<small id="selected-meeting-tip">未选择会议</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
<button class="btn primary sm" id="btn-process" disabled>处理当前会议</button>
|
<button class="btn primary sm" id="btn-process" disabled>处理当前会议</button>
|
||||||
|
<button class="btn sm" id="btn-toggle-result-edit" disabled>编辑结果</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body panel-scroll" id="result-body">
|
<div class="panel-body panel-scroll" id="result-body">
|
||||||
<div id="result-empty" class="empty-state">
|
<div id="result-empty" class="empty-state">
|
||||||
|
|
@ -59,6 +62,7 @@
|
||||||
<pre class="stream-content" id="stream-content"></pre>
|
<pre class="stream-content" id="stream-content"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<textarea id="result-editor" spellcheck="false"></textarea>
|
||||||
<article id="result-md" class="md-content"></article>
|
<article id="result-md" class="md-content"></article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -68,27 +72,20 @@
|
||||||
<section class="panel" id="template-panel">
|
<section class="panel" id="template-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span>模板与原文</span>
|
<span>右侧编辑区</span>
|
||||||
<small>每个分区独立滚动,页面整体固定</small>
|
<small id="editor-resource-label">当前资源:模板</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="panel-tabs" id="panel-tabs">
|
<label class="inline-label" for="tpl-select">模板选择</label>
|
||||||
<button class="tab active" data-tab="template">模板</button>
|
|
||||||
<button class="tab" data-tab="original">原文</button>
|
|
||||||
</div>
|
|
||||||
<select class="btn sm" id="tpl-select"></select>
|
<select class="btn sm" id="tpl-select"></select>
|
||||||
<button class="btn sm" id="btn-toggle-edit">编辑模板</button>
|
<button class="btn sm" id="btn-open-template">打开模板</button>
|
||||||
|
<button class="btn sm" id="btn-toggle-side-edit" disabled>编辑资源</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body panel-scroll" id="template-body">
|
<div class="panel-body panel-scroll" id="template-body">
|
||||||
<section id="template-tab">
|
<textarea id="side-editor" spellcheck="false"></textarea>
|
||||||
<textarea id="template-editor" spellcheck="false"></textarea>
|
<article id="side-preview" class="md-content"></article>
|
||||||
<article id="template-preview" class="md-content"></article>
|
<pre id="side-plain-preview"></pre>
|
||||||
</section>
|
|
||||||
<section id="original-tab" hidden>
|
|
||||||
<article id="original-markdown" class="md-content" hidden></article>
|
|
||||||
<pre id="original-content"></pre>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
system:
|
||||||
|
role: |
|
||||||
|
你是一名模板规则解析助手。你的任务不是复述模板内容,而是从会议纪要模板中提炼“后续生成时必须遵守的固定规则”。
|
||||||
|
这些规则既可能来自模板末尾的使用说明,也可能隐藏在模板正文的占位符、示例内容、标题层级、列表形式和删留逻辑中。
|
||||||
|
你要把这些规则整理成一份可长期复用的模板要求,供另一个总结模型直接使用。
|
||||||
|
|
||||||
|
mode_contracts:
|
||||||
|
parse_template_requirements: |
|
||||||
|
请根据模板内容抽取“可通用于后续生成”的固定要求,规则如下:
|
||||||
|
|
||||||
|
识别范围:
|
||||||
|
- 优先识别模板中的显式说明,例如“说明”“使用说明”“输出要求”等
|
||||||
|
- 如果没有显式说明,也必须从模板正文中提炼隐式规则
|
||||||
|
- 隐式规则重点包括:
|
||||||
|
- 标题层级和整体结构保留规则
|
||||||
|
- 占位符替换规则
|
||||||
|
- 示例文本是否需要替换或删除
|
||||||
|
- 哪些空白条目、小节、子项在无内容时应省略
|
||||||
|
- 标题、分组、条目数量是否允许按实际内容增减
|
||||||
|
- 问答、决策、行动项、引用、复选框等格式是否需要保留
|
||||||
|
|
||||||
|
尤其注意以下常见占位/示例模式,并在输出中转成通用规则:
|
||||||
|
- `X`、`XX`、`XXX`
|
||||||
|
- `[占位内容]`
|
||||||
|
- `Q:`、`A:`
|
||||||
|
- `- [ ]`
|
||||||
|
- 引号中的示例句子
|
||||||
|
- 括号中的说明性示例
|
||||||
|
- 明显只是演示结构的“主题1 / 主题2 / 主题3”等文字
|
||||||
|
|
||||||
|
提炼原则:
|
||||||
|
- 不要照抄整份模板,不要把模板正文逐段改写成摘要
|
||||||
|
- 只保留“能约束后续生成结果”的规则
|
||||||
|
- 规则要尽量写成通用表达,不要绑定某个具体模板里的业务词
|
||||||
|
- 如果模板中出现“X/XX/XXX、[占位内容]、示例标题、示例问答、示例行动项”等,应明确提炼出“这些内容需要替换为真实内容,不能原样输出”的规则
|
||||||
|
- 如果模板里有未被会议内容支撑的空白项、占位项、示例项,应提炼出“无依据时省略,不强行补齐”的规则
|
||||||
|
- 如果模板允许子项数量增减、标题动态改写、按实际内容调整分组,应明确写出
|
||||||
|
- 如果某些格式本身具有结构意义,例如 Markdown 层级、问答对、行动项复选框、引用块,应提炼出“在有内容时保留该格式”的规则
|
||||||
|
- 如果某条内容只是模板主题本身,不是生成约束,不要保留
|
||||||
|
|
||||||
|
输出要求:
|
||||||
|
- 不输出 JSON
|
||||||
|
- 输出为中文纯文本
|
||||||
|
- 每行一条规则,以 `- ` 开头
|
||||||
|
- 优先输出 6 到 12 条高价值规则
|
||||||
|
- 规则表述简洁、明确、可直接给下游模型使用
|
||||||
|
|
||||||
|
user_template:
|
||||||
|
template_input: |
|
||||||
|
请解析下面模板的固定要求,输出一组可复用的模板规则。
|
||||||
|
|
||||||
|
模板名:
|
||||||
|
{template_name}
|
||||||
|
|
||||||
|
模板内容:
|
||||||
|
{template_content}
|
||||||
245
web/server.py
245
web/server.py
|
|
@ -10,11 +10,10 @@ from pathlib import Path
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Request
|
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from agents.chat import get_qwen_response
|
|
||||||
from prompt_loader import load_prompt
|
from prompt_loader import load_prompt
|
||||||
|
|
||||||
FRONTEND_DIR = PROJECT_ROOT / "frontend"
|
FRONTEND_DIR = PROJECT_ROOT / "frontend"
|
||||||
|
|
@ -24,6 +23,7 @@ DATA_DIR = DATA_ROOT / "meetings"
|
||||||
RESULTS_MD_DIR = PROJECT_ROOT / "data" / "results" / "md"
|
RESULTS_MD_DIR = PROJECT_ROOT / "data" / "results" / "md"
|
||||||
RESULTS_JSON_DIR = PROJECT_ROOT / "data" / "results" / "json"
|
RESULTS_JSON_DIR = PROJECT_ROOT / "data" / "results" / "json"
|
||||||
TEMPLATE_DIR = PROJECT_ROOT / "template"
|
TEMPLATE_DIR = PROJECT_ROOT / "template"
|
||||||
|
PROMPT_DIR = PROJECT_ROOT / "prompt" / "zh"
|
||||||
EXAMPLES_DIR = PROJECT_ROOT / "examples"
|
EXAMPLES_DIR = PROJECT_ROOT / "examples"
|
||||||
CONFIG_FILE = PROJECT_ROOT / "config.json"
|
CONFIG_FILE = PROJECT_ROOT / "config.json"
|
||||||
APP_STATE_FILE = DATA_ROOT / "app_state.json"
|
APP_STATE_FILE = DATA_ROOT / "app_state.json"
|
||||||
|
|
@ -33,10 +33,7 @@ RESULTS_MD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
RESULTS_JSON_DIR.mkdir(parents=True, exist_ok=True)
|
RESULTS_JSON_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
FRONTEND_ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
FRONTEND_ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
ALL_RESULT_DIRS = (RESULTS_MD_DIR, RESULTS_JSON_DIR)
|
|
||||||
|
|
||||||
app = FastAPI(title="Meeting Summary Web")
|
app = FastAPI(title="Meeting Summary Web")
|
||||||
|
|
||||||
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets")
|
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -72,6 +69,7 @@ def _set_active_meeting(meeting_id: str | None):
|
||||||
|
|
||||||
def _get_llm_client(cfg: dict):
|
def _get_llm_client(cfg: dict):
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
|
||||||
return OpenAI(api_key=cfg["api_key"], base_url=cfg["api_base_url"])
|
return OpenAI(api_key=cfg["api_key"], base_url=cfg["api_base_url"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -107,6 +105,13 @@ def _write_meeting_meta(meeting_id: str, meta: dict):
|
||||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_child(base_dir: Path, name: str) -> Path:
|
||||||
|
target = (base_dir / name).resolve()
|
||||||
|
if target.parent != base_dir.resolve():
|
||||||
|
raise HTTPException(400, "Invalid resource path")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
def _find_transcript_file(meeting_id: str) -> Path | None:
|
def _find_transcript_file(meeting_id: str) -> Path | None:
|
||||||
mdir = DATA_DIR / meeting_id
|
mdir = DATA_DIR / meeting_id
|
||||||
for ext in (".txt", ".md"):
|
for ext in (".txt", ".md"):
|
||||||
|
|
@ -136,10 +141,7 @@ def _meeting_summary(meeting_id: str) -> dict:
|
||||||
def _list_meeting_ids() -> list[str]:
|
def _list_meeting_ids() -> list[str]:
|
||||||
if not DATA_DIR.exists():
|
if not DATA_DIR.exists():
|
||||||
return []
|
return []
|
||||||
return sorted(
|
return sorted([p.name for p in DATA_DIR.iterdir() if p.is_dir()], reverse=True)
|
||||||
[p.name for p in DATA_DIR.iterdir() if p.is_dir()],
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _list_meetings() -> list[dict]:
|
def _list_meetings() -> list[dict]:
|
||||||
|
|
@ -173,42 +175,58 @@ async def file_tree():
|
||||||
def _build_branch(label, base_dir, prefix, delete_mode):
|
def _build_branch(label, base_dir, prefix, delete_mode):
|
||||||
branch = {"name": label, "type": "folder", "children": []}
|
branch = {"name": label, "type": "folder", "children": []}
|
||||||
if base_dir.exists():
|
if base_dir.exists():
|
||||||
for md in sorted(base_dir.iterdir()):
|
for subdir in sorted(base_dir.iterdir()):
|
||||||
if not md.is_dir():
|
if not subdir.is_dir():
|
||||||
continue
|
continue
|
||||||
meta = _read_meeting_meta(md.name)
|
meta = _read_meeting_meta(subdir.name)
|
||||||
children = []
|
children = []
|
||||||
for f in sorted(md.iterdir()):
|
for f in sorted(subdir.iterdir()):
|
||||||
if f.is_file() and f.name != "meta.json":
|
if f.is_file() and f.name != "meta.json":
|
||||||
children.append({
|
children.append(
|
||||||
|
{
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": f"{prefix}/{md.name}/{f.name}",
|
"path": f"{prefix}/{subdir.name}/{f.name}",
|
||||||
})
|
}
|
||||||
branch["children"].append({
|
)
|
||||||
"name": meta.get("name", md.name),
|
branch["children"].append(
|
||||||
|
{
|
||||||
|
"name": meta.get("name", subdir.name),
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"id": md.name,
|
"id": subdir.name,
|
||||||
"active": md.name == active_meeting_id,
|
"active": subdir.name == active_meeting_id,
|
||||||
"delete_mode": delete_mode,
|
"delete_mode": delete_mode,
|
||||||
"children": children,
|
"children": children,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
tree["children"].append(branch)
|
||||||
|
|
||||||
|
def _build_flat_branch(label, base_dir, prefix, suffixes):
|
||||||
|
branch = {"name": label, "type": "folder", "children": []}
|
||||||
|
if base_dir.exists():
|
||||||
|
for f in sorted(base_dir.iterdir()):
|
||||||
|
if f.is_file() and f.suffix in suffixes:
|
||||||
|
branch["children"].append(
|
||||||
|
{
|
||||||
|
"name": f.name,
|
||||||
|
"type": "file",
|
||||||
|
"path": f"{prefix}/{f.name}",
|
||||||
|
}
|
||||||
|
)
|
||||||
tree["children"].append(branch)
|
tree["children"].append(branch)
|
||||||
|
|
||||||
_build_branch("会议原文", DATA_DIR, "meetings", "meeting")
|
_build_branch("会议原文", DATA_DIR, "meetings", "meeting")
|
||||||
_build_branch("处理结果MD", RESULTS_MD_DIR, "results_md", "results")
|
_build_branch("会议结果", RESULTS_MD_DIR, "results_md", "results")
|
||||||
_build_branch("处理结果JSON", RESULTS_JSON_DIR, "results_json", "results")
|
_build_branch("结构化主题", RESULTS_JSON_DIR, "results_json", "results")
|
||||||
|
_build_flat_branch("提示词", PROMPT_DIR, "prompts", {".yaml", ".yml"})
|
||||||
|
_build_flat_branch("模板", TEMPLATE_DIR, "templates", {".md"})
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/meetings")
|
@app.get("/api/meetings")
|
||||||
async def list_meetings():
|
async def list_meetings():
|
||||||
active_meeting_id = _load_app_state().get("active_meeting_id")
|
active_meeting_id = _load_app_state().get("active_meeting_id")
|
||||||
return {
|
return {"active_meeting_id": active_meeting_id, "meetings": _list_meetings()}
|
||||||
"active_meeting_id": active_meeting_id,
|
|
||||||
"meetings": _list_meetings(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/current-meeting")
|
@app.get("/api/current-meeting")
|
||||||
|
|
@ -246,9 +264,9 @@ async def import_meeting(name: str = Form(...), file: UploadFile = File(...)):
|
||||||
if ext not in (".txt", ".md"):
|
if ext not in (".txt", ".md"):
|
||||||
raise HTTPException(400, "Only .txt and .md files are supported")
|
raise HTTPException(400, "Only .txt and .md files are supported")
|
||||||
|
|
||||||
mid = str(int(time.time() * 1000))
|
meeting_id = str(int(time.time() * 1000))
|
||||||
mdir = DATA_DIR / mid
|
meeting_dir = DATA_DIR / meeting_id
|
||||||
mdir.mkdir(parents=True, exist_ok=True)
|
meeting_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
try:
|
try:
|
||||||
|
|
@ -257,26 +275,30 @@ async def import_meeting(name: str = Form(...), file: UploadFile = File(...)):
|
||||||
text = content.decode("gbk", errors="replace")
|
text = content.decode("gbk", errors="replace")
|
||||||
|
|
||||||
dest = "transcript" + ext
|
dest = "transcript" + ext
|
||||||
(mdir / dest).write_text(text, encoding="utf-8")
|
(meeting_dir / dest).write_text(text, encoding="utf-8")
|
||||||
|
_write_meeting_meta(
|
||||||
_write_meeting_meta(mid, {
|
meeting_id,
|
||||||
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"original_filename": file.filename,
|
"original_filename": file.filename,
|
||||||
})
|
},
|
||||||
_set_active_meeting(mid)
|
)
|
||||||
return {"id": mid, "name": name}
|
_set_active_meeting(meeting_id)
|
||||||
|
return {"id": meeting_id, "name": name}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/meetings/{meeting_id}")
|
@app.delete("/api/meetings/{meeting_id}")
|
||||||
async def delete_meeting(meeting_id: str):
|
async def delete_meeting(meeting_id: str):
|
||||||
if not (DATA_DIR / meeting_id).exists():
|
if not (DATA_DIR / meeting_id).exists():
|
||||||
raise HTTPException(404, "Meeting not found")
|
raise HTTPException(404, "Meeting not found")
|
||||||
|
|
||||||
active_meeting_id = _load_app_state().get("active_meeting_id")
|
active_meeting_id = _load_app_state().get("active_meeting_id")
|
||||||
for base in (DATA_DIR, RESULTS_MD_DIR, RESULTS_JSON_DIR):
|
for base in (DATA_DIR, RESULTS_MD_DIR, RESULTS_JSON_DIR):
|
||||||
bd = base / meeting_id
|
meeting_dir = base / meeting_id
|
||||||
if bd.exists():
|
if meeting_dir.exists():
|
||||||
shutil.rmtree(str(bd))
|
shutil.rmtree(str(meeting_dir))
|
||||||
|
|
||||||
if active_meeting_id == meeting_id:
|
if active_meeting_id == meeting_id:
|
||||||
remaining = _list_meeting_ids()
|
remaining = _list_meeting_ids()
|
||||||
_set_active_meeting(remaining[0] if remaining else None)
|
_set_active_meeting(remaining[0] if remaining else None)
|
||||||
|
|
@ -287,9 +309,9 @@ async def delete_meeting(meeting_id: str):
|
||||||
async def delete_meeting_results(meeting_id: str):
|
async def delete_meeting_results(meeting_id: str):
|
||||||
deleted = False
|
deleted = False
|
||||||
for base in (RESULTS_MD_DIR, RESULTS_JSON_DIR):
|
for base in (RESULTS_MD_DIR, RESULTS_JSON_DIR):
|
||||||
bd = base / meeting_id
|
meeting_dir = base / meeting_id
|
||||||
if bd.exists():
|
if meeting_dir.exists():
|
||||||
shutil.rmtree(str(bd))
|
shutil.rmtree(str(meeting_dir))
|
||||||
deleted = True
|
deleted = True
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(404, "Meeting results not found")
|
raise HTTPException(404, "Meeting results not found")
|
||||||
|
|
@ -325,7 +347,7 @@ async def list_templates():
|
||||||
|
|
||||||
@app.get("/api/templates/{name}")
|
@app.get("/api/templates/{name}")
|
||||||
async def get_template(name: str):
|
async def get_template(name: str):
|
||||||
fp = TEMPLATE_DIR / name
|
fp = _resolve_child(TEMPLATE_DIR, name)
|
||||||
if not fp.exists():
|
if not fp.exists():
|
||||||
raise HTTPException(404, f"Template not found: {name}")
|
raise HTTPException(404, f"Template not found: {name}")
|
||||||
return {"name": name, "content": fp.read_text(encoding="utf-8")}
|
return {"name": name, "content": fp.read_text(encoding="utf-8")}
|
||||||
|
|
@ -336,7 +358,34 @@ async def save_template(name: str, payload: dict):
|
||||||
content = payload.get("content")
|
content = payload.get("content")
|
||||||
if content is None:
|
if content is None:
|
||||||
raise HTTPException(400, "Missing content field")
|
raise HTTPException(400, "Missing content field")
|
||||||
(TEMPLATE_DIR / name).write_text(content, encoding="utf-8")
|
_resolve_child(TEMPLATE_DIR, name).write_text(content, encoding="utf-8")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/prompts")
|
||||||
|
async def list_prompts():
|
||||||
|
prompts = []
|
||||||
|
if PROMPT_DIR.exists():
|
||||||
|
for f in sorted(PROMPT_DIR.iterdir()):
|
||||||
|
if f.is_file() and f.suffix in {".yaml", ".yml"}:
|
||||||
|
prompts.append({"name": f.name})
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/prompts/{name}")
|
||||||
|
async def get_prompt(name: str):
|
||||||
|
fp = _resolve_child(PROMPT_DIR, name)
|
||||||
|
if not fp.exists():
|
||||||
|
raise HTTPException(404, f"Prompt not found: {name}")
|
||||||
|
return {"name": name, "content": fp.read_text(encoding="utf-8")}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/prompts/{name}")
|
||||||
|
async def save_prompt(name: str, payload: dict):
|
||||||
|
content = payload.get("content")
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(400, "Missing content field")
|
||||||
|
_resolve_child(PROMPT_DIR, name).write_text(content, encoding="utf-8")
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -348,10 +397,24 @@ async def get_meeting_transcript(meeting_id: str):
|
||||||
raise HTTPException(404, "No transcript found")
|
raise HTTPException(404, "No transcript found")
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/meetings/{meeting_id}/summary")
|
||||||
|
async def save_meeting_summary(meeting_id: str, payload: dict):
|
||||||
|
content = payload.get("content")
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(400, "Missing content field")
|
||||||
|
if not (DATA_DIR / meeting_id).exists():
|
||||||
|
raise HTTPException(404, "Meeting not found")
|
||||||
|
|
||||||
|
summary_dir = RESULTS_MD_DIR / meeting_id
|
||||||
|
summary_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(summary_dir / "meeting_summary.md").write_text(content, encoding="utf-8")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/meetings/{meeting_id}/process")
|
@app.get("/api/meetings/{meeting_id}/process")
|
||||||
async def process_meeting(meeting_id: str, request: Request, template_name: str = "template1.md"):
|
async def process_meeting(meeting_id: str, request: Request, template_name: str = "template1.md"):
|
||||||
mdir = DATA_DIR / meeting_id
|
meeting_dir = DATA_DIR / meeting_id
|
||||||
if not mdir.exists():
|
if not meeting_dir.exists():
|
||||||
raise HTTPException(404, "Meeting not found")
|
raise HTTPException(404, "Meeting not found")
|
||||||
_set_active_meeting(meeting_id)
|
_set_active_meeting(meeting_id)
|
||||||
|
|
||||||
|
|
@ -360,63 +423,68 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
|
||||||
raise HTTPException(400, "No transcript found")
|
raise HTTPException(400, "No transcript found")
|
||||||
transcript = transcript_file.read_text(encoding="utf-8")
|
transcript = transcript_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
tpl_path = TEMPLATE_DIR / template_name
|
template_path = _resolve_child(TEMPLATE_DIR, template_name)
|
||||||
if not tpl_path.exists():
|
if not template_path.exists():
|
||||||
raise HTTPException(404, f"Template not found: {template_name}")
|
raise HTTPException(404, f"Template not found: {template_name}")
|
||||||
template_content = tpl_path.read_text(encoding="utf-8")
|
template_content = template_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
prompt = load_prompt("meeting_summary", "zh")
|
prompt = load_prompt("meeting_summary", "zh")
|
||||||
|
|
||||||
cfg = _load_config()
|
cfg = _load_config()
|
||||||
model_name = cfg["model_name"]
|
model_name = cfg["model_name"]
|
||||||
|
events = queue.Queue()
|
||||||
eq = queue.Queue()
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
try:
|
try:
|
||||||
client = _get_llm_client(cfg)
|
client = _get_llm_client(cfg)
|
||||||
|
|
||||||
eq.put({"type": "status", "data": "preprocessing"})
|
events.put({"type": "status", "data": "preprocessing"})
|
||||||
|
system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["data_preproces"]
|
||||||
|
user_prompt = prompt["user_template"]["article_preproces"].format(article=transcript)
|
||||||
|
|
||||||
sp = prompt["system"]["role"] + prompt["mode_contracts"]["data_preproces"]
|
sub_topics = ""
|
||||||
up = prompt["user_template"]["article_preproces"].format(article=transcript)
|
for chunk_type, chunk_content in _llm_stream(client, model_name, system_prompt, user_prompt):
|
||||||
|
if chunk_content:
|
||||||
|
text = str(chunk_content)
|
||||||
|
events.put({"type": "chunk", "data": {"stage": 1, "chunk_type": chunk_type, "text": text}})
|
||||||
|
if chunk_type == "content":
|
||||||
|
sub_topics += text
|
||||||
|
|
||||||
sub = ""
|
events.put({"type": "status", "data": "preprocessing_done"})
|
||||||
for ct, cc in _llm_stream(client, model_name, sp, up):
|
|
||||||
if cc:
|
|
||||||
eq.put({"type": "chunk", "data": {"stage": 1, "chunk_type": ct, "text": str(cc)}})
|
|
||||||
if ct == "content":
|
|
||||||
sub += str(cc)
|
|
||||||
|
|
||||||
eq.put({"type": "status", "data": "preprocessing_done"})
|
results_json_dir = RESULTS_JSON_DIR / meeting_id
|
||||||
|
results_json_dir.mkdir(parents=True, exist_ok=True)
|
||||||
rjdir = RESULTS_JSON_DIR / meeting_id
|
|
||||||
rjdir.mkdir(parents=True, exist_ok=True)
|
|
||||||
try:
|
try:
|
||||||
sd = json.loads(sub)
|
data = json.loads(sub_topics)
|
||||||
(rjdir / "sub_topic.json").write_text(json.dumps(sd, ensure_ascii=False, indent=4), encoding="utf-8")
|
(results_json_dir / "sub_topic.json").write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=4),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
(rjdir / "sub_topic.json").write_text(sub, encoding="utf-8")
|
(results_json_dir / "sub_topic.json").write_text(sub_topics, encoding="utf-8")
|
||||||
|
|
||||||
eq.put({"type": "status", "data": "summarizing"})
|
events.put({"type": "status", "data": "summarizing"})
|
||||||
|
system_prompt = prompt["system"]["role"] + prompt["mode_contracts"]["data_summary"].format(
|
||||||
sp = prompt["system"]["role"] + prompt["mode_contracts"]["data_summary"].format(template=template_content)
|
template=template_content
|
||||||
up = prompt["user_template"]["article_summary"].format(article=transcript, sub_topices=sub)
|
)
|
||||||
|
user_prompt = prompt["user_template"]["article_summary"].format(
|
||||||
|
article=transcript,
|
||||||
|
sub_topices=sub_topics,
|
||||||
|
)
|
||||||
|
|
||||||
result = ""
|
result = ""
|
||||||
for ct, cc in _llm_stream(client, model_name, sp, up):
|
for chunk_type, chunk_content in _llm_stream(client, model_name, system_prompt, user_prompt):
|
||||||
if cc:
|
if chunk_content:
|
||||||
eq.put({"type": "chunk", "data": {"stage": 2, "chunk_type": ct, "text": str(cc)}})
|
text = str(chunk_content)
|
||||||
if ct == "content":
|
events.put({"type": "chunk", "data": {"stage": 2, "chunk_type": chunk_type, "text": text}})
|
||||||
result += str(cc)
|
if chunk_type == "content":
|
||||||
|
result += text
|
||||||
|
|
||||||
rmdir = RESULTS_MD_DIR / meeting_id
|
results_md_dir = RESULTS_MD_DIR / meeting_id
|
||||||
rmdir.mkdir(parents=True, exist_ok=True)
|
results_md_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(rmdir / "meeting_summary.md").write_text(result, encoding="utf-8")
|
(results_md_dir / "meeting_summary.md").write_text(result, encoding="utf-8")
|
||||||
eq.put({"type": "done", "data": {"result": result}})
|
events.put({"type": "done", "data": {"result": result}})
|
||||||
|
except Exception as exc:
|
||||||
except Exception as e:
|
events.put({"type": "error", "data": str(exc)})
|
||||||
eq.put({"type": "error", "data": str(e)})
|
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
|
|
@ -426,9 +494,9 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
|
||||||
if await request.is_disconnected():
|
if await request.is_disconnected():
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
evt = await loop.run_in_executor(None, eq.get, True, 0.5)
|
event = await loop.run_in_executor(None, events.get, True, 0.5)
|
||||||
yield f"data: {json.dumps(evt, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||||||
if evt["type"] in ("done", "error"):
|
if event["type"] in {"done", "error"}:
|
||||||
break
|
break
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
yield ": heartbeat\n\n"
|
yield ": heartbeat\n\n"
|
||||||
|
|
@ -446,4 +514,5 @@ async def process_meeting(meeting_id: str, request: Request, template_name: str
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue