my_meeting/web/static/index.html

968 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meeting Summary</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/13.0.3/marked.min.js"></script>
<style>
:root {
--bg: #f0f2f5;
--panel-bg: #fff;
--border: #e4e7ed;
--text: #303133;
--text2: #909399;
--accent: #409eff;
--accent-light: #ecf5ff;
--hover: #f5f7fa;
--selected: #ecf5ff;
--header-h: 42px;
--panel-header-h: 36px;
--gutter-w: 4px;
--danger: #f56c6c;
--radius: 4px;
--font: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Microsoft YaHei",sans-serif;
--mono: "Cascadia Code","Fira Code","JetBrains Mono",Consolas,monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; }
.app { display: flex; flex-direction: column; height: 100vh; }
/* ─── Header ─── */
.header {
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--panel-bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
user-select: none;
}
.header .title { font-size: 14px; font-weight: 600; letter-spacing: -0.2px; }
.header .btns { display: flex; gap: 6px; }
/* ─── Main area ─── */
.main { display: flex; flex: 1; overflow: hidden; position: relative; }
.panel {
height: 100%;
background: var(--panel-bg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
height: var(--panel-header-h);
min-height: var(--panel-header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
background: #fafbfc;
border-bottom: 1px solid var(--border);
font-size: 12px;
color: var(--text2);
gap: 4px;
user-select: none;
}
.panel-body { flex: 1; overflow: auto; }
/* ─── Resize gutter ─── */
.gutter {
width: var(--gutter-w);
background: var(--border);
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s;
position: relative;
}
.gutter:hover, .gutter.dragging { background: var(--accent); }
/* ─── File Tree ─── */
#sidebar { flex-basis: 220px; flex-shrink: 0; }
#sidebar .panel-body { padding: 2px 0; overflow-y: auto; }
.tree { font-size: 13px; user-select: none; }
.tree-node { }
.tree-row {
display: flex;
align-items: center;
height: 28px;
padding: 0 4px;
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
gap: 2px;
}
.tree-row:hover { background: var(--hover); }
.tree-row.selected { background: var(--selected); color: var(--accent); }
.tree-row .arrow {
width: 16px;
height: 16px;
min-width: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: var(--text2);
flex-shrink: 0;
transition: transform 0.1s;
}
.tree-row .arrow.expanded { transform: rotate(90deg); }
.tree-row .arrow.none { visibility: hidden; }
.tree-row .icon { flex-shrink: 0; width: 18px; text-align: center; font-size: 14px; }
.tree-row .label { overflow: hidden; text-overflow: ellipsis; flex: 1; }
.tree-row .del-btn {
display: none;
margin-left: auto;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
font-size: 10px;
flex-shrink: 0;
border-radius: 2px;
color: var(--text2);
}
.tree-row:hover .del-btn { display: block; }
.tree-row .del-btn:hover { color: #fff; background: var(--danger); border-radius: 2px; }
.tree-children { display: none; }
.tree-children.open { display: block; }
/* ─── Buttons ─── */
.btn {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 5px 11px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
color: var(--text);
font-size: 12px;
cursor: pointer;
font-family: var(--font);
white-space: nowrap;
transition: all 0.12s;
height: 28px;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn.primary:hover { background: #337ecc; }
.btn.sm { padding: 2px 8px; font-size: 11px; height: 24px; }
.btn:disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
select.btn {
appearance: none;
padding-right: 22px;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23909399' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 6px center;
background-size: 10px;
}
select.btn:hover { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23409eff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); }
/* ─── Result Panel ─── */
#result-panel { flex: 1; min-width: 320px; }
#result-empty, #processing-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text2);
gap: 10px;
}
#result-md { display: none; padding: 12px 16px; line-height: 1.8; font-size: 14px; color: #1f2937; height: 100%; }
#processing-indicator .spinner {
width: 30px; height: 30px;
border: 3px solid #e4e7ed;
border-top-color: var(--accent);
border-radius: 50%;
animation: spin .65s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#stream-box {
margin-top: 12px;
background: #f9fafb;
border: 1px solid var(--border);
border-radius: var(--radius);
max-width: 85%;
width: 100%;
overflow: hidden;
font-family: var(--mono);
font-size: 12px;
display: none;
}
#stream-title {
text-align: center;
padding: 5px 10px;
background: #ebeef5;
font-weight: 600;
font-size: 12px;
color: var(--text);
border-bottom: 1px solid var(--border);
}
#stream-content {
margin: 0;
padding: 6px 10px;
background: #f9fafb;
font-family: var(--mono);
font-size: 12px;
color: #555;
white-space: pre-wrap;
word-break: break-all;
max-height: 90px;
overflow-y: auto;
min-height: 38px;
}
/* ─── Template Panel ─── */
#template-panel { flex-basis: 360px; flex-shrink: 0; min-width: 260px; }
#template-tab { height: 100%; }
#template-editor {
width: 100%; height: 100%;
border: none;
padding: 12px;
font-family: var(--mono);
font-size: 13px;
line-height: 1.65;
resize: none;
outline: none;
color: var(--text);
background: #fafbfd;
display: none;
}
#template-preview { display: block; padding: 12px 16px; height: 100%; }
#template-body { position: relative; }
.panel-tabs {
display: flex;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
height: 24px;
}
.panel-tabs .tab {
padding: 2px 10px;
font-size: 11px;
cursor: pointer;
background: #fff;
color: var(--text2);
transition: all 0.12s;
user-select: none;
}
.panel-tabs .tab:hover { color: var(--accent); }
.panel-tabs .tab.active { background: var(--accent); color: #fff; }
.panel-tabs .tab + .tab { border-left: 1px solid var(--border); }
#original-content {
height: 100%;
margin: 0;
padding: 12px 16px;
font-family: var(--mono);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
overflow-y: auto;
color: var(--text);
background: #fafbfd;
}
/* ─── Markdown content ─── */
.md-content h1 { font-size: 1.5em; margin: .6em 0 .3em; border-bottom: 1px solid var(--border); padding-bottom: .2em; }
.md-content h2 { font-size: 1.3em; margin: .6em 0 .3em; }
.md-content h3 { font-size: 1.1em; margin: .5em 0 .2em; }
.md-content p { margin: .4em 0; }
.md-content ul, .md-content ol { padding-left: 1.5em; margin: .3em 0; }
.md-content li { margin: 2px 0; }
.md-content code { background: #f0f2f5; padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: .88em; }
.md-content pre { background: #282c34; color: #abb2bf; padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: .5em 0; }
.md-content pre code { background: none; padding: 0; color: inherit; }
.md-content blockquote { border-left: 3px solid var(--accent); padding: 2px 12px; margin: .5em 0; color: var(--text2); background: var(--accent-light); }
.md-content hr { border: none; border-top: 1px solid var(--border); margin: .8em 0; }
.md-content table { border-collapse: collapse; width: 100%; margin: .5em 0; }
.md-content th, .md-content td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.md-content th { background: #f9fafb; }
.md-content a { color: var(--accent); }
.md-content img { max-width: 100%; }
/* ─── Modal ─── */
.modal-mask {
position: fixed; inset: 0;
background: rgba(0,0,0,0.35);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-mask.show { display: flex; }
.modal-box {
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
width: 460px;
max-width: 94vw;
padding: 22px 24px;
max-height: 85vh;
overflow-y: auto;
}
.modal-box h3 { font-size: 15px; margin-bottom: 18px; }
.form-field { margin-bottom: 12px; }
.form-field label { display: block; font-size: 12px; color: var(--text2); margin-bottom: 4px; font-weight: 500; }
.form-field input {
width: 100%;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 13px;
font-family: var(--font);
outline: none;
transition: border-color 0.12s;
}
.form-field input:focus { border-color: var(--accent); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
/* ─── Toast ─── */
.toast {
position: fixed; top: 16px; right: 16px;
padding: 10px 16px;
border-radius: 4px;
font-size: 13px;
z-index: 2000;
opacity: 0;
transform: translateY(-8px);
transition: all .2s;
pointer-events: none;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.ok { background: #f0f9eb; color: #67c23a; border: 1px solid #e1f3d8; }
.toast.err { background: #fef0f0; color: #f56c6c; border: 1px solid #fde2e2; }
</style>
</head>
<body>
<div class="app">
<div class="header">
<span class="title">Meeting Summary</span>
<div class="btns">
<button class="btn primary" id="btn-import">+ 导入会议</button>
<button class="btn" id="btn-settings">⚙ 设置</button>
</div>
</div>
<div class="main" id="main-container">
<!-- Left: File Tree -->
<div class="panel" id="sidebar">
<div class="panel-header">文件资源管理器</div>
<div class="panel-body"><div class="tree" id="file-tree"></div></div>
</div>
<div class="gutter" id="gutter-1"></div>
<!-- Center: Result -->
<div class="panel" id="result-panel">
<div class="panel-header">
<span>处理结果</span>
<button class="btn sm primary" id="btn-process" disabled>▶ 处理</button>
</div>
<div class="panel-body" id="result-body">
<div id="result-empty"><span style="font-size:36px;opacity:.25">📄</span><span>选择左侧会议后点击「处理」</span></div>
<div id="processing-indicator" style="display:none">
<div class="spinner"></div>
<div id="stream-box">
<div id="stream-title"></div>
<pre id="stream-content"></pre>
</div>
</div>
<div id="result-md" class="md-content"></div>
</div>
</div>
<div class="gutter" id="gutter-2"></div>
<!-- Right: Template Editor -->
<div class="panel" id="template-panel">
<div class="panel-header">
<span>模板编辑器</span>
<div style="display:flex;gap:4px;align-items:center;">
<div class="panel-tabs" id="panel-tabs">
<span class="tab active" data-tab="template">模板</span>
<span class="tab" data-tab="original">原文</span>
</div>
<span style="font-size:12px;color:var(--text2);">选择模板:</span>
<select class="btn sm" id="tpl-select"></select>
<button class="btn sm" id="btn-toggle-edit">✏ 编辑</button>
</div>
</div>
<div class="panel-body" id="template-body">
<div id="template-tab">
<textarea id="template-editor" spellcheck="false"></textarea>
<div id="template-preview" class="md-content"></div>
</div>
<div id="original-tab" style="display:none;height:100%;">
<pre id="original-content"></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Import Modal -->
<div class="modal-mask" id="modal-import">
<div class="modal-box">
<h3>导入会议转录</h3>
<div class="form-field">
<label>会议名称</label>
<input type="text" id="import-name" placeholder="例: 2026-05-08 运维周会">
</div>
<div class="form-field">
<label>转录文件 (.txt / .md)</label>
<input type="file" id="import-file" accept=".txt,.md">
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('modal-import')">取消</button>
<button class="btn primary" id="btn-confirm-import">导入</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal-mask" id="modal-settings">
<div class="modal-box">
<h3>API 配置</h3>
<div class="form-field">
<label>Base URL</label>
<input type="text" id="cfg-url" placeholder="http://host:port/v1">
</div>
<div class="form-field">
<label>API Key</label>
<input type="text" id="cfg-key" placeholder="your-api-key">
</div>
<div class="form-field">
<label>Model Name</label>
<input type="text" id="cfg-model" placeholder="Qwen3.6-35B">
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('modal-settings')">取消</button>
<button class="btn primary" id="btn-save-settings">保存</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
var $ = function(s) { return document.querySelector(s); };
var $$ = function(s) { return document.querySelectorAll(s); };
var state = {
meetingId: null,
templateName: 'template1.md',
editMode: false,
processing: false,
templates: [],
};
function toast(msg, type) {
var el = $('#toast');
el.textContent = msg;
el.className = 'toast show ' + (type || 'ok');
clearTimeout(el._timer);
el._timer = setTimeout(function() { el.classList.remove('show'); }, 2500);
}
function closeModal(id) {
$('#' + id).classList.remove('show');
}
function openModal(id) {
$('#' + id).classList.add('show');
}
async function api(url, opts) {
var res = await fetch(url, opts || {});
if (!res.ok) {
var e = await res.json().catch(function() { return { detail: res.statusText }; });
throw new Error(e.detail || 'Request failed');
}
return res.json();
}
// ─── Resizable Panels ───
function initResize(gutterId, leftId, rightId) {
var gutter = document.getElementById(gutterId);
var left = document.getElementById(leftId);
var right = document.getElementById(rightId);
var container = document.getElementById('main-container');
var dragging = false;
var startX = 0;
var startLeftW = 0;
var startRightW = 0;
gutter.addEventListener('mousedown', function(e) {
e.preventDefault();
dragging = true;
gutter.classList.add('dragging');
startX = e.clientX;
startLeftW = left.getBoundingClientRect().width;
startRightW = right.getBoundingClientRect().width;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function(e) {
if (!dragging) return;
var dx = e.clientX - startX;
var newLeftW = Math.max(left.dataset.minWidth || 150, startLeftW + dx);
var newRightW = startRightW - dx;
var minRight = right.dataset.minWidth || 200;
if (newRightW < minRight) {
newRightW = minRight;
newLeftW = startLeftW + startRightW - minRight;
}
if (leftId === 'sidebar') {
left.style.flexBasis = newLeftW + 'px';
left.style.flexGrow = '0';
} else {
left.style.flexGrow = '1';
left.style.flexBasis = '0%';
}
right.style.flexBasis = newRightW + 'px';
right.style.flexGrow = '0';
});
document.addEventListener('mouseup', function() {
if (!dragging) return;
dragging = false;
gutter.classList.remove('dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
document.addEventListener('DOMContentLoaded', function() {
initResize('gutter-1', 'sidebar', 'result-panel');
initResize('gutter-2', 'result-panel', 'template-panel');
document.getElementById('sidebar').dataset.minWidth = 180;
document.getElementById('template-panel').dataset.minWidth = 260;
document.getElementById('result-panel').dataset.minWidth = 280;
});
// ─── File Tree ───
function buildTree(data) {
var el = $('#file-tree');
el.innerHTML = '';
(data.children || []).forEach(function(n) { renderNode(n, el, 0); });
}
function renderNode(node, parent, depth) {
var div = document.createElement('div');
div.className = 'tree-node';
var row = document.createElement('div');
row.className = 'tree-row';
row.style.paddingLeft = (8 + depth * 18) + 'px';
if (node.type === 'folder') {
var arrow = document.createElement('span');
arrow.className = 'arrow';
arrow.textContent = '▸';
row.appendChild(arrow);
var icon = document.createElement('span');
icon.className = 'icon';
icon.textContent = '📁';
row.appendChild(icon);
var label = document.createElement('span');
label.className = 'label';
label.textContent = node.name;
row.appendChild(label);
if (node.id) {
var del = document.createElement('span');
del.className = 'del-btn';
del.textContent = '×';
del.addEventListener('click', function(e) { e.stopPropagation(); deleteMeeting(node.id); });
row.appendChild(del);
}
row.addEventListener('click', function() {
$$('.tree-row').forEach(function(r) { r.classList.remove('selected'); });
row.classList.add('selected');
var kids = div.querySelector(':scope > .tree-children');
if (kids) {
var open = !kids.classList.contains('open');
kids.classList.toggle('open', open);
arrow.classList.toggle('expanded', open);
}
if (node.id) { selectMeeting(node.id); }
});
} else {
var arrNone = document.createElement('span');
arrNone.className = 'arrow none';
row.appendChild(arrNone);
var ficon = document.createElement('span');
ficon.className = 'icon';
var n = node.name;
ficon.textContent = n.endsWith('.md') ? '📝' : n.endsWith('.json') ? '📊' : n.endsWith('.txt') ? '📃' : '📄';
row.appendChild(ficon);
var flabel = document.createElement('span');
flabel.className = 'label';
flabel.textContent = n;
row.appendChild(flabel);
row.addEventListener('click', function() {
$$('.tree-row').forEach(function(r) { r.classList.remove('selected'); });
row.classList.add('selected');
if (node.path) {
var p = node.path;
if (p.startsWith('meetings/') || p.startsWith('results_md/') || p.startsWith('results_json/')) {
var parts = p.replace(/^(meetings|results_md|results_json)\//, '').split('/');
var mid = parts[0];
var fname = parts.slice(1).join('/');
viewMeetingFile(mid, fname);
}
}
});
}
div.appendChild(row);
if (node.children && node.children.length) {
var kids = document.createElement('div');
kids.className = 'tree-children';
node.children.forEach(function(c) { renderNode(c, kids, depth + 1); });
div.appendChild(kids);
}
parent.appendChild(div);
}
// ─── Meetings ───
function selectMeeting(mid) {
if (state.processing) return;
state.meetingId = mid;
$('#btn-process').disabled = false;
showResultPanel();
api('/api/meetings/' + mid + '/file/meeting_summary.md').then(function(r) {
showResult(r.content);
}).catch(function() {
showEmpty();
});
}
function viewMeetingFile(mid, fname) {
if (state.processing) return;
state.meetingId = mid;
$('#btn-process').disabled = false;
if (fname === 'meeting_summary.md') {
switchToOriginalTab(mid);
}
api('/api/meetings/' + mid + '/file/' + encodeURIComponent(fname)).then(function(r) {
var c = r.content;
if (fname.endsWith('.json')) {
try { c = '```json\n' + JSON.stringify(JSON.parse(c), null, 2) + '\n```'; } catch(e) {}
} else if (fname.endsWith('.txt')) {
c = '```\n' + c + '\n```';
}
showResult(c);
}).catch(function() { showEmpty(); });
}
function viewExampleFile(fname) {
state.meetingId = null;
$('#btn-process').disabled = true;
api('/api/examples/' + encodeURIComponent(fname)).then(function(r) {
var c = r.content;
if (fname.endsWith('.json')) {
try { c = '```json\n' + JSON.stringify(JSON.parse(c), null, 2) + '\n```'; } catch(e) {}
} else if (fname.endsWith('.txt')) {
c = '```\n' + c + '\n```';
}
showResult(c);
}).catch(function() { showEmpty(); });
}
function deleteMeeting(mid) {
if (!confirm('确定删除此会议及所有数据?')) return;
api('/api/meetings/' + mid, { method: 'DELETE' }).then(function() {
toast('已删除');
if (state.meetingId === mid) { state.meetingId = null; showEmpty(); $('#btn-process').disabled = true; }
refresh();
}).catch(function(e) { toast(e.message, 'err'); });
}
// ─── Result ───
function showResult(md) {
$('#processing-indicator').style.display = 'none';
$('#result-empty').style.display = 'none';
var el = $('#result-md');
el.style.display = 'block';
el.innerHTML = marked.parse(md);
el.scrollTop = 0;
}
function showResultPanel() {
$('#result-empty').style.display = 'none';
$('#processing-indicator').style.display = 'none';
$('#result-md').style.display = 'none';
}
function showEmpty() {
$('#processing-indicator').style.display = 'none';
$('#result-md').style.display = 'none';
$('#result-empty').style.display = 'flex';
}
// ─── Processing (SSE) ───
$('#btn-process').addEventListener('click', function() {
if (!state.meetingId || state.processing) return;
state.processing = true;
$('#btn-process').disabled = true;
$('#result-empty').style.display = 'none';
$('#result-md').style.display = 'none';
$('#processing-indicator').style.display = 'flex';
$('#stream-title').textContent = '正在预处理...';
$('#stream-content').textContent = '';
$('#stream-box').style.display = 'block';
var url = '/api/meetings/' + state.meetingId + '/process?template_name=' + encodeURIComponent(state.templateName);
var es = new EventSource(url);
var resultAcc = '';
var streamAcc = '';
es.onmessage = function(e) {
if (!e.data || e.data.indexOf(': heartbeat') === 0) return;
try {
var evt = JSON.parse(e.data);
if (evt.type === 'status') {
if (evt.data === 'preprocessing') { $('#stream-title').textContent = '第一阶段:数据预处理...'; streamAcc = ''; $('#stream-content').textContent = ''; }
else if (evt.data === 'preprocessing_done') $('#stream-title').textContent = '第一阶段完成,开始生成总结...';
else if (evt.data === 'summarizing') { $('#stream-title').textContent = '第二阶段:生成会议总结...'; streamAcc = ''; $('#stream-content').textContent = ''; }
} else if (evt.type === 'chunk') {
var d = evt.data;
if (d.text) {
streamAcc += d.text;
var lines = streamAcc.replace(/\r\n/g, '\n').split('\n');
var last3 = lines.slice(-3).join('\n');
if (last3.trim()) {
$('#stream-content').textContent = last3;
}
}
if (d.stage === 2 && d.chunk_type === 'content') {
resultAcc += d.text;
$('#result-md').style.display = 'block';
$('#result-md').innerHTML = marked.parse(resultAcc);
$('#result-md').scrollTop = $('#result-md').scrollHeight;
}
} else if (evt.type === 'done') {
es.close();
$('#stream-title').textContent = '完成';
switchToOriginalTab(state.meetingId);
setTimeout(function() {
$('#stream-box').style.display = 'none';
$('#processing-indicator').style.display = 'none';
state.processing = false;
$('#btn-process').disabled = false;
refresh();
}, 600);
if (evt.data && evt.data.result) {
$('#result-md').innerHTML = marked.parse(evt.data.result);
}
} else if (evt.type === 'error') {
es.close();
toast('处理失败: ' + evt.data, 'err');
$('#stream-box').style.display = 'none';
$('#stream-content').textContent = '';
setTimeout(function() {
$('#processing-indicator').style.display = 'none';
state.processing = false;
$('#btn-process').disabled = false;
}, 1500);
}
} catch(ex) {}
};
es.onerror = function() {
es.close();
state.processing = false;
$('#processing-indicator').style.display = 'none';
$('#stream-box').style.display = 'none';
$('#stream-content').textContent = '';
$('#btn-process').disabled = false;
if (!resultAcc) toast('连接中断', 'err');
refresh();
};
});
// ─── Template ───
function loadTemplate(name) {
state.templateName = name;
api('/api/templates/' + name).then(function(r) {
state.editMode = false;
$('#template-editor').value = r.content;
$('#template-editor').style.display = 'none';
$('#template-preview').style.display = 'block';
$('#template-preview').innerHTML = marked.parse(r.content);
$('#btn-toggle-edit').innerHTML = '✏ 编辑';
var sel = $('#tpl-select');
if (sel) sel.value = name;
}).catch(function(e) { toast(e.message, 'err'); });
}
$('#btn-toggle-edit').addEventListener('click', function() {
state.editMode = !state.editMode;
if (state.editMode) {
$('#template-editor').style.display = 'block';
$('#template-preview').style.display = 'none';
$('#btn-toggle-edit').innerHTML = '👁 预览';
} else {
var content = $('#template-editor').value;
$('#template-editor').style.display = 'none';
$('#template-preview').style.display = 'block';
$('#template-preview').innerHTML = marked.parse(content);
$('#btn-toggle-edit').innerHTML = '✏ 编辑';
api('/api/templates/' + state.templateName, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content }),
}).catch(function(e) { toast('保存失败: ' + e.message, 'err'); });
}
});
$('#tpl-select').addEventListener('change', function() {
state.templateName = this.value;
loadTemplate(this.value);
});
$('#panel-tabs').addEventListener('click', function(e) {
var tab = e.target.closest('.tab');
if (!tab) return;
var tabName = tab.dataset.tab;
$$('#panel-tabs .tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
if (tabName === 'template') {
$('#template-tab').style.display = 'block';
$('#original-tab').style.display = 'none';
} else {
$('#template-tab').style.display = 'none';
$('#original-tab').style.display = 'block';
if (state.meetingId) {
loadOriginalContent(state.meetingId);
}
}
});
function switchToOriginalTab(mid) {
var ot = $$('#panel-tabs .tab[data-tab="original"]')[0];
if (ot && !ot.classList.contains('active')) {
ot.click();
} else if (ot) {
loadOriginalContent(mid);
}
}
function loadOriginalContent(mid) {
api('/api/meetings/' + mid + '/transcript').then(function(r) {
$('#original-content').textContent = r.content;
}).catch(function() {
$('#original-content').textContent = '(无法加载原文)';
});
}
// ─── Import ───
$('#btn-import').addEventListener('click', function() {
openModal('modal-import');
$('#import-name').value = '';
$('#import-file').value = '';
});
$('#btn-confirm-import').addEventListener('click', function() {
var name = $('#import-name').value.trim();
var file = $('#import-file').files[0];
if (!name) { toast('请输入会议名称', 'err'); return; }
if (!file) { toast('请选择文件', 'err'); return; }
var fd = new FormData();
fd.append('name', name);
fd.append('file', file);
fetch('/api/meetings/import', { method: 'POST', body: fd }).then(function(r) {
if (!r.ok) return r.json().then(function(e) { throw new Error(e.detail); });
return r.json();
}).then(function(d) {
closeModal('modal-import');
toast('导入成功: ' + d.name);
refresh().then(function() { selectMeeting(d.id); });
}).catch(function(e) { toast('导入失败: ' + e.message, 'err'); });
});
document.getElementById('modal-import').addEventListener('click', function(e) {
if (e.target === this) closeModal('modal-import');
});
document.getElementById('modal-settings').addEventListener('click', function(e) {
if (e.target === this) closeModal('modal-settings');
});
// ─── Settings ───
$('#btn-settings').addEventListener('click', function() {
api('/api/settings').then(function(cfg) {
$('#cfg-url').value = cfg.api_base_url || '';
$('#cfg-key').value = cfg.api_key || '';
$('#cfg-model').value = cfg.model_name || '';
openModal('modal-settings');
}).catch(function() { openModal('modal-settings'); });
});
$('#btn-save-settings').addEventListener('click', function() {
var cfg = {
api_base_url: $('#cfg-url').value.trim(),
api_key: $('#cfg-key').value.trim(),
model_name: $('#cfg-model').value.trim(),
};
api('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg),
}).then(function() {
closeModal('modal-settings');
toast('配置已保存');
}).catch(function(e) { toast(e.message, 'err'); });
});
// ─── Init ───
function refresh() {
return api('/api/templates').then(function(ts) {
state.templates = ts;
var sel = $('#tpl-select');
sel.innerHTML = '';
ts.forEach(function(t) {
var o = document.createElement('option');
o.value = t.name;
o.textContent = t.name;
sel.appendChild(o);
});
sel.value = state.templateName;
}).then(function() {
return api('/api/tree');
}).then(function(tree) {
buildTree(tree);
});
}
refresh().then(function() {
loadTemplate(state.templateName);
});
</script>
</body>
</html>