import { renderWorkspace } from "./agent-ui.js"; import { downloadMarkdown, sessionToMarkdown } from "./export.js"; import { renderIntakePanel } from "./intake-ui.js"; let sessionId = null; let activeBookId = null; let lastView = null; let books = []; let composerForceInput = false; const $ = (id) => document.getElementById(id); const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" }; const REASON = { skill_selection: "选择 skill", intake: "填写需求", input: "补充说明", worker_questions: "回答提问", approve_step: "确认执行", review_artifact: "验收产物", }; async function api(path, options = {}) { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "请求失败"); return data; } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } function statusFor(view, loading) { if (loading) return { cls: "running", text: "处理中…" }; if (view.phase === "done") return { cls: "done", text: "已完成" }; if (view.phase === "running" && !view.waitingReason) { return { cls: "running", text: view.focus?.action ?? "Agent 运行中" }; } if (view.waitingReason) { return { cls: "waiting", text: REASON[view.waitingReason.kind] ?? view.waitingReason.kind }; } return { cls: "", text: PHASE[view.phase] ?? view.phase }; } function isAgentBusy(view, loading) { if (loading) return true; return view.phase === "running" && !view.waitingReason; } function resolveComposer(view, loading) { if (view.phase === "done") return { mode: "idle", text: "会话已结束" }; if (isAgentBusy(view, loading)) { return { mode: "waiting", text: view.focus?.action ?? "Agent 或 Skill 执行中…" }; } const confirmIntake = view.actions?.find((a) => a.type === "confirm_intake"); if (view.waitingReason?.kind === "intake" && view.intake) { const send = view.actions?.find((a) => a.type === "send_message"); return { mode: view.intake.ready && confirmIntake ? "intake_ready" : "intake", intake: view.intake, intakePrompt: view.intakePrompt, confirmIntake, placeholder: send?.placeholder ?? "补充信息…", }; } const approve = view.actions?.find((a) => a.type === "approve"); const accept = view.actions?.find((a) => a.type === "accept"); if ((approve || accept) && !composerForceInput) { return { mode: "action", primary: approve ?? accept, primaryType: approve ? "approve" : "accept", hint: approve ? view.focus?.detail : "验收 Skill 产出", showReject: true, }; } const send = view.actions?.find((a) => a.type === "send_message"); if ( send || view.waitingReason?.kind === "input" || view.waitingReason?.kind === "skill_selection" || view.waitingReason?.kind === "worker_questions" || composerForceInput ) { let hint = view.hints?.[0] ?? null; if (view.waitingReason?.kind === "skill_selection") { hint = "选择 skill 包,或输入名称 / 编号"; } return { mode: "input", placeholder: send?.placeholder ?? "输入消息…", hint }; } const finish = view.actions?.find((a) => a.type === "finish"); if (finish) { return { mode: "action", primary: finish, primaryType: "finish", hint: null, showReject: false }; } return { mode: "idle", text: "暂无可用操作" }; } function renderComposer(view, loading) { const root = $("composer"); if (!root) return; const spec = resolveComposer(view, loading); if (spec.mode === "waiting") { root.innerHTML = `
${esc(spec.text)}
`; return; } if (spec.mode === "idle") { root.innerHTML = `
${esc(spec.text)}
`; return; } if (spec.mode === "intake" || spec.mode === "intake_ready") { const confirm = spec.mode === "intake_ready" && spec.confirmIntake ? `
` : ""; root.innerHTML = ` ${spec.intakePrompt ? `

${esc(spec.intakePrompt)}

` : ""}
${renderIntakePanel(spec.intake, { variant: "composer" })}
${confirm}
`; wireComposerForm(); root.querySelector("[data-act=confirm_intake]")?.addEventListener("click", () => runAction("confirm_intake")); return; } if (spec.mode === "action") { const reject = spec.showReject ? ` ` : ""; root.innerHTML = ` ${spec.hint ? `

${esc(spec.hint)}

` : ""}
${reject}
`; root.querySelector(`[data-act="${spec.primaryType}"]`)?.addEventListener("click", () => runAction(spec.primaryType)); root.querySelector("[data-act=reject]")?.addEventListener("click", () => runAction("reject")); root.querySelector("[data-act=force-input]")?.addEventListener("click", () => { composerForceInput = true; renderComposer(view, loading); }); return; } root.innerHTML = ` ${spec.hint ? `

${esc(spec.hint)}

` : ""}
`; wireComposerForm(); } function wireComposerForm() { const form = $("composer-form"); const input = $("composer-input"); form?.addEventListener("submit", async (e) => { e.preventDefault(); await sendText(input?.value ?? ""); }); input?.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); form?.requestSubmit(); } }); input?.focus(); } function renderBookList() { const list = $("book-list"); if (!list) return; if (!books.length) { list.innerHTML = ``; $("btn-new-inline")?.addEventListener("click", openNewBookDialog); return; } list.innerHTML = ""; for (const book of books) { const btn = document.createElement("button"); btn.type = "button"; btn.className = `book-item${book.id === activeBookId ? " active" : ""}`; const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "未选 skill"; btn.innerHTML = `
${esc(book.title)}
${esc(skill)} · ${esc(book.preview?.slice(0, 40) || "")}
`; btn.addEventListener("click", () => openBook(book.id)); list.appendChild(btn); } } function renderHeader(view, loading) { const st = statusFor(view, loading); $("work-title").textContent = view.bookTitle ?? "未命名作品"; const skill = view.activeSkill ?? "未选 skill"; const phase = view.waitingReason ? REASON[view.waitingReason.kind] ?? view.phase : PHASE[view.phase] ?? view.phase; $("work-meta").textContent = `${skill} · ${phase}`; $("status-dot").className = `status-dot ${st.cls}`; $("status-text").textContent = st.text; $("btn-delete").hidden = !view.bookId; $("btn-saves").hidden = !view.bookId; $("btn-export").disabled = !(view.messages?.length); } function renderEmpty() { sessionId = null; lastView = null; activeBookId = null; composerForceInput = false; $("work-title").textContent = "未打开作品"; $("work-meta").textContent = ""; $("status-dot").className = "status-dot"; $("status-text").textContent = "—"; $("btn-delete").hidden = true; $("btn-saves").hidden = true; $("btn-export").disabled = true; $("message-feed").innerHTML = `

