guanghulab/homepage/index_corpus_backup.html

492 lines
21 KiB
HTML
Raw Permalink Normal View History

<!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>