492 lines
21 KiB
HTML
492 lines
21 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|||
|
|
<title>语料采集 · Corpus Agent</title>
|
|||
|
|
<style>
|
|||
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|||
|
|
:root{
|
|||
|
|
--deep:#0b1424;--mid:#141e30;--shallow:#1a2a44;
|
|||
|
|
--cyan:#2a9dbf;--glow:#5ad0e0;--light:#d4eaf0;
|
|||
|
|
--violet:#7b5dbf;--heart:#e04a74;--green:#40b87a;
|
|||
|
|
--text:#e4ecf5;--text-soft:#8aa0b8;--text-faint:#5a7088;
|
|||
|
|
--glass:rgba(11,20,36,.7);--glass-edge:rgba(90,208,224,.08);
|
|||
|
|
--card-bg:rgba(11,20,36,.4);--border:rgba(90,208,224,.10);
|
|||
|
|
--shadow:0 8px 32px rgba(0,0,0,.3);
|
|||
|
|
}
|
|||
|
|
@keyframes breathe{0%,100%{box-shadow:0 0 4px rgba(42,157,191,.08),0 0 12px rgba(42,157,191,.02)}50%{box-shadow:0 0 10px rgba(42,157,191,.28),0 0 28px rgba(42,157,191,.1)}}
|
|||
|
|
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|||
|
|
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|||
|
|
@keyframes typing{0%,80%,100%{transform:scale(.6)}40%{transform:scale(1)}}
|
|||
|
|
|
|||
|
|
html,body{height:100%;background:var(--deep);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
|
|||
|
|
|
|||
|
|
/* Login */
|
|||
|
|
.login-copyright{color:var(--text-faint);font-size:11px;margin-top:24px}
|
|||
|
|
|
|||
|
|
/* Main layout */
|
|||
|
|
.app{display:none;height:100vh;flex-direction:column}
|
|||
|
|
|
|||
|
|
/* Header */
|
|||
|
|
.header{background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
|||
|
|
.header-left{display:flex;align-items:center;gap:12px}
|
|||
|
|
.header-left .logo{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--cyan),var(--violet));opacity:.8}
|
|||
|
|
.header-left h2{font-size:16px;font-weight:500}
|
|||
|
|
.header-left .user-badge{background:rgba(42,157,191,.12);color:var(--cyan);padding:2px 10px;border-radius:12px;font-size:12px}
|
|||
|
|
.header-right{display:flex;align-items:center;gap:8px}
|
|||
|
|
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:6px;font-size:12px;cursor:pointer;transition:.2s;border:none}
|
|||
|
|
.btn-ghost{background:transparent;color:var(--text-soft);border:1px solid var(--border)}
|
|||
|
|
.btn-ghost:hover{background:rgba(255,255,255,.04);color:var(--text)}
|
|||
|
|
.btn-primary{background:linear-gradient(135deg,var(--cyan),#3ab0c8);color:var(--deep);font-weight:500}
|
|||
|
|
.btn-primary:hover{box-shadow:0 2px 12px rgba(42,157,191,.25)}
|
|||
|
|
.btn-sm{padding:4px 10px;font-size:11px}
|
|||
|
|
|
|||
|
|
/* Content */
|
|||
|
|
.content{flex:1;display:flex;overflow:hidden}
|
|||
|
|
|
|||
|
|
/* Chat area */
|
|||
|
|
.chat-area{flex:1;display:flex;flex-direction:column;min-width:0}
|
|||
|
|
.chat-messages{flex:1;overflow-y:auto;padding:20px 24px}
|
|||
|
|
.chat-messages::-webkit-scrollbar{width:4px}
|
|||
|
|
.chat-messages::-webkit-scrollbar-track{background:transparent}
|
|||
|
|
.chat-messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
|||
|
|
|
|||
|
|
.msg{max-width:75%;margin-bottom:16px;animation:fadeIn .3s ease;line-height:1.7}
|
|||
|
|
.msg.user{margin-left:auto}
|
|||
|
|
.msg.assistant{margin-right:auto}
|
|||
|
|
.msg.system{max-width:90%;margin:12px auto;text-align:center}
|
|||
|
|
.msg .label{font-size:11px;color:var(--text-soft);margin-bottom:4px;font-weight:500}
|
|||
|
|
.msg.user .label{text-align:right}
|
|||
|
|
.msg .bubble{padding:10px 16px;border-radius:10px;font-size:14px}
|
|||
|
|
.msg.user .bubble{background:linear-gradient(135deg,var(--cyan),#3ab0c8);color:var(--deep);border-bottom-right-radius:4px}
|
|||
|
|
.msg.assistant .bubble{background:var(--card-bg);border:1px solid var(--border);border-bottom-left-radius:4px;color:var(--text)}
|
|||
|
|
.msg.system .bubble{background:rgba(224,74,116,.08);border:1px solid rgba(224,74,116,.15);color:var(--heart);font-size:13px}
|
|||
|
|
.msg .bubble code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px;font-size:13px}
|
|||
|
|
.msg .bubble pre{background:rgba(0,0,0,.25);padding:10px;border-radius:6px;margin:8px 0;overflow-x:auto;font-size:13px}
|
|||
|
|
.msg .bubble strong{color:var(--glow)}
|
|||
|
|
|
|||
|
|
/* Typing indicator */
|
|||
|
|
.typing{display:flex;gap:5px;padding:4px 0}
|
|||
|
|
.typing span{width:7px;height:7px;background:var(--text-soft);border-radius:50%;animation:typing 1.2s infinite ease-in-out}
|
|||
|
|
.typing span:nth-child(2){animation-delay:.2s}
|
|||
|
|
.typing span:nth-child(3){animation-delay:.4s}
|
|||
|
|
|
|||
|
|
/* Chat input */
|
|||
|
|
.chat-input{background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-top:1px solid var(--border);padding:14px 24px;display:flex;gap:10px;align-items:center;flex-shrink:0}
|
|||
|
|
.chat-input input{flex:1;padding:10px 16px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none;transition:.2s}
|
|||
|
|
.chat-input input:focus{border-color:var(--cyan)}
|
|||
|
|
.chat-input input::placeholder{color:var(--text-faint)}
|
|||
|
|
|
|||
|
|
/* Sidebar */
|
|||
|
|
.sidebar{width:320px;background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
|||
|
|
.sidebar-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0}
|
|||
|
|
.sidebar-tab{padding:10px 14px;font-size:12px;color:var(--text-soft);cursor:pointer;border-bottom:2px solid transparent;transition:.2s;background:none;border-top:none;border-left:none;border-right:none}
|
|||
|
|
.sidebar-tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
|
|||
|
|
.sidebar-tab:hover{color:var(--text)}
|
|||
|
|
.sidebar-content{flex:1;overflow-y:auto;padding:16px}
|
|||
|
|
.sidebar-content::-webkit-scrollbar{width:4px}
|
|||
|
|
.sidebar-content::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
|||
|
|
|
|||
|
|
.sidebar-section{margin-bottom:20px}
|
|||
|
|
.sidebar-section h3{font-size:12px;color:var(--text-soft);font-weight:500;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|||
|
|
.sidebar-section .desc{font-size:12px;color:var(--text-faint);margin-bottom:8px;line-height:1.6}
|
|||
|
|
|
|||
|
|
.code-block{background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:6px;padding:10px;font-family:'SFMono-Regular',Consolas,Monaco,monospace;font-size:11px;color:var(--glow);overflow-x:auto;white-space:pre-wrap;word-break:break-all}
|
|||
|
|
.copy-btn{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--cyan);cursor:pointer;margin-top:6px;background:none;border:none;padding:4px 8px;border-radius:4px}
|
|||
|
|
.copy-btn:hover{background:rgba(42,157,191,.1)}
|
|||
|
|
|
|||
|
|
/* Stats */
|
|||
|
|
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|||
|
|
.stat-card{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center}
|
|||
|
|
.stat-card .num{font-size:18px;font-weight:600;color:var(--glow)}
|
|||
|
|
.stat-card .label{font-size:11px;color:var(--text-soft);margin-top:2px}
|
|||
|
|
|
|||
|
|
/* Samples */
|
|||
|
|
.sample-item{background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;cursor:pointer;transition:.2s}
|
|||
|
|
.sample-item:hover{border-color:rgba(42,157,191,.2)}
|
|||
|
|
.sample-item .preview{font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:4px}
|
|||
|
|
.sample-item .meta{font-size:11px;color:var(--text-faint)}
|
|||
|
|
.sample-item .tag{display:inline-block;background:rgba(42,157,191,.1);color:var(--cyan);padding:1px 6px;border-radius:4px;font-size:10px;margin-right:4px}
|
|||
|
|
|
|||
|
|
.empty-state{padding:40px 20px;text-align:center;color:var(--text-faint);font-size:13px;line-height:1.8}
|
|||
|
|
|
|||
|
|
/* Progress bar */
|
|||
|
|
.progress-bar{height:3px;background:var(--border);border-radius:2px;overflow:hidden;margin:8px 0}
|
|||
|
|
.progress-bar .fill{height:100%;background:linear-gradient(90deg,var(--cyan),var(--violet));border-radius:2px;transition:width .3s ease;width:0%}
|
|||
|
|
|
|||
|
|
/* Responsive */
|
|||
|
|
@media(max-width:768px){
|
|||
|
|
.sidebar{display:none}
|
|||
|
|
.msg{max-width:90%}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
|
|||
|
|
<!-- Login -->
|
|||
|
|
|
|||
|
|
|
|||
|
|
<!-- App -->
|
|||
|
|
<div class="app" id="app">
|
|||
|
|
<div class="header">
|
|||
|
|
<div class="header-left">
|
|||
|
|
<div class="logo"></div>
|
|||
|
|
<h2>Corpus Agent</h2>
|
|||
|
|
<span class="user-badge" id="user-badge">—</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="header-right">
|
|||
|
|
<span style="font-size:11px;color:var(--text-faint)" id="info-version">v1.2.0</span>
|
|||
|
|
<button class="btn btn-ghost btn-sm" onclick="exportAll()">📥 导出</button>
|
|||
|
|
<button class="btn btn-ghost btn-sm" onclick="doLogout()">退出</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="content">
|
|||
|
|
<!-- Chat -->
|
|||
|
|
<div class="chat-area">
|
|||
|
|
<div class="chat-messages" id="chat-messages">
|
|||
|
|
<div class="msg system">
|
|||
|
|
<div class="bubble">🧠 语料助手已就绪。告诉我你要整理哪里的对话,我来帮你采集、判断、脱敏、格式化。</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="chat-input">
|
|||
|
|
<input type="text" id="chat-input" placeholder="告诉我要整理什么对话..." autocomplete="off">
|
|||
|
|
<button class="btn btn-primary" id="send-btn" onclick="sendMessage()">发送</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Sidebar -->
|
|||
|
|
<div class="sidebar">
|
|||
|
|
<div class="sidebar-tabs">
|
|||
|
|
<button class="sidebar-tab active" onclick="switchSidebar('api',this)">API</button>
|
|||
|
|
<button class="sidebar-tab" onclick="switchSidebar('corpus',this)">语料库</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-content" id="sidebar-api">
|
|||
|
|
<div class="sidebar-section">
|
|||
|
|
<h3>WorkBuddy MCP</h3>
|
|||
|
|
<div class="desc">在 WorkBuddy 中连接此 MCP Server 即可远程调用语料采集能力</div>
|
|||
|
|
<div class="code-block" id="mcp-config">正在加载...</div>
|
|||
|
|
<button class="copy-btn" onclick="copyText('mcp-config')">📋 复制配置</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-section">
|
|||
|
|
<h3>REST API</h3>
|
|||
|
|
<div class="desc">直接调用 API 采集语料</div>
|
|||
|
|
<div class="code-block" id="api-curl">正在加载...</div>
|
|||
|
|
<button class="copy-btn" onclick="copyText('api-curl')">📋 复制命令</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-section">
|
|||
|
|
<h3>统计数据</h3>
|
|||
|
|
<div class="stats-grid" id="stats-grid">
|
|||
|
|
<div class="stat-card"><div class="num" id="stat-total">0</div><div class="label">语料总数</div></div>
|
|||
|
|
<div class="stat-card"><div class="num" id="stat-chars">0</div><div class="label">总字符数</div></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-section">
|
|||
|
|
<h3>关于</h3>
|
|||
|
|
<div class="desc">
|
|||
|
|
Corpus Agent 是铸渊的语料采集分身。<br>
|
|||
|
|
采集的对话会经过 AI 判断 → 自动脱敏 → 格式化为 SFT 训练语料。<br><br>
|
|||
|
|
所有采集的语料按代码仓库账号分开存储。<br>
|
|||
|
|
导出格式:JSONL (ChatML)<br><br>
|
|||
|
|
需要帮助?在对话区输入问题,Agent 会引导你完成采集。
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-content" id="sidebar-corpus" style="display:none">
|
|||
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|||
|
|
<h3 style="font-size:12px;color:var(--text-soft);font-weight:500">已采集的语料</h3>
|
|||
|
|
<button class="btn btn-ghost btn-sm" onclick="loadSamples()">🔄 刷新</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="sample-list">
|
|||
|
|
<div class="empty-state">还没有语料<br>在对话区告诉 Agent 你想整理什么</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
let token = localStorage.getItem('corpus_token') || '';
|
|||
|
|
let username = '';
|
|||
|
|
let chatHistory = [];
|
|||
|
|
|
|||
|
|
// ─── 检测首页登录状态 ────────────────────────────────────────────
|
|||
|
|
(function() {
|
|||
|
|
var u = localStorage.getItem('current_user');
|
|||
|
|
if (!u || localStorage.getItem('logged_in') !== 'true') {
|
|||
|
|
window.location.href = '/';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
username = u;
|
|||
|
|
document.getElementById('user-badge').textContent = username;
|
|||
|
|
fetch('/api/auth/quick-login?username=' + encodeURIComponent(u))
|
|||
|
|
.then(function(r) { return r.json(); })
|
|||
|
|
.then(function(d) { if (d.ok) { token = d.token; updateApiPanel(); loadStats(); loadSamples(); addSystemMsg('✅ 欢迎回来,有什么语料需要整理的?'); } })
|
|||
|
|
.catch(function() {});
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
function doLogout() {
|
|||
|
|
localStorage.removeItem('corpus_token');
|
|||
|
|
token = ''; username = '';
|
|||
|
|
document.getElementById('app').style.display = 'none';
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Auto-login
|
|||
|
|
if (token) {
|
|||
|
|
(async () => {
|
|||
|
|
try {
|
|||
|
|
const r = await fetch('/api/auth/check?token=' + token);
|
|||
|
|
const d = await r.json();
|
|||
|
|
if (d.ok) {
|
|||
|
|
username = d.username;
|
|||
|
|
|
|||
|
|
document.getElementById('app').style.display = 'flex';
|
|||
|
|
document.getElementById('user-badge').textContent = username;
|
|||
|
|
updateApiPanel();
|
|||
|
|
loadStats();
|
|||
|
|
loadSamples();
|
|||
|
|
} else {
|
|||
|
|
localStorage.removeItem('corpus_token');
|
|||
|
|
}
|
|||
|
|
} catch(e) { localStorage.removeItem('corpus_token'); }
|
|||
|
|
})();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Enter key ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
document.getElementById('chat-input').addEventListener('keydown', e => { if(e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
|
|||
|
|
|
|||
|
|
// ─── Chat ───────────────────────────────────────────────────────────
|
|||
|
|
function addSystemMsg(text) {
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
const d = document.createElement('div'); d.className = 'msg system';
|
|||
|
|
d.innerHTML = '<div class="bubble">' + text + '</div>';
|
|||
|
|
c.appendChild(d); c.scrollTop = c.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addMsg(role, text, keepHistory) {
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
const d = document.createElement('div'); d.className = 'msg ' + role;
|
|||
|
|
const label = role === 'user' ? '你' : role === 'assistant' ? '助手' : '系统';
|
|||
|
|
|
|||
|
|
let content = escHtml(text).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`([^`]+)`/g,'<code>$1</code>');
|
|||
|
|
|
|||
|
|
d.innerHTML = '<div class="label">' + label + '</div><div class="bubble">' + content + '</div>';
|
|||
|
|
c.appendChild(d); c.scrollTop = c.scrollHeight;
|
|||
|
|
|
|||
|
|
if (keepHistory !== false) {
|
|||
|
|
chatHistory.push({role, content: text});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showTyping() {
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
const d = document.createElement('div'); d.className = 'msg assistant'; d.id = 'typing-msg';
|
|||
|
|
d.innerHTML = '<div class="label">助手</div><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div>';
|
|||
|
|
c.appendChild(d); c.scrollTop = c.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hideTyping() {
|
|||
|
|
const el = document.getElementById('typing-msg');
|
|||
|
|
if (el) el.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function sendMessage() {
|
|||
|
|
const input = document.getElementById('chat-input');
|
|||
|
|
const msg = input.value.trim();
|
|||
|
|
if (!msg || !token) return;
|
|||
|
|
input.value = '';
|
|||
|
|
|
|||
|
|
addMsg('user', msg);
|
|||
|
|
showTyping();
|
|||
|
|
|
|||
|
|
// Strip typing indicator from history for chat-v2
|
|||
|
|
try {
|
|||
|
|
const r = await fetch('/api/corpus/chat?token=' + token, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {'Content-Type': 'application/json'},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
message: msg,
|
|||
|
|
persona: 'zhuyuan',
|
|||
|
|
engine: 'deepseek'
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!r.ok) {
|
|||
|
|
hideTyping();
|
|||
|
|
addSystemMsg('❌ 请求失败 (' + r.status + ')');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Read SSE stream
|
|||
|
|
const reader = r.body.getReader();
|
|||
|
|
const decoder = new TextDecoder();
|
|||
|
|
let buffer = '';
|
|||
|
|
let fullText = '';
|
|||
|
|
let streamBubble = null;
|
|||
|
|
|
|||
|
|
while (true) {
|
|||
|
|
const {done, value} = await reader.read();
|
|||
|
|
if (done) break;
|
|||
|
|
|
|||
|
|
buffer += decoder.decode(value, {stream: true});
|
|||
|
|
|
|||
|
|
while (buffer.includes('\n\n')) {
|
|||
|
|
const idx = buffer.indexOf('\n\n');
|
|||
|
|
const event = buffer.slice(0, idx);
|
|||
|
|
buffer = buffer.slice(idx + 2);
|
|||
|
|
|
|||
|
|
if (!event.startsWith('data: ')) continue;
|
|||
|
|
const dataStr = event.slice(6).trim();
|
|||
|
|
if (dataStr === '[DONE]') continue;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(dataStr);
|
|||
|
|
const type = data.type || '';
|
|||
|
|
|
|||
|
|
if (type === 'persona-loaded') {
|
|||
|
|
hideTyping();
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
const d = document.createElement('div'); d.className = 'msg assistant';
|
|||
|
|
d.innerHTML = '<div class="label">助手</div><div class="bubble" id="stream-bubble"></div>';
|
|||
|
|
c.appendChild(d);
|
|||
|
|
streamBubble = document.getElementById('stream-bubble');
|
|||
|
|
} else if (type === 'token') {
|
|||
|
|
fullText += (data.token || '');
|
|||
|
|
if (!streamBubble) {
|
|||
|
|
hideTyping();
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
const d = document.createElement('div'); d.className = 'msg assistant';
|
|||
|
|
d.innerHTML = '<div class="label">助手</div><div class="bubble" id="stream-bubble"></div>';
|
|||
|
|
c.appendChild(d);
|
|||
|
|
streamBubble = document.getElementById('stream-bubble');
|
|||
|
|
}
|
|||
|
|
streamBubble.innerHTML = escHtml(fullText).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\`([^\`]+)\`/g,'<code>$1</code>');
|
|||
|
|
document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
|
|||
|
|
} else if (type === 'error') {
|
|||
|
|
hideTyping();
|
|||
|
|
addSystemMsg('⚠️ ' + (data.message || '错误'));
|
|||
|
|
}
|
|||
|
|
} catch(e) {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hideTyping();
|
|||
|
|
if (fullText && streamBubble) {
|
|||
|
|
streamBubble.innerHTML = escHtml(fullText).replace(/\n/g,'<br>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\`([^\`]+)\`/g,'<code>$1</code>');
|
|||
|
|
} else if (fullText) {
|
|||
|
|
addMsg('assistant', fullText);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch(e) {
|
|||
|
|
hideTyping();
|
|||
|
|
addSystemMsg('❌ 连接错误: ' + e.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function scrollToBottom() {
|
|||
|
|
const c = document.getElementById('chat-messages');
|
|||
|
|
c.scrollTop = c.scrollHeight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Sidebar ────────────────────────────────────────────────────────
|
|||
|
|
function switchSidebar(name, btn) {
|
|||
|
|
document.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
|
|||
|
|
document.querySelectorAll('.sidebar-content').forEach(t => t.style.display = 'none');
|
|||
|
|
if (btn) btn.classList.add('active');
|
|||
|
|
document.getElementById('sidebar-' + name).style.display = 'block';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateApiPanel() {
|
|||
|
|
document.getElementById('mcp-config').textContent =
|
|||
|
|
`{
|
|||
|
|
"mcpServers": {
|
|||
|
|
"corpus-agent": {
|
|||
|
|
"url": "https://guanghulab.com/mcp/corpus",
|
|||
|
|
"token": "${token.substring(0,8)}..."
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}`;
|
|||
|
|
document.getElementById('api-curl').textContent =
|
|||
|
|
`# 快速采集一段文本
|
|||
|
|
curl -X POST https://guanghulab.com/api/corpus/collect?token=${token.substring(0,8)}... \\
|
|||
|
|
-H 'Content-Type: application/json' \\
|
|||
|
|
-d '{"text":"...","source":"manual"}'
|
|||
|
|
|
|||
|
|
# 查看语料统计
|
|||
|
|
curl -s https://guanghulab.com/api/corpus/stats?token=${token.substring(0,8)}...`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function copyText(id) {
|
|||
|
|
const el = document.getElementById(id);
|
|||
|
|
navigator.clipboard.writeText(el.textContent).then(() => {
|
|||
|
|
const btn = el.nextElementSibling;
|
|||
|
|
const orig = btn.textContent;
|
|||
|
|
btn.textContent = '✅ 已复制';
|
|||
|
|
setTimeout(() => btn.textContent = orig, 1500);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Data ───────────────────────────────────────────────────────────
|
|||
|
|
async function loadStats() {
|
|||
|
|
if (!token) return;
|
|||
|
|
try {
|
|||
|
|
const r = await fetch('/api/corpus/stats?token=' + token);
|
|||
|
|
const d = await r.json();
|
|||
|
|
if (d.ok) {
|
|||
|
|
document.getElementById('stat-total').textContent = d.total_samples || 0;
|
|||
|
|
document.getElementById('stat-chars').textContent = (d.total_chars || 0).toLocaleString();
|
|||
|
|
}
|
|||
|
|
} catch(e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadSamples() {
|
|||
|
|
if (!token) return;
|
|||
|
|
const el = document.getElementById('sample-list');
|
|||
|
|
try {
|
|||
|
|
const r = await fetch('/api/corpus/list?token=' + token + '&page=1&size=20');
|
|||
|
|
const d = await r.json();
|
|||
|
|
if (!d.ok || !d.samples || d.samples.length === 0) {
|
|||
|
|
el.innerHTML = '<div class="empty-state">还没有语料<br>在对话区告诉 Agent 你想整理什么</div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
let html = '';
|
|||
|
|
for (const s of d.samples) {
|
|||
|
|
const msgs = s.messages || [];
|
|||
|
|
const first = msgs[0]?.content?.slice(0, 60) || '(空)';
|
|||
|
|
const tags = (s.tags || []).map(t => '<span class="tag">' + t + '</span>').join('');
|
|||
|
|
html += '<div class="sample-item"><div class="preview">' + escHtml(first) + '</div><div class="meta">' + tags + ' ' + (s.source || '') + '</div></div>';
|
|||
|
|
}
|
|||
|
|
el.innerHTML = html;
|
|||
|
|
} catch(e) {
|
|||
|
|
el.innerHTML = '<div class="empty-state">加载失败</div>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function exportAll() {
|
|||
|
|
if (!token) return;
|
|||
|
|
const r = await fetch('/api/corpus/export?token=' + token + '&format=jsonl');
|
|||
|
|
if (r.ok) {
|
|||
|
|
const blob = await r.blob();
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = URL.createObjectURL(blob);
|
|||
|
|
a.download = 'corpus_' + username + '_' + new Date().toISOString().slice(0,10) + '.jsonl';
|
|||
|
|
a.click();
|
|||
|
|
addSystemMsg('✅ 语料已导出:' + a.download);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Utilities ──────────────────────────────────────────────────────
|
|||
|
|
function escHtml(s) {
|
|||
|
|
if (typeof s !== 'string') s = String(s || '');
|
|||
|
|
const d = document.createElement('div');
|
|||
|
|
d.textContent = s;
|
|||
|
|
return d.innerHTML;
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|