Initial commit

This commit is contained in:
2026-07-10 08:31:27 +08:00
commit 2b74c30d36
134 changed files with 21801 additions and 0 deletions

276
web/agent-ui.js Normal file
View File

@@ -0,0 +1,276 @@
import { renderIntakePanel } from "./intake-ui.js";
const HIDE_KINDS = new Set(["worker_stub"]);
const MSG_CLASS = {
user_input: "user",
agent_tool: "agent",
orchestrator_decision: "agent",
orchestrator_prompt: "agent",
worker_running: "skill",
worker_output: "skill",
worker_questions: "skill",
error: "error",
};
const MSG_LABEL = {
user_input: "你",
agent_tool: "Tool",
orchestrator_decision: "Agent",
orchestrator_prompt: "Agent",
worker_running: "Skill",
worker_output: "Skill",
worker_questions: "Skill 提问",
error: "错误",
system_info: "系统",
};
function esc(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function fmtTime(iso) {
if (!iso) return "";
return new Date(iso).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}
function msgLabel(msg) {
const k = msg.kind ?? (msg.role === "user" ? "user_input" : "system_info");
if (k.startsWith("worker") && msg.actor) return `Skill · ${msg.actor}`;
return MSG_LABEL[k] ?? k;
}
function msgBody(msg, view) {
let body = (msg.body ?? msg.text ?? "").trim();
if (msg.kind === "worker_questions" && !body) {
const wr = view.waitingReason;
if (wr?.kind === "worker_questions") {
const qs = (wr.questions ?? []).filter(Boolean);
if (qs.length) body = qs.map((q) => `- ${q}`).join("\n");
}
if (!body) body = "请补充当前 skill 需要的信息。";
}
return body;
}
export function renderSkillPicker(view, onPick) {
const el = document.getElementById("skill-picker");
if (!el) return;
const selecting = view.waitingReason?.kind === "skill_selection";
const skills = view.skills ?? [];
if (!selecting || !skills.length) {
el.hidden = true;
el.innerHTML = "";
return;
}
el.hidden = false;
el.innerHTML = `
<h2>选择 skill 包</h2>
<p>Agent 需要知道你要用哪套能力。选一项开始实例化(也可在下方输入名称或编号)。</p>
<div class="skill-grid" id="skill-grid"></div>`;
const grid = el.querySelector("#skill-grid");
skills.forEach((skill, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "skill-card";
btn.innerHTML = `
<div class="skill-card-name">${esc(skill.name)}</div>
<div class="skill-card-desc">${esc(skill.description || "无说明")}</div>
<div class="skill-card-tag">${esc(skill.category || "")} · #${i + 1}</div>`;
btn.addEventListener("click", () => onPick(skill.name));
grid.appendChild(btn);
});
}
export function renderLifecycle(view) {
const toggle = document.getElementById("lifecycle-toggle");
if (!toggle) return;
const stage = view.lifecycleStage ?? "design";
document.body.dataset.lifecycle = stage;
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
const s = btn.getAttribute("data-stage");
btn.classList.toggle("active", s === stage);
if (s === "play") {
btn.disabled = !view.playReady;
btn.title = view.playReady ? "" : "实例就绪后可切换";
} else {
btn.disabled = false;
}
});
}
export function renderSkillGuide(view) {
const wrap = document.getElementById("skill-guide");
const list = document.getElementById("skill-guide-list");
if (!wrap || !list) return;
const show = view.lifecycleStage === "design" && (view.skillCatalog?.length ?? 0) > 0;
if (!show) {
wrap.hidden = true;
return;
}
wrap.hidden = false;
list.innerHTML = view.skillCatalog
.map(
(s) => `
<div class="skill-guide-item ${s.status}">
<span class="skill-guide-dot"></span>
<div>
<div class="skill-guide-label">${esc(s.label)} <code>${esc(s.id)}</code></div>
<div class="skill-guide-purpose">${esc(s.purpose)}</div>
</div>
</div>`,
)
.join("");
}
export function renderAgentPanel(view, loading) {
const focusEl = document.getElementById("agent-focus");
const traceEl = document.getElementById("tool-trace");
const timelineEl = document.getElementById("agent-timeline");
const badgeEl = document.getElementById("burst-badge");
if (!focusEl || !timelineEl) return;
const f = view.focus;
if (f) {
focusEl.innerHTML = `
<div class="agent-focus-who">${esc(f.actorLabel)}</div>
<div class="agent-focus-action">${esc(loading ? "处理中…" : f.action)}</div>
${f.detail ? `<div class="agent-focus-detail">${esc(f.detail)}</div>` : ""}`;
} else {
focusEl.innerHTML = `<div class="agent-focus-detail">待命</div>`;
}
if (badgeEl && view.burst) {
const show = view.burst.count > 0 || view.phase === "running";
badgeEl.hidden = !show;
badgeEl.textContent = `burst ${view.burst.count}/${view.burst.max}`;
}
if (traceEl) {
const trace = view.toolTrace ?? [];
if (!trace.length) {
traceEl.hidden = true;
traceEl.innerHTML = "";
} else {
traceEl.hidden = false;
traceEl.innerHTML = trace
.map(
(t) => `
<div class="tool-trace-item">
<span class="tool-trace-name">${esc(t.name)}</span>
<span style="float:right;color:var(--muted)">${fmtTime(t.at)}</span>
<div>${esc(t.summary || "—")}</div>
</div>`,
)
.join("");
}
}
const items = [];
for (const msg of view.messages ?? []) {
if (msg.role === "user") {
items.push({ kind: "user_input", title: "你", body: msg.text, at: msg.createdAt });
continue;
}
const kind = msg.kind ?? "system_info";
if (HIDE_KINDS.has(kind)) continue;
items.push({
kind,
title: msg.title ?? msgLabel(msg),
body: msgBody(msg, view).slice(0, 160),
at: msg.createdAt,
});
}
if (loading && f) {
items.push({ kind: "pending", title: f.action, body: f.detail ?? "", at: "" });
}
if (!items.length) {
timelineEl.innerHTML = `<p class="timeline-empty">调度记录将出现在这里</p>`;
return;
}
timelineEl.innerHTML = items
.map(
(it) => `
<div class="timeline-item">
<strong>${esc(it.title)}</strong>
<span style="color:var(--muted);margin-left:6px">${fmtTime(it.at)}</span>
<div style="margin-top:2px;color:var(--muted)">${esc(it.body)}</div>
</div>`,
)
.join("");
timelineEl.scrollTop = timelineEl.scrollHeight;
}
export function renderMessageFeed(view, loading) {
const feed = document.getElementById("message-feed");
if (!feed) return;
const intake =
view.waitingReason?.kind === "intake" && view.intake?.fields?.length;
const selecting = view.waitingReason?.kind === "skill_selection";
feed.innerHTML = "";
if (intake) {
const box = document.createElement("div");
box.className = "intake-panel";
box.innerHTML = renderIntakePanel(view.intake, { variant: "feed" });
feed.appendChild(box);
}
const visible = (view.messages ?? []).filter((m) => {
if (m.role === "user") return true;
return !HIDE_KINDS.has(m.kind ?? "");
});
if (!visible.length && !loading && !intake) {
const p = document.createElement("p");
p.className = "empty";
if (selecting) {
p.textContent = "在上方选择 skill 包,或在下方输入 skill 名称。";
} else if (view.lifecycleStage === "play") {
p.textContent = "游玩模式Agent 将调度 run skill推进世界与叙事。";
} else {
p.textContent = "创作模式:与 Agent 对话,完成实例化后可切到游玩。";
}
feed.appendChild(p);
return;
}
for (const msg of visible) {
const isUser = msg.role === "user";
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
const card = document.createElement("article");
card.className = `msg ${MSG_CLASS[kind] ?? "system"}`;
card.innerHTML = `
<header class="msg-head">
<span class="msg-tag">${esc(msgLabel(msg))}</span>
<span>${esc(msg.title ?? "")}</span>
<span class="msg-time">${fmtTime(msg.createdAt)}</span>
</header>
<div class="msg-body">${esc(msgBody(msg, view))}</div>`;
feed.appendChild(card);
}
if (loading) {
const pending = document.createElement("article");
pending.className = "msg agent msg-pending";
pending.innerHTML = `
<header class="msg-head"><span class="msg-tag">进行中</span><span>${esc(view.focus?.action ?? "处理中")}</span></header>
<div class="msg-body">…</div>`;
feed.appendChild(pending);
}
feed.scrollTop = feed.scrollHeight;
}
export function renderWorkspace(view, loading, onPickSkill) {
renderLifecycle(view);
renderSkillPicker(view, onPickSkill);
renderMessageFeed(view, loading);
renderSkillGuide(view);
renderAgentPanel(view, loading);
}