my_meeting/web/static/index.html

968 lines
30 KiB
HTML
Raw Permalink Normal View History

2026-05-09 03:23:57 +00:00
<!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>