Files
local-rig/local_rig/ui.html
T
hermes-mac13 93758f3720 feat: 从 tutti-selfhost 拆出 local_rig 独立仓
- local_rig/ 完整包(7 源文件 + ui.html + CHANGELOG)
- tests/test_local_rig.py + test_rig_real_agent.py(纯 local_rig 测试)
- scripts/15-criterion4-live.sh(判据④活环境门)
- README.md(定位/快速开始/与 tutti-selfhost 的关系)
- requirements.txt(唯一外部依赖 pyyaml)
- .gitignore

来源: figmar/tutti-selfhost @ 3b23853 (2026-10-06)
原 PR #43 已合入 tutti-selfhost main
2026-10-06 13:36:14 +08:00

166 lines
6.2 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Rig Dashboard</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#e6edf3;--dim:#8b949e;--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff}
body{font:14px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);padding:20px}
h1{font-size:18px;margin-bottom:4px}
.sub{color:var(--dim);font-size:12px;margin-bottom:20px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-bottom:20px}
.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px}
.card h3{font-size:13px;color:var(--dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
.member{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)}
.member:last-child{border:none}
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.dot.running{background:var(--green)}.dot.stopped{background:var(--dim)}.dot.ready{background:var(--blue)}
.mname{font-weight:600;font-size:13px}
.mmeta{color:var(--dim);font-size:11px;margin-left:auto}
.task{padding:8px 0;border-bottom:1px solid var(--border);font-size:13px}
.task:last-child{border:none}
.tid{color:var(--dim);font-size:11px;font-family:monospace}
.tprompt{margin:2px 0}
.tresult{color:var(--green);font-size:12px;max-height:60px;overflow:hidden}
.terror{color:var(--red);font-size:12px}
.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:11px;font-weight:600}
.badge.pending{background:#3d1d00;color:var(--yellow)}
.badge.running{background:#0d1f2d;color:var(--blue)}
.badge.done{background:#0d2818;color:var(--green)}
.badge.failed{background:#2d0d0d;color:var(--red)}
.badge.timeout{background:#2d0d2d;color:var(--purple)}
form{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
select,input,button{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:6px;font-size:13px}
select:focus,input:focus{outline:none;border-color:var(--blue)}
button{cursor:pointer;background:var(--blue);color:#fff;border:none;font-weight:600}
button:hover{opacity:.85}
button.sec{background:var(--card);color:var(--text);border:1px solid var(--border)}
#log{margin-top:12px;font-family:monospace;font-size:11px;color:var(--dim);max-height:120px;overflow-y:auto;background:var(--card);border-radius:6px;padding:8px;display:none}
</style>
</head>
<body>
<h1>🔧 Rig Dashboard</h1>
<div class="sub" id="sub">connecting...</div>
<form id="assign-form" onsubmit="return assign(event)">
<select id="f-assignee"></select>
<input id="f-prompt" placeholder="任务描述..." style="flex:1;min-width:200px" required>
<button type="submit">派发</button>
</form>
<div class="grid">
<div class="card"><h3>Members</h3><div id="members"></div></div>
<div class="card"><h3>Tasks</h3><div id="tasks"><div style="color:var(--dim)">loading...</div></div></div>
</div>
<div id="log"></div>
<script>
const BASE = location.origin;
let members = {};
async function api(path, opts) {
const r = await fetch(BASE + path, opts);
return r.json();
}
function log(msg) {
const el = document.getElementById('log');
el.style.display = 'block';
el.innerHTML += `<div>[${new Date().toLocaleTimeString()}] ${msg}</div>`;
el.scrollTop = el.scrollHeight;
}
async function refresh() {
try {
const [rig, status, tasks] = await Promise.all([
api('/rig'), api('/status'), api('/tasks?limit=20')
]);
// sub
document.getElementById('sub').textContent =
`${rig.name} · ${new Date().toLocaleTimeString()}`;
// members
const all = {};
for (const pod of rig.pods || []) {
for (const m of pod.members || []) {
all[pod.id + '.' + m.id] = m;
}
}
members = all;
const el = document.getElementById('members');
el.innerHTML = '';
for (const [mid, m] of Object.entries(all)) {
const st = status[mid] || 'stopped';
const dot = st === 'running' ? 'running' : st === 'ready' ? 'ready' : 'stopped';
const meta = m.runtime + (m.profile && m.profile !== 'default' ? ' · ' + m.profile : '');
el.innerHTML += `<div class="member">
<div class="dot ${dot}"></div>
<span class="mname">${mid}</span>
<span class="mmeta">${meta}</span>
</div>`;
}
// assignee dropdown
const sel = document.getElementById('f-assignee');
if (!sel.options.length) {
for (const mid of Object.keys(all)) {
const o = document.createElement('option');
o.value = mid; o.textContent = mid;
sel.appendChild(o);
}
}
// tasks
const tel = document.getElementById('tasks');
if (!tasks.length) {
tel.innerHTML = '<div style="color:var(--dim)">无任务</div>';
} else {
tel.innerHTML = tasks.map(t => {
let extra = '';
if (t.result) extra = `<div class="tresult">${esc(t.result.slice(0,200))}</div>`;
if (t.error) extra = `<div class="terror">${esc(t.error.slice(0,200))}</div>`;
return `<div class="task">
<span class="badge ${t.status}">${t.status}</span>
<span class="tid">${t.id.slice(0,16)}...</span>
<div class="tprompt">${esc(t.prompt)}</div>
${extra}
</div>`;
}).join('');
}
} catch (e) {
log('refresh error: ' + e.message);
}
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
async function assign(e) {
e.preventDefault();
const assignee = document.getElementById('f-assignee').value;
const prompt = document.getElementById('f-prompt').value;
if (!assignee || !prompt) return false;
try {
const r = await api('/tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({assignee, prompt})
});
log(`✅ ${r.id} → ${assignee}`);
document.getElementById('f-prompt').value = '';
refresh();
} catch (err) {
log('❌ ' + err.message);
}
return false;
}
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>