346 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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