968 lines
30 KiB
HTML
968 lines
30 KiB
HTML
<!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>
|