346 lines
14 KiB
HTML
346 lines
14 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>语料采集系统 · Corpus Agent</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
|
||
.container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||
|
||
/* Header */
|
||
.header { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: white; padding: 40px 20px; text-align: center; border-radius: 12px; margin-bottom: 24px; }
|
||
.header h1 { font-size: 28px; margin-bottom: 8px; }
|
||
.header p { color: #8899aa; font-size: 14px; }
|
||
|
||
/* Cards */
|
||
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||
.card h2 { font-size: 18px; margin-bottom: 16px; color: #1a1a2e; }
|
||
|
||
/* Login / User area */
|
||
.login-box { text-align: center; padding: 40px; }
|
||
.login-box input { padding: 12px 16px; width: 300px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; margin-bottom: 12px; }
|
||
.login-box input:focus { outline: none; border-color: #4a6cf7; }
|
||
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; transition: 0.2s; font-weight: 500; }
|
||
.btn-primary { background: #4a6cf7; color: white; }
|
||
.btn-primary:hover { background: #3b5de7; }
|
||
.btn-success { background: #10b981; color: white; }
|
||
.btn-success:hover { background: #059669; }
|
||
.btn-danger { background: #ef4444; color: white; }
|
||
.btn-danger:hover { background: #dc2626; }
|
||
.btn-sm { padding: 6px 14px; font-size: 12px; }
|
||
|
||
.user-info { display: flex; justify-content: space-between; align-items: center; }
|
||
.user-info .badge { background: #e8f0fe; color: #4a6cf7; padding: 4px 12px; border-radius: 20px; font-size: 13px; }
|
||
|
||
/* Stats grid */
|
||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 16px 0; }
|
||
.stat-card { background: #f8f9fc; border-radius: 8px; padding: 16px; text-align: center; }
|
||
.stat-card .num { font-size: 28px; font-weight: 700; color: #1a1a2e; }
|
||
.stat-card .label { font-size: 12px; color: #8899aa; margin-top: 4px; }
|
||
|
||
/* Sample list */
|
||
.sample-list { max-height: 400px; overflow-y: auto; }
|
||
.sample-item { padding: 12px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: 0.1s; }
|
||
.sample-item:hover { background: #f8f9fc; }
|
||
.sample-item:last-child { border: none; }
|
||
.sample-item .msg { font-size: 13px; color: #666; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.sample-item .meta { font-size: 11px; color: #999; margin-top: 4px; }
|
||
.sample-item .tag { display: inline-block; background: #e8f0fe; color: #4a6cf7; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-right: 4px; }
|
||
|
||
/* Tabs */
|
||
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
||
.tab { padding: 8px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; background: #f0f2f5; color: #666; transition: 0.2s; border: none; }
|
||
.tab.active { background: #4a6cf7; color: white; }
|
||
|
||
.hidden { display: none; }
|
||
|
||
/* Preview / input area */
|
||
.preview-area { background: #f8f9fc; border-radius: 8px; padding: 16px; margin: 12px 0; font-size: 13px; line-height: 1.6; max-height: 300px; overflow-y: auto; }
|
||
.preview-area .valuable { border-left: 3px solid #10b981; padding-left: 12px; margin: 8px 0; }
|
||
.preview-area .filtered { border-left: 3px solid #ef4444; padding-left: 12px; margin: 8px 0; opacity: 0.5; }
|
||
|
||
/* Mac client download */
|
||
.download-section { text-align: center; padding: 20px; }
|
||
.download-section p { color: #666; font-size: 13px; margin: 8px 0; }
|
||
code { background: #f0f2f5; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
|
||
|
||
/* Footer */
|
||
.footer { text-align: center; color: #999; font-size: 12px; padding: 20px 0; }
|
||
|
||
/* Empty state */
|
||
.empty-state { text-align: center; padding: 40px; color: #999; }
|
||
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container" id="app">
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<h1>🧠 语料采集系统</h1>
|
||
<p>Corpus Agent · 自动采集 · 脱敏 · 格式化</p>
|
||
</div>
|
||
|
||
<!-- Login -->
|
||
<div class="card" id="login-section">
|
||
<div class="login-box">
|
||
<h2 style="margin-bottom:8px">登录</h2>
|
||
<p style="color:#666; font-size:13px; margin-bottom:20px">
|
||
使用 Gitea Access Token 登录
|
||
</p>
|
||
<input type="password" id="token-input" placeholder="输入 Gitea Access Token">
|
||
<br>
|
||
<button class="btn btn-primary" onclick="login()">登录</button>
|
||
<p style="color:#999; font-size:12px; margin-top:12px">
|
||
访问 <a href="https://guanghulab.com/user/settings/applications" target="_blank" style="color:#4a6cf7">Gitea → 设置 → 应用</a> 创建 Token
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main (after login) -->
|
||
<div id="main-section" class="hidden">
|
||
<!-- User info -->
|
||
<div class="card">
|
||
<div class="user-info">
|
||
<div>
|
||
<strong id="display-name">用户</strong>
|
||
<span class="badge" id="sample-count">0 条语料</span>
|
||
</div>
|
||
<button class="btn btn-sm btn-danger" onclick="logout()">退出</button>
|
||
</div>
|
||
|
||
<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 class="stat-card">
|
||
<div class="num" id="stat-sources">0</div>
|
||
<div class="label">来源渠道</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('collection')">📥 实时采集</button>
|
||
<button class="tab" onclick="switchTab('browse')">📚 语料浏览</button>
|
||
<button class="tab" onclick="switchTab('client')">💻 Mac 客户端</button>
|
||
</div>
|
||
|
||
<!-- Tab: 实时采集 -->
|
||
<div class="card" id="tab-collection">
|
||
<h2>📥 实时采集</h2>
|
||
<p style="color:#666; font-size:13px; margin-bottom:12px">粘贴对话文本,引擎自动判断价值并脱敏存储</p>
|
||
<textarea id="input-text" style="width:100%;height:120px;padding:12px;border:2px solid #e0e0e0;border-radius:8px;font-size:13px;resize:vertical" placeholder="粘贴聊天内容..."></textarea>
|
||
<div style="margin-top:12px">
|
||
<button class="btn btn-primary" onclick="collectText()">采集</button>
|
||
<button class="btn btn-sm" style="margin-left:8px" onclick="document.getElementById('input-text').value=''">清空</button>
|
||
</div>
|
||
<div id="collect-result" class="hidden preview-area"></div>
|
||
</div>
|
||
|
||
<!-- Tab: 语料浏览 -->
|
||
<div class="card hidden" id="tab-browse">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||
<h2 style="margin:0">📚 语料库</h2>
|
||
<div>
|
||
<button class="btn btn-sm btn-success" onclick="exportCorpus()">📥 导出 JSONL</button>
|
||
</div>
|
||
</div>
|
||
<div id="sample-container">
|
||
<div class="empty-state">
|
||
<div class="icon">📭</div>
|
||
<p>还没有语料,开始采集吧</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Mac 客户端 -->
|
||
<div class="card hidden" id="tab-client">
|
||
<div class="download-section">
|
||
<h2>💻 Mac 客户端</h2>
|
||
<p>在 Mac 上运行的桌面采集助手,自动截图→OCR→筛选→上传</p>
|
||
|
||
<div style="text-align:left;max-width:600px;margin:20px auto">
|
||
<h3 style="font-size:14px;margin-bottom:12px">📋 安装步骤</h3>
|
||
<ol style="font-size:13px;color:#666;line-height:2">
|
||
<li>下载 <code>mac-corpus-agent.py</code> 到你的 Mac</li>
|
||
<li>安装依赖:<code>pip3 install pyautogui pillow websocket-client</code></li>
|
||
<li>运行:<code>python3 mac-corpus-agent.py --token YOUR_TOKEN</code></li>
|
||
<li>选择模式:<code>--mode auto</code>(实时)或 <code>--mode scroll</code>(滚屏)</li>
|
||
</ol>
|
||
|
||
<h3 style="font-size:14px;margin:12px 0 8px">⚙️ 模式说明</h3>
|
||
<table style="width:100%;font-size:13px;border-collapse:collapse">
|
||
<tr style="border-bottom:1px solid #eee">
|
||
<td style="padding:8px"><code>auto</code> 实时模式</td>
|
||
<td style="padding:8px;color:#666">在后台运行,监控屏幕变化,自动采集</td>
|
||
</tr>
|
||
<tr style="border-bottom:1px solid #eee">
|
||
<td style="padding:8px"><code>scroll</code> 滚屏模式</td>
|
||
<td style="padding:8px;color:#666">自动滚屏采集历史对话(需手动滚到顶部)</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:8px"><code>clipboard</code> 剪贴板模式</td>
|
||
<td style="padding:8px;color:#666">监听剪贴板,自动采集复制的内容</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p style="color:#999;font-size:12px;margin-top:20px">
|
||
Mac 客户端需要 macOS 12.0+,使用系统原生 OCR
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer">
|
||
语料采集系统 v1.0 · 国作登字-2026-A-00037559
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let token = localStorage.getItem('corpus_token') || '';
|
||
let username = '';
|
||
const API = '';
|
||
|
||
async function api(method, path, body) {
|
||
const url = `${API}${path}${path.includes('?') ? '&' : '?'}token=${token}`;
|
||
const opts = { method, headers: {'Content-Type': 'application/json'} };
|
||
if (body) opts.body = JSON.stringify(body);
|
||
const resp = await fetch(url, opts);
|
||
return resp.json();
|
||
}
|
||
|
||
// --- Login ---
|
||
async function login() {
|
||
const t = document.getElementById('token-input').value.trim();
|
||
if (!t) return alert('请输入Token');
|
||
const r = await fetch(`${API}/api/auth/login`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({gitea_token: t})
|
||
});
|
||
const d = await r.json();
|
||
if (!d.ok) return alert('登录失败: ' + JSON.stringify(d));
|
||
token = d.token;
|
||
username = d.username;
|
||
localStorage.setItem('corpus_token', token);
|
||
document.getElementById('login-section').classList.add('hidden');
|
||
document.getElementById('main-section').classList.remove('hidden');
|
||
document.getElementById('display-name').textContent = username;
|
||
document.getElementById('sample-count').textContent = d.stats.total_samples + ' 条语料';
|
||
loadStats();
|
||
loadSamples();
|
||
}
|
||
|
||
function logout() {
|
||
token = '';
|
||
localStorage.removeItem('corpus_token');
|
||
document.getElementById('login-section').classList.remove('hidden');
|
||
document.getElementById('main-section').classList.add('hidden');
|
||
}
|
||
|
||
// Auto login on load
|
||
if (token) {
|
||
api('GET', '/api/auth/check').then(d => {
|
||
if (d.ok) {
|
||
username = d.username;
|
||
document.getElementById('login-section').classList.add('hidden');
|
||
document.getElementById('main-section').classList.remove('hidden');
|
||
document.getElementById('display-name').textContent = username;
|
||
loadStats();
|
||
loadSamples();
|
||
} else {
|
||
localStorage.removeItem('corpus_token');
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Tabs ---
|
||
function switchTab(name) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('[id^="tab-"]').forEach(t => t.classList.add('hidden'));
|
||
event.target.classList.add('active');
|
||
document.getElementById('tab-' + name).classList.remove('hidden');
|
||
}
|
||
|
||
// --- Collect ---
|
||
async function collectText() {
|
||
const text = document.getElementById('input-text').value.trim();
|
||
if (!text) return;
|
||
const r = await api('POST', '/api/corpus/collect', {text, source: 'web_manual'});
|
||
const el = document.getElementById('collect-result');
|
||
el.classList.remove('hidden');
|
||
if (r.collected > 0) {
|
||
el.innerHTML = `<div class="valuable">✅ 采集了 ${r.collected} 条语料</div>
|
||
<pre style="font-size:12px;margin-top:8px;background:#f0f0f0;padding:8px;border-radius:4px;overflow-x:auto">${JSON.stringify(r.preview, null, 2)}</pre>`;
|
||
loadStats();
|
||
} else {
|
||
el.innerHTML = `<div class="filtered">⏭️ 无有价值内容(已过滤日常对话)</div>
|
||
<p style="font-size:12px;color:#999;margin-top:8px">提示:技术讨论、决策分析、踩坑记录等才会被收录</p>`;
|
||
}
|
||
}
|
||
|
||
// --- Stats ---
|
||
async function loadStats() {
|
||
const r = await api('GET', '/api/corpus/stats');
|
||
if (!r.ok) return;
|
||
document.getElementById('stat-total').textContent = r.total_samples || 0;
|
||
document.getElementById('stat-chars').textContent = (r.total_chars || 0).toLocaleString();
|
||
document.getElementById('stat-sources').textContent = Object.keys(r.by_source || {}).length;
|
||
document.getElementById('sample-count').textContent = (r.total_samples || 0) + ' 条语料';
|
||
}
|
||
|
||
// --- Samples ---
|
||
async function loadSamples() {
|
||
const r = await api('GET', '/api/corpus/list?page=1&size=50');
|
||
if (!r.ok) return;
|
||
const container = document.getElementById('sample-container');
|
||
if (r.samples.length === 0) {
|
||
container.innerHTML = `<div class="empty-state"><div class="icon">📭</div><p>还没有语料,开始采集吧</p></div>`;
|
||
return;
|
||
}
|
||
let html = '<div class="sample-list">';
|
||
for (const s of r.samples) {
|
||
const msgs = s.messages || [];
|
||
const first = msgs[0]?.content?.slice(0, 80) || '(空)';
|
||
const tags = (s.tags || []).map(t => `<span class="tag">${t}</span>`).join('');
|
||
const src = s.source || 'unknown';
|
||
html += `<div class="sample-item">
|
||
<div class="msg">${escapeHtml(first)}</div>
|
||
<div class="meta">${tags}<span style="margin-left:8px">来源: ${src}</span></div>
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// --- Export ---
|
||
async function exportCorpus() {
|
||
const r = await fetch(`${API}/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();
|
||
}
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|