import { renderWorkspace, updateLiveStreamPanel } from "./agent-ui.js"; import { downloadMarkdown, sessionToMarkdown } from "./export.js"; import { renderIntakePanel } from "./intake-ui.js"; import { displaySkillPackLabel } from "./display-labels.js"; import { clearQuestionCardState, collectQuestionAnswers, getActiveQuestions, renderQuestionsCard, } from "./questions-ui.js"; let sessionId = null; let activeBookId = null; let lastView = null; let books = []; let composerForceInput = false; let sidebarNav = { level: "root" }; let bookSelectMode = false; let selectedBookIds = new Set(); const playSavesByBook = new Map(); const playSavesLoading = new Set(); let bookMenuBookId = null; let livePollTimer = null; const LIVE_POLL_MS = 280; const $ = (id) => document.getElementById(id); const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" }; const REASON = { skill_selection: "选择能力包", intake: "补充信息", input: "等待输入", approve_step: "确认执行", review_artifact: "验收产物", worker_questions: "回答提问", revision: "按反馈修改", }; /** @type {{ id: string, name: string, description: string }[]} */ let directors = []; 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 ?? "总管运行中" }; } 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 (view.phase === "error") { const send = view.actions?.find((a) => a.type === "send_message"); return { mode: "input", placeholder: send?.placeholder ?? "说明后重试,或直接发送继续…", hint: view.hints?.[0] ?? "执行出错,可在下方重试", }; } if (isAgentBusy(view, loading)) { return { mode: "waiting", text: view.focus?.action ?? "总管或 Worker 执行中…" }; } const confirmIntake = view.actions?.find((a) => a.type === "confirm_intake"); if (view.waitingReason?.kind === "intake" && view.intake) { const hasUser = (view.messages ?? []).some((m) => m.role === "user"); const send = view.actions?.find((a) => a.type === "send_message"); return { mode: view.intake.ready && confirmIntake ? "intake_ready" : "intake", intake: view.intake, intakePrompt: hasUser ? view.intakePrompt : null, showIntakePanel: hasUser, confirmIntake, placeholder: send?.placeholder ?? "描述你想创作什么…", }; } const approve = view.actions?.find((a) => a.type === "approve"); const accept = view.actions?.find((a) => a.type === "accept"); if (approve && !composerForceInput) { return { mode: "action", primary: approve, primaryType: "approve", hint: view.focus?.detail, showReject: true, }; } if (accept && view.waitingReason?.kind !== "review_artifact" && !composerForceInput) { return { mode: "action", primary: accept, primaryType: "accept", hint: "验收产物", showReject: true, }; } const send = view.actions?.find((a) => a.type === "send_message"); const hasQuestionsCard = (view.waitingReason?.kind === "worker_questions" && view.waitingReason?.questions?.length) || (view.waitingReason?.kind === "input" && view.waitingReason?.questions?.length) || (view.waitingReason?.kind === "review_artifact" && view.waitingReason?.questions?.length); if (view.waitingReason?.kind === "review_artifact") { const parseBroken = Boolean(view.reviewArtifact?.workerSetView?.parseError); return { mode: "input", placeholder: send?.placeholder ?? "输入修改意见后发送…", hint: hasQuestionsCard ? "接受产物即收起追问(表示无需再完善)" : null, acceptAction: accept ? { label: accept.label || "接受目前产物", disabled: parseBroken, disabledTitle: parseBroken ? "规格解析失败,请先发送修改意见让 Worker 重写" : undefined, } : null, }; } if (hasQuestionsCard && !composerForceInput) { return { mode: "input", placeholder: send?.placeholder ?? "也可在此自由补充…", hint: null, }; } if ( send || view.waitingReason?.kind === "input" || view.waitingReason?.kind === "worker_questions" || composerForceInput ) { const wr = view.waitingReason; if (wr?.kind === "worker_questions" && !hasQuestionsCard) { return { mode: "input", placeholder: "在此直接回答上面的问题…", hint: null, }; } const hint = view.hints?.[0] ?? null; 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.intakePrompt)}
` : ""} ${intakePanel} ${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)}
` : ""}${esc(spec.hint)}
` : ""} `; wireComposerForm(); root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept")); } 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 bookPlayReady(bookId) { return lastView?.bookId === bookId && Boolean(lastView?.playReady); } async function fetchPlaySavesForBook(bookId, force = false) { if (!force && (playSavesByBook.has(bookId) || playSavesLoading.has(bookId))) return; playSavesLoading.add(bookId); renderBookList(); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/saves`); playSavesByBook.set(bookId, data.saves ?? []); } catch { playSavesByBook.set(bookId, []); } finally { playSavesLoading.delete(bookId); renderBookList(); } } function setBookSelectMode(on, { keepSelection = false } = {}) { bookSelectMode = on; if (!on) selectedBookIds = new Set(); else if (!keepSelection) selectedBookIds = new Set(); renderBookList(); } function toggleBookSelection(bookId) { if (!bookSelectMode) setBookSelectMode(true, { keepSelection: true }); if (selectedBookIds.has(bookId)) selectedBookIds.delete(bookId); else selectedBookIds.add(bookId); renderBookList(); } function hideBookMenu() { const menu = $("book-action-menu"); if (menu) menu.hidden = true; bookMenuBookId = null; } function refreshBookMenuLabels() { const n = selectedBookIds.size; const multi = bookSelectMode && n > 0; $("book-menu-open")?.toggleAttribute("hidden", multi); $("book-menu-rename")?.toggleAttribute("hidden", n !== 1); const dup = $("book-menu-duplicate"); if (dup) dup.textContent = n > 1 ? `复制 ${n} 项备份` : "复制备份"; const del = $("book-menu-delete"); if (del) del.textContent = n > 1 ? `删除 ${n} 项` : "删除"; const sel = $("book-menu-select"); if (sel) sel.textContent = bookSelectMode ? "取消多选" : "多选"; } function showBookMenu(bookId, x, y) { const menu = $("book-action-menu"); if (!menu) return; bookMenuBookId = bookId; refreshBookMenuLabels(); menu.hidden = false; menu.style.left = `${x}px`; menu.style.top = `${y}px`; const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) { menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`; } if (rect.bottom > window.innerHeight) { menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`; } } function getNavBook() { if (sidebarNav.level !== "book") return null; return books.find((b) => b.id === sidebarNav.bookId) ?? null; } function navigateToRoot() { sidebarNav = { level: "root" }; renderPathBar(); renderBookList(); } function navigateToBook(bookId) { sidebarNav = { level: "book", bookId }; renderPathBar(); renderBookList(); void fetchPlaySavesForBook(bookId); } async function renameBookById(bookId) { const book = books.find((b) => b.id === bookId); if (!book) return; const title = prompt("作品名称", book.title); if (title == null) return; const trimmed = title.trim(); if (!trimmed) return alert("名称不能为空"); try { const data = await api(`/api/books/${encodeURIComponent(bookId)}`, { method: "PUT", body: JSON.stringify({ title: trimmed }), }); const i = books.findIndex((b) => b.id === bookId); if (i >= 0 && data.book) books[i] = { ...books[i], ...data.book }; if (activeBookId === bookId && lastView) { renderSession({ ...lastView, bookTitle: trimmed }, false); } else { renderBookList(); } } catch (err) { alert(err.message); } } async function duplicateBookById(bookId, title) { try { const data = await api(`/api/books/${encodeURIComponent(bookId)}/duplicate`, { method: "POST", body: JSON.stringify(title ? { title } : {}), }); if (data.book) { books.unshift({ id: data.book.id, title: data.book.title, activeSkillId: data.book.activeSkillId ?? data.book.orchestratorId, activeSkillName: data.book.activeSkillName ?? data.book.orchestratorName, preview: data.book.preview, updatedAt: data.book.updatedAt, orchestratorId: data.book.orchestratorId, orchestratorName: data.book.orchestratorName, }); renderBookList(); } return data.book; } catch (err) { alert(err.message); return null; } } async function duplicateSelectedBooks() { const ids = bookSelectMode || selectedBookIds.size ? [...selectedBookIds] : []; if (!ids.length) return; for (const id of ids) { await duplicateBookById(id); } setBookSelectMode(false); } async function deleteSelectedBooks() { const ids = [...selectedBookIds]; if (!ids.length) return; const label = ids.length === 1 ? `「${books.find((b) => b.id === ids[0])?.title ?? "该作品"}」` : `${ids.length} 个作品`; if (!confirm(`删除 ${label}?不可恢复。`)) return; try { const data = await api("/api/books/batch", { method: "DELETE", body: JSON.stringify({ ids }), }); const deleted = new Set(data.deleted ?? ids); books = books.filter((b) => !deleted.has(b.id)); setBookSelectMode(false); if (activeBookId && deleted.has(activeBookId)) { if (books.length) await openBook(books[0].id); else renderEmpty(); } else { renderBookList(); } } catch (err) { alert(err.message); } } function rectsIntersect(a, b) { return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; } function setupBookBoxSelect() { const list = $("book-list"); const overlay = $("book-box-overlay"); if (!list || !overlay) return; let dragging = false; let start = null; const clearDrag = () => { dragging = false; start = null; overlay.hidden = true; }; list.addEventListener("mousedown", (e) => { if (sidebarNav.level !== "root" || e.button !== 0) return; if (e.target.closest("#book-action-menu")) return; dragging = true; start = { x: e.clientX, y: e.clientY }; overlay.hidden = true; const onMove = (ev) => { if (!dragging || !start) return; const dx = Math.abs(ev.clientX - start.x); const dy = Math.abs(ev.clientY - start.y); if (dx < 6 && dy < 6) return; if (!bookSelectMode) setBookSelectMode(true, { keepSelection: true }); const left = Math.min(start.x, ev.clientX); const top = Math.min(start.y, ev.clientY); const width = Math.abs(ev.clientX - start.x); const height = Math.abs(ev.clientY - start.y); overlay.hidden = false; overlay.style.left = `${left}px`; overlay.style.top = `${top}px`; overlay.style.width = `${width}px`; overlay.style.height = `${height}px`; }; const onUp = (ev) => { if (!dragging || !start) return; const dx = Math.abs(ev.clientX - start.x); const dy = Math.abs(ev.clientY - start.y); if (dx >= 6 || dy >= 6) { const box = { left: Math.min(start.x, ev.clientX), right: Math.max(start.x, ev.clientX), top: Math.min(start.y, ev.clientY), bottom: Math.max(start.y, ev.clientY), }; for (const row of list.querySelectorAll("[data-book-id]")) { const rect = row.getBoundingClientRect(); if (rectsIntersect(box, rect)) selectedBookIds.add(row.dataset.bookId); } renderBookList(); } document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); clearDrag(); }; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); }); } function renderPathBar() { const bar = $("book-path-bar"); if (!bar) return; bar.innerHTML = ""; bar.hidden = !books.length; const rootBtn = document.createElement("button"); rootBtn.type = "button"; rootBtn.className = "path-seg"; rootBtn.dataset.nav = "root"; rootBtn.innerHTML = `作品`; if (sidebarNav.level === "root") rootBtn.classList.add("current"); rootBtn.addEventListener("click", () => { if (sidebarNav.level !== "root") navigateToRoot(); }); bar.appendChild(rootBtn); if (sidebarNav.level === "book") { const book = getNavBook(); if (!book) return; const sep = document.createElement("span"); sep.className = "path-sep"; sep.setAttribute("aria-hidden", "true"); sep.textContent = "›"; bar.appendChild(sep); const bookSeg = document.createElement("span"); bookSeg.className = "path-seg current"; bookSeg.textContent = book.title; bar.appendChild(bookSeg); } } function renderExplorerRow({ name, meta, active, selected, bookId, actionClass, onClick, onDelete }) { const row = document.createElement("button"); row.type = "button"; row.className = ["explorer-row", actionClass, active ? "active" : "", selected ? "selected" : ""] .filter(Boolean) .join(" "); if (bookId) row.dataset.bookId = bookId; row.innerHTML = ` ${esc(name)} ${meta ? `` : ""} ${onDelete ? `×` : ""}`; row.addEventListener("click", (e) => { if (e.target.closest(".explorer-del")) return; onClick?.(e); }); row.querySelector(".explorer-del")?.addEventListener("click", (e) => { e.stopPropagation(); onDelete?.(); }); if (bookId) { row.addEventListener("contextmenu", (e) => { e.preventDefault(); if (bookSelectMode && !selectedBookIds.has(bookId)) { selectedBookIds.add(bookId); renderBookList(); } showBookMenu(bookId, e.clientX, e.clientY); }); } return row; } function renderBookContents(book, list) { const loading = playSavesLoading.has(book.id); const saves = playSavesByBook.get(book.id); const ready = bookPlayReady(book.id); list.appendChild( renderExplorerRow({ name: "继续创作", meta: "自动保存", active: activeBookId === book.id && lastView?.lifecycleStage !== "play", onClick: () => { void openBook(book.id).then(() => navigateToRoot()); }, }), ); if (loading && saves === undefined) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "加载存档…"; list.appendChild(hint); return; } const saveList = saves ?? []; if (!saveList.length && !ready) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "验收 Worker 集后可保存定稿 / 游玩存档"; list.appendChild(hint); return; } if (saveList.length) { const label = document.createElement("div"); label.className = "explorer-section-label"; label.textContent = "存档"; list.appendChild(label); } for (const s of saveList) { const kindLabel = s.kindLabel || (s.kind === "instance" ? "创作定稿" : s.kind === "opening" ? "开局" : "游玩进度"); list.appendChild( renderExplorerRow({ name: s.label, meta: `${kindLabel} · ${new Date(s.createdAt).toLocaleString("zh-CN")}`, onClick: () => void loadPlaySave(book.id, s.id), onDelete: () => void deletePlaySave(book.id, s.id, s.label), actionClass: "explorer-file", }), ); } if (ready) { list.appendChild( renderExplorerRow({ name: "+ 新建游玩", actionClass: "explorer-action", onClick: () => void startNewPlayForBook(book.id), }), ); } else if (!saveList.length) { const hint = document.createElement("p"); hint.className = "explorer-hint"; hint.textContent = "暂无游玩存档"; list.appendChild(hint); } } function renderBookList() { const list = $("book-list"); if (!list) return; renderPathBar(); if (!books.length) { list.innerHTML = ``; $("btn-new-inline")?.addEventListener("click", openNewBookDialog); return; } list.innerHTML = ""; if (sidebarNav.level === "book") { const book = getNavBook(); if (!book) { navigateToRoot(); return; } renderBookContents(book, list); return; } for (const book of books) { const skill = book.activeSkillName ?? book.activeSkillId ?? book.orchestratorName ?? "实例设计"; const selected = selectedBookIds.has(book.id); list.appendChild( renderExplorerRow({ name: book.title, meta: skill, active: book.id === activeBookId && !selected, selected, bookId: book.id, actionClass: "explorer-folder", onClick: () => { if (bookSelectMode) { toggleBookSelection(book.id); return; } navigateToBook(book.id); void openBook(book.id); }, }), ); } } async function loadPlaySave(bookId, saveId) { try { if (lastView) renderSession(lastView, true); const data = await api( `/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`, { method: "POST" }, ); sidebarNav = { level: "root" }; renderSession(data.session, false); } catch (err) { if (lastView) renderSession(lastView, false); alert(err.message); } } async function deletePlaySave(bookId, saveId, label) { if (!confirm(`删除游玩存档「${label}」?`)) return; try { await api( `/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}`, { method: "DELETE" }, ); await fetchPlaySavesForBook(bookId, true); } catch (err) { alert(err.message); } } async function startNewPlayForBook(bookId) { if (!bookPlayReady(bookId)) { alert("须先验收 Worker 集"); return; } try { if (lastView) renderSession(lastView, true); const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, { method: "POST" }); sidebarNav = { level: "root" }; renderSession(data.session, false); } catch (err) { if (lastView) renderSession(lastView, false); alert(err.message); } } function renderHeader(view, loading) { const st = statusFor(view, loading); $("work-title").textContent = view.bookTitle ?? "未命名作品"; const skill = displaySkillPackLabel(view.activeSkill) || view.selectedRecipe?.name || "导演"; 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; const showSavePlay = Boolean(view.bookId && view.playReady && view.lifecycleStage === "play"); const showSaveInstance = Boolean(view.bookId && view.playReady); $("btn-save-play").hidden = !showSavePlay; const btnInst = $("btn-save-instance"); if (btnInst) btnInst.hidden = !showSaveInstance; $("btn-export").disabled = !(view.messages?.length); } function renderEmpty() { sessionId = null; lastView = null; activeBookId = null; sidebarNav = { level: "root" }; setBookSelectMode(false); composerForceInput = false; $("work-title").textContent = "未打开作品"; $("work-meta").textContent = ""; $("status-dot").className = "status-dot"; $("status-text").textContent = "—"; $("btn-save-play").hidden = true; const btnInst = $("btn-save-instance"); if (btnInst) btnInst.hidden = true; $("btn-export").disabled = true; $("message-feed").innerHTML = `点击左侧 + 新建作品
`; $("skill-picker").hidden = true; $("agent-focus").innerHTML = ""; $("agent-timeline").innerHTML = ""; $("skill-guide-list").innerHTML = ""; const wsp = $("worker-set-user-panel"); if (wsp) wsp.innerHTML = ""; $("tool-trace").hidden = true; hideBookMenu(); document.body.dataset.lifecycle = "design"; $("composer").innerHTML = `