点击左侧 + 新建作品

`; $("skill-picker").hidden = true; $("skill-guide").hidden = true; $("agent-focus").innerHTML = ""; $("agent-timeline").innerHTML = ""; $("tool-trace").hidden = true; document.body.dataset.lifecycle = "design"; $("composer").innerHTML = `
暂无打开的作品
`; renderBookList(); } function renderSession(view, loading = false) { lastView = view; sessionId = view.id; activeBookId = view.bookId ?? activeBookId; if (!loading && !["approve_step", "review_artifact"].includes(view.waitingReason?.kind)) { composerForceInput = false; } renderHeader(view, loading); renderBookList(); renderWorkspace(view, loading, (skillId) => sendText(skillId)); renderComposer(view, loading); } async function sendText(text) { const trimmed = text?.trim(); if (!trimmed || !sessionId) return; try { renderSession(lastView, true); const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/messages`, { method: "POST", body: JSON.stringify({ text: trimmed }), }); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function runAction(action) { if (!sessionId) return; try { renderSession(lastView, true); const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/actions`, { method: "POST", body: JSON.stringify({ action }), }); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function setLifecycle(stage) { if (!sessionId || stage === lastView?.lifecycleStage) return; if (stage === "play" && !lastView?.playReady) return; try { const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/lifecycle`, { method: "POST", body: JSON.stringify({ stage }), }); renderSession(view, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function loadBooks() { const data = await api("/api/books"); books = data.books ?? []; renderBookList(); } function openNewBookDialog() { $("input-book-title").value = ""; $("dialog-new-book").showModal(); $("input-book-title").focus(); } async function createBook() { const title = $("input-book-title").value.trim() || "未命名作品"; $("btn-create-book").disabled = true; try { const data = await api("/api/books", { method: "POST", body: JSON.stringify({ title }), }); $("dialog-new-book").close(); if (data.book) books.unshift(data.book); activeBookId = data.book?.id; renderSession(data.session, false); } catch (err) { alert(err.message); } finally { $("btn-create-book").disabled = false; } } async function openBook(bookId) { activeBookId = bookId; renderBookList(); if (lastView) renderSession(lastView, true); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/open`, { method: "POST" }); if (data.book) { const i = books.findIndex((b) => b.id === bookId); if (i >= 0) books[i] = { ...books[i], ...data.book }; } renderSession(data.session, false); } catch (err) { if (lastView) renderSession({ ...lastView, hints: [err.message] }, false); } } async function deleteBook() { if (!activeBookId || !lastView?.bookTitle) return; if (!confirm(`删除「${lastView.bookTitle}」?不可恢复。`)) return; try { await api(`/api/books/${encodeURIComponent(activeBookId)}`, { method: "DELETE" }); books = books.filter((b) => b.id !== activeBookId); if (books.length) await openBook(books[0].id); else renderEmpty(); } catch (err) { alert(err.message); } } function exportSession() { if (!lastView) return; const name = (lastView.bookTitle ?? "session").replace(/[\\/:*?"<>|]/g, "_"); downloadMarkdown(`${name}.md`, sessionToMarkdown(lastView)); } async function refreshSaves() { if (!activeBookId) return; const data = await api(`/api/books/${encodeURIComponent(activeBookId)}/saves`); const list = $("saves-list"); const saves = data.saves ?? []; if (!saves.length) { list.innerHTML = `

暂无存档

`; return; } list.innerHTML = saves .map( (s) => `
${esc(s.label)} ${esc(s.kind)}
`, ) .join(""); list.querySelectorAll("[data-load]").forEach((btn) => { btn.addEventListener("click", async () => { try { const data = await api( `/api/books/${encodeURIComponent(activeBookId)}/saves/${encodeURIComponent(btn.getAttribute("data-load"))}/load`, { method: "POST" }, ); $("dialog-saves").close(); renderSession(data.session, false); } catch (err) { alert(err.message); } }); }); list.querySelectorAll("[data-del]").forEach((btn) => { btn.addEventListener("click", async () => { if (!confirm("删除此存档?")) return; await api( `/api/books/${encodeURIComponent(activeBookId)}/saves/${encodeURIComponent(btn.getAttribute("data-del"))}`, { method: "DELETE" }, ); refreshSaves(); }); }); } async function createSave() { const label = $("input-save-label").value.trim(); const kind = document.querySelector('input[name="save-kind"]:checked')?.value ?? "run"; if (!label) return alert("请输入名称"); try { await api(`/api/books/${encodeURIComponent(activeBookId)}/saves`, { method: "POST", body: JSON.stringify({ label, kind, sessionId }), }); $("input-save-label").value = ""; refreshSaves(); } catch (err) { alert(err.message); } } $("btn-new-book")?.addEventListener("click", openNewBookDialog); $("btn-new-inline")?.addEventListener("click", openNewBookDialog); $("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close()); $("form-new-book")?.addEventListener("submit", (e) => { e.preventDefault(); createBook(); }); $("lifecycle-toggle")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-stage]"); if (!btn?.disabled) setLifecycle(btn.getAttribute("data-stage")); }); $("btn-delete")?.addEventListener("click", deleteBook); $("btn-export")?.addEventListener("click", exportSession); $("btn-saves")?.addEventListener("click", () => { $("dialog-saves").showModal(); refreshSaves(); }); $("btn-close-saves")?.addEventListener("click", () => $("dialog-saves").close()); $("btn-save-create")?.addEventListener("click", createSave); async function init() { try { await loadBooks(); if (books.length) await openBook(books[0].id); else renderEmpty(); } catch { $("status-text").textContent = "加载失败"; } } init();