Play rounds end in present.v1 body instead of creation HITL.
Narrator uses capability-probed structured delivery and falls back to shell body on failure; play UI hides worker questions and keeps thinking on process rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+47
-31
@@ -647,7 +647,19 @@ function formatPlayPresentHtml(body, view) {
|
||||
if (presentView) {
|
||||
return renderPresentShellHtml(presentView.packet, esc, { tweaks });
|
||||
}
|
||||
return renderPresentShellHtml(presentFromPlain(trimmed, fallbackShell), esc, {
|
||||
const scene =
|
||||
parsed && typeof parsed === "object"
|
||||
? typeof parsed.正文 === "string"
|
||||
? parsed.正文
|
||||
: typeof parsed.body === "string"
|
||||
? parsed.body
|
||||
: typeof parsed.brief === "string"
|
||||
? parsed.brief
|
||||
: ""
|
||||
: "";
|
||||
const plain = (scene || (!parsed ? trimmed : "")).trim();
|
||||
if (!plain) return `<p class="empty-sm">(无正文)</p>`;
|
||||
return renderPresentShellHtml(presentFromPlain(plain, fallbackShell), esc, {
|
||||
tweaks,
|
||||
});
|
||||
}
|
||||
@@ -702,6 +714,16 @@ function wireCoordRailChrome() {
|
||||
}
|
||||
}
|
||||
|
||||
function isWorkspaceNav() {
|
||||
const level = document.body.dataset.navLevel;
|
||||
return level === "book" || level === "product";
|
||||
}
|
||||
|
||||
function syncExpandLabel() {
|
||||
const label = document.getElementById("rail-expand-label");
|
||||
if (label) label.textContent = activeRailTab === "log" ? "志" : "书";
|
||||
}
|
||||
|
||||
export function setRailTab(tab, { expand = false } = {}) {
|
||||
activeRailTab = tab === "log" ? "log" : "books";
|
||||
if (expand) railUserExpanded = true;
|
||||
@@ -717,6 +739,7 @@ export function setRailTab(tab, { expand = false } = {}) {
|
||||
panel.classList.toggle("active", active);
|
||||
panel.toggleAttribute("hidden", !active);
|
||||
});
|
||||
syncExpandLabel();
|
||||
syncRailCollapse();
|
||||
}
|
||||
|
||||
@@ -728,25 +751,23 @@ function wireRailChrome() {
|
||||
const btn = e.target.closest(".rail-tab");
|
||||
if (!btn) return;
|
||||
const tab = btn.getAttribute("data-rail-tab") ?? "books";
|
||||
const collapsed = document.body.classList.contains("rail-collapsed");
|
||||
setRailTab(tab, { expand: collapsed });
|
||||
setRailTab(tab);
|
||||
});
|
||||
}
|
||||
const toggle = document.getElementById("btn-rail-toggle");
|
||||
if (toggle && !toggle.dataset.wired) {
|
||||
toggle.dataset.wired = "1";
|
||||
toggle.addEventListener("click", () => {
|
||||
const inBook = document.body.dataset.navLevel === "book";
|
||||
const collapsed = document.body.classList.contains("rail-collapsed");
|
||||
if (inBook && !collapsed) {
|
||||
window.dispatchEvent(new CustomEvent("wa:nav-root"));
|
||||
return;
|
||||
}
|
||||
if (document.body.dataset.railCanCollapse !== "1") {
|
||||
railUserExpanded = true;
|
||||
} else {
|
||||
railUserExpanded = !railUserExpanded;
|
||||
}
|
||||
const expand = document.getElementById("btn-rail-expand");
|
||||
if (expand && !expand.dataset.wired) {
|
||||
expand.dataset.wired = "1";
|
||||
expand.addEventListener("click", () => {
|
||||
railUserExpanded = true;
|
||||
syncRailCollapse();
|
||||
});
|
||||
}
|
||||
const collapse = document.getElementById("btn-rail-collapse");
|
||||
if (collapse && !collapse.dataset.wired) {
|
||||
collapse.dataset.wired = "1";
|
||||
collapse.addEventListener("click", () => {
|
||||
if (document.body.dataset.railCanCollapse !== "1") return;
|
||||
railUserExpanded = false;
|
||||
syncRailCollapse();
|
||||
});
|
||||
}
|
||||
@@ -762,7 +783,7 @@ function hasStartedCreation(view) {
|
||||
}
|
||||
|
||||
function syncRailCollapse(view) {
|
||||
const inBook = document.body.dataset.navLevel === "book";
|
||||
const inBook = isWorkspaceNav();
|
||||
if (!inBook) {
|
||||
document.body.dataset.railCanCollapse = "0";
|
||||
railUserExpanded = true;
|
||||
@@ -772,18 +793,12 @@ function syncRailCollapse(view) {
|
||||
const canCollapse = document.body.dataset.railCanCollapse === "1";
|
||||
const collapsed = canCollapse && !railUserExpanded;
|
||||
document.body.classList.toggle("rail-collapsed", collapsed);
|
||||
const toggle = document.getElementById("btn-rail-toggle");
|
||||
if (toggle) {
|
||||
if (inBook && !collapsed) {
|
||||
toggle.title = "返回作品目录";
|
||||
toggle.setAttribute("aria-label", "返回作品目录");
|
||||
toggle.textContent = "‹";
|
||||
} else {
|
||||
toggle.title = collapsed ? "展开侧栏" : "收起侧栏";
|
||||
toggle.setAttribute("aria-label", collapsed ? "展开侧栏" : "收起侧栏");
|
||||
toggle.textContent = collapsed ? "›" : "‹";
|
||||
}
|
||||
const expand = document.getElementById("btn-rail-expand");
|
||||
if (expand) {
|
||||
expand.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
expand.title = collapsed ? "展开侧栏" : "收起侧栏";
|
||||
}
|
||||
syncExpandLabel();
|
||||
}
|
||||
|
||||
export function resetRailChrome() {
|
||||
@@ -831,6 +846,7 @@ function shouldShowInFeed(msg, view) {
|
||||
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
||||
if (msg.compressed) return false;
|
||||
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
|
||||
if (isPlayView(view) && kind === "worker_questions") return false;
|
||||
if (isSkillSelectionMessage(msg)) return false;
|
||||
if (isReviewSidecarQuestionStub(msg)) return false;
|
||||
if (
|
||||
@@ -852,7 +868,7 @@ function shouldShowInFeed(msg, view) {
|
||||
}
|
||||
|
||||
function maybeSyncRail(view) {
|
||||
if (document.body.dataset.navLevel !== "book") {
|
||||
if (!isWorkspaceNav()) {
|
||||
railUserExpanded = true;
|
||||
document.body.dataset.railCanCollapse = "0";
|
||||
syncRailCollapse(view);
|
||||
|
||||
+237
-129
@@ -2,6 +2,7 @@ import {
|
||||
renderWorkspace,
|
||||
updateLiveStreamPanel,
|
||||
resetRailChrome,
|
||||
setRailTab,
|
||||
fillStepArtifactDialog,
|
||||
canOfferSaveProduct,
|
||||
canOfferEnterPlay,
|
||||
@@ -32,7 +33,6 @@ let selectedBookIds = new Set();
|
||||
let activePlaySaveId = null;
|
||||
const playSavesByBook = new Map();
|
||||
const playWorkingInstanceByBook = new Map();
|
||||
const expandedProductsByBook = new Map();
|
||||
const playSavesLoading = new Set();
|
||||
let bookMenuBookId = null;
|
||||
let livePollTimer = null;
|
||||
@@ -99,7 +99,7 @@ function resolveUserTask(view, loading) {
|
||||
const hasQuestions = Boolean(getActiveQuestions(view)?.questions?.length);
|
||||
const moduleOpening = isModuleOpeningWaiting(view);
|
||||
|
||||
if (isPlayView(view) && wr?.kind !== "worker_questions") {
|
||||
if (isPlayView(view)) {
|
||||
return {
|
||||
id: "speak",
|
||||
label: USER_TASK.speak.label,
|
||||
@@ -1019,20 +1019,6 @@ function runsForProduct(bookId, instanceId, products) {
|
||||
);
|
||||
}
|
||||
|
||||
function isProductExpanded(bookId, instanceId, fallback) {
|
||||
const set = expandedProductsByBook.get(bookId);
|
||||
if (set) return set.has(instanceId);
|
||||
return Boolean(fallback);
|
||||
}
|
||||
|
||||
function toggleProductExpanded(bookId, instanceId) {
|
||||
const open = isProductExpanded(bookId, instanceId, false);
|
||||
const next = new Set();
|
||||
if (!open) next.add(instanceId);
|
||||
expandedProductsByBook.set(bookId, next);
|
||||
renderBookList();
|
||||
}
|
||||
|
||||
function bookPlayReady(bookId) {
|
||||
if (lastView?.bookId === bookId && Boolean(lastView?.playReady || lastView?.hasProduct)) {
|
||||
return true;
|
||||
@@ -1070,7 +1056,7 @@ async function fetchPlaySavesForBook(bookId, force = false) {
|
||||
}
|
||||
|
||||
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
||||
if (on && sidebarNav.level === "book") return;
|
||||
if (on && sidebarNav.level !== "root") return;
|
||||
bookSelectMode = on;
|
||||
if (!on) selectedBookIds = new Set();
|
||||
else if (!keepSelection) selectedBookIds = new Set();
|
||||
@@ -1134,26 +1120,93 @@ function showBookMenu(bookId, x, y) {
|
||||
}
|
||||
|
||||
function isCatalogNav() {
|
||||
return sidebarNav.level !== "book";
|
||||
return sidebarNav.level === "root";
|
||||
}
|
||||
|
||||
function isWorkspaceNav() {
|
||||
return sidebarNav.level === "book" || sidebarNav.level === "product";
|
||||
}
|
||||
|
||||
function getNavBook() {
|
||||
if (sidebarNav.level !== "book") return null;
|
||||
if (!isWorkspaceNav() || !sidebarNav.bookId) return null;
|
||||
return books.find((b) => b.id === sidebarNav.bookId) ?? null;
|
||||
}
|
||||
|
||||
function syncRailContext() {
|
||||
const el = $("rail-context");
|
||||
if (!el) return;
|
||||
function getNavProduct() {
|
||||
if (sidebarNav.level !== "product" || !sidebarNav.productId || !sidebarNav.bookId) return null;
|
||||
return productInstancesForBook(sidebarNav.bookId).find((p) => p.id === sidebarNav.productId) ?? null;
|
||||
}
|
||||
|
||||
function pathSep() {
|
||||
const li = document.createElement("li");
|
||||
li.className = "path-bar-item";
|
||||
li.setAttribute("aria-hidden", "true");
|
||||
const sep = document.createElement("span");
|
||||
sep.className = "path-sep";
|
||||
sep.textContent = "›";
|
||||
li.appendChild(sep);
|
||||
return li;
|
||||
}
|
||||
|
||||
function pathItem({ label, title, current, onClick }) {
|
||||
const li = document.createElement("li");
|
||||
li.className = current ? "path-bar-item path-bar-item-current" : "path-bar-item";
|
||||
const el = document.createElement(current || !onClick ? "span" : "button");
|
||||
el.className = current ? "path-seg current" : "path-seg";
|
||||
if (el.tagName === "BUTTON") el.type = "button";
|
||||
if (current) el.setAttribute("aria-current", "page");
|
||||
el.textContent = label;
|
||||
if (title) el.title = title;
|
||||
if (!current && onClick) el.addEventListener("click", onClick);
|
||||
li.appendChild(el);
|
||||
return li;
|
||||
}
|
||||
|
||||
function renderPathBar() {
|
||||
const bar = $("book-path-bar");
|
||||
if (!bar) return;
|
||||
bar.innerHTML = "";
|
||||
const book = getNavBook();
|
||||
if (sidebarNav.level === "book" && book) {
|
||||
el.textContent = book.title;
|
||||
el.title = book.title;
|
||||
} else {
|
||||
el.textContent = "作品";
|
||||
el.removeAttribute("title");
|
||||
const product = getNavProduct();
|
||||
const atProduct = sidebarNav.level === "product" && Boolean(book);
|
||||
const inBook = Boolean(book) && (sidebarNav.level === "book" || atProduct);
|
||||
|
||||
$("rail-tabs")?.toggleAttribute("hidden", !isWorkspaceNav());
|
||||
|
||||
const list = document.createElement("ol");
|
||||
list.className = "path-bar-list";
|
||||
list.appendChild(
|
||||
pathItem({
|
||||
label: "目录",
|
||||
title: "作品目录",
|
||||
current: !inBook,
|
||||
onClick: () => showCatalog(),
|
||||
}),
|
||||
);
|
||||
if (inBook) {
|
||||
const bookLabel = book.title?.trim() || "书本";
|
||||
list.appendChild(pathSep());
|
||||
list.appendChild(
|
||||
pathItem({
|
||||
label: bookLabel,
|
||||
title: bookLabel,
|
||||
current: !atProduct,
|
||||
onClick: () => navigateToBook(book.id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
$("rail-tabs")?.toggleAttribute("hidden", sidebarNav.level !== "book");
|
||||
if (atProduct) {
|
||||
const productLabel = product?.label?.trim() || "定稿";
|
||||
list.appendChild(pathSep());
|
||||
list.appendChild(
|
||||
pathItem({
|
||||
label: productLabel,
|
||||
title: productLabel,
|
||||
current: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
bar.appendChild(list);
|
||||
}
|
||||
|
||||
function navigateToBook(bookId) {
|
||||
@@ -1162,7 +1215,20 @@ function navigateToBook(bookId) {
|
||||
document.body.dataset.navLevel = "book";
|
||||
bookSelectMode = false;
|
||||
selectedBookIds = new Set();
|
||||
syncRailContext();
|
||||
setRailTab("books", { expand: true });
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
void fetchPlaySavesForBook(bookId);
|
||||
}
|
||||
|
||||
function navigateToProduct(bookId, productId) {
|
||||
if (!bookId || !productId) return;
|
||||
sidebarNav = { level: "product", bookId, productId };
|
||||
document.body.dataset.navLevel = "product";
|
||||
bookSelectMode = false;
|
||||
selectedBookIds = new Set();
|
||||
setRailTab("books", { expand: true });
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
void fetchPlaySavesForBook(bookId);
|
||||
}
|
||||
@@ -1175,7 +1241,7 @@ function showCatalog() {
|
||||
bookSelectMode = false;
|
||||
selectedBookIds = new Set();
|
||||
closeWorkspace();
|
||||
syncRailContext();
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
}
|
||||
|
||||
@@ -1248,17 +1314,15 @@ function renderExplorerRow({
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderBookBranch(book, list) {
|
||||
function playingInstanceIdForBook(book) {
|
||||
const playHere = book.id === activeBookId && isPlayNow();
|
||||
if (playHere && lastView?.playInstanceId) return lastView.playInstanceId;
|
||||
return playWorkingInstanceByBook.get(book.id) ?? null;
|
||||
}
|
||||
|
||||
function renderProductContents(book, product, list) {
|
||||
const loading = playSavesLoading.has(book.id);
|
||||
const saves = playSavesByBook.get(book.id);
|
||||
const products = productInstancesForBook(book.id);
|
||||
const playHere = book.id === activeBookId && isPlayNow();
|
||||
const playingInstanceId =
|
||||
playHere && lastView?.playInstanceId
|
||||
? lastView.playInstanceId
|
||||
: playWorkingInstanceByBook.get(book.id);
|
||||
const latestId = products[0]?.id;
|
||||
|
||||
if (loading && saves === undefined) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
@@ -1267,83 +1331,51 @@ function renderBookBranch(book, list) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const product of products) {
|
||||
const isLatest = product.id === latestId;
|
||||
const isPlayingHere = playHere && playingInstanceId === product.id;
|
||||
const hasWorking = playingInstanceId === product.id;
|
||||
const expanded = isProductExpanded(book.id, product.id, isPlayingHere);
|
||||
const runs = runsForProduct(book.id, product.id, products);
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = `explorer-product${expanded ? " is-open" : ""}`;
|
||||
const playHere = book.id === activeBookId && isPlayNow();
|
||||
const playingInstanceId = playingInstanceIdForBook(book);
|
||||
const hasWorking = playingInstanceId === product.id;
|
||||
const runs = runsForProduct(book.id, product.id, productInstancesForBook(book.id));
|
||||
|
||||
const header = renderExplorerRow({
|
||||
name: product.label,
|
||||
meta: isLatest ? "最新" : "",
|
||||
twistie: true,
|
||||
expanded,
|
||||
title: "点开游玩存档,双击改名",
|
||||
onTwistie: () => toggleProductExpanded(book.id, product.id),
|
||||
onClick: () => toggleProductExpanded(book.id, product.id),
|
||||
onRename: () => void renamePlaySave(book.id, product.id, product.label),
|
||||
onDelete: () => void deletePlaySave(book.id, product.id, product.label, "落档"),
|
||||
});
|
||||
header.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
wrap.appendChild(header);
|
||||
|
||||
if (expanded) {
|
||||
const drop = document.createElement("div");
|
||||
drop.className = "explorer-drop";
|
||||
drop.setAttribute("role", "group");
|
||||
drop.setAttribute("aria-label", `${product.label} 的游玩存档`);
|
||||
|
||||
if (hasWorking) {
|
||||
drop.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "当前游玩",
|
||||
meta: isPlayingHere && !activePlaySaveId ? "进行中" : "工作副本",
|
||||
active: isPlayingHere && !activePlaySaveId,
|
||||
child: true,
|
||||
onClick: () => void openCurrentPlay(book.id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!runs.length && !hasWorking) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-drop-empty";
|
||||
hint.textContent = "还没有游玩存档";
|
||||
drop.appendChild(hint);
|
||||
}
|
||||
|
||||
for (const run of runs) {
|
||||
const runWhen = formatSaveWhen(run.createdAt);
|
||||
drop.appendChild(
|
||||
renderExplorerRow({
|
||||
name: run.label,
|
||||
meta: runWhen || "游玩进度",
|
||||
active: playHere && activePlaySaveId === run.id,
|
||||
child: true,
|
||||
actionClass: "explorer-file",
|
||||
onClick: () => void loadPlaySave(book.id, run.id),
|
||||
onRename: () => void renamePlaySave(book.id, run.id, run.label),
|
||||
onDelete: () => void deletePlaySave(book.id, run.id, run.label, "游玩存档"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
drop.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "+ 新开一局",
|
||||
actionClass: "explorer-action",
|
||||
child: true,
|
||||
onClick: () => void startNewPlayForBook(book.id, product.id),
|
||||
}),
|
||||
);
|
||||
wrap.appendChild(drop);
|
||||
}
|
||||
|
||||
list.appendChild(wrap);
|
||||
if (hasWorking) {
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "当前游玩",
|
||||
meta: playHere && !activePlaySaveId ? "进行中" : "工作副本",
|
||||
active: playHere && !activePlaySaveId,
|
||||
onClick: () => void openCurrentPlay(book.id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!runs.length && !hasWorking) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = "还没有游玩存档";
|
||||
list.appendChild(hint);
|
||||
}
|
||||
|
||||
for (const run of runs) {
|
||||
const runWhen = formatSaveWhen(run.createdAt);
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: run.label,
|
||||
meta: runWhen || "游玩进度",
|
||||
active: playHere && activePlaySaveId === run.id,
|
||||
actionClass: "explorer-file",
|
||||
onClick: () => void loadPlaySave(book.id, run.id),
|
||||
onRename: () => void renamePlaySave(book.id, run.id, run.label),
|
||||
onDelete: () => void deletePlaySave(book.id, run.id, run.label, "游玩存档"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "+ 新开一局",
|
||||
actionClass: "explorer-action",
|
||||
onClick: () => void startNewPlayForBook(book.id, product.id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function renderBookContents(book, list) {
|
||||
@@ -1356,13 +1388,56 @@ function renderBookContents(book, list) {
|
||||
onClick: () => void openBookDesign(book.id),
|
||||
}),
|
||||
);
|
||||
renderBookBranch(book, list);
|
||||
|
||||
const loading = playSavesLoading.has(book.id);
|
||||
const saves = playSavesByBook.get(book.id);
|
||||
if (loading && saves === undefined) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = "加载存档…";
|
||||
list.appendChild(hint);
|
||||
return;
|
||||
}
|
||||
|
||||
const products = productInstancesForBook(book.id);
|
||||
if (!products.length) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = bookPlayReady(book.id)
|
||||
? "还没有定稿。可在更多菜单里保存定稿。"
|
||||
: "验收后可保存定稿。";
|
||||
list.appendChild(hint);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestId = products[0]?.id;
|
||||
const playingInstanceId = playingInstanceIdForBook(book);
|
||||
const section = document.createElement("p");
|
||||
section.className = "explorer-section-label";
|
||||
section.textContent = "定稿";
|
||||
list.appendChild(section);
|
||||
|
||||
for (const product of products) {
|
||||
const bits = [];
|
||||
if (product.id === latestId) bits.push("最新");
|
||||
if (playingInstanceId === product.id) bits.push("游玩中");
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: product.label,
|
||||
meta: bits.join(" · "),
|
||||
title: "打开定稿,双击改名",
|
||||
onClick: () => navigateToProduct(book.id, product.id),
|
||||
onRename: () => void renamePlaySave(book.id, product.id, product.label),
|
||||
onDelete: () => void deletePlaySave(book.id, product.id, product.label, "定稿"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBookList() {
|
||||
const list = $("book-list");
|
||||
if (!list) return;
|
||||
syncRailContext();
|
||||
renderPathBar();
|
||||
if (!books.length) {
|
||||
list.innerHTML = `<p class="sidebar-empty">暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button></p>`;
|
||||
$("btn-new-inline")?.addEventListener("click", openNewBookDialog);
|
||||
@@ -1370,6 +1445,34 @@ function renderBookList() {
|
||||
}
|
||||
list.innerHTML = "";
|
||||
|
||||
if (sidebarNav.level === "product") {
|
||||
const book = getNavBook();
|
||||
if (!book) {
|
||||
sidebarNav = { level: "root" };
|
||||
document.body.dataset.navLevel = "root";
|
||||
renderPathBar();
|
||||
} else {
|
||||
const product = getNavProduct();
|
||||
const loading = playSavesLoading.has(book.id) && !playSavesByBook.has(book.id);
|
||||
if (product) {
|
||||
renderProductContents(book, product, list);
|
||||
return;
|
||||
}
|
||||
if (loading) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = "加载存档…";
|
||||
list.appendChild(hint);
|
||||
return;
|
||||
}
|
||||
sidebarNav = { level: "book", bookId: book.id };
|
||||
document.body.dataset.navLevel = "book";
|
||||
renderPathBar();
|
||||
renderBookContents(book, list);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sidebarNav.level === "book") {
|
||||
const book = getNavBook();
|
||||
if (book) {
|
||||
@@ -1378,7 +1481,7 @@ function renderBookList() {
|
||||
}
|
||||
sidebarNav = { level: "root" };
|
||||
document.body.dataset.navLevel = "root";
|
||||
syncRailContext();
|
||||
renderPathBar();
|
||||
}
|
||||
|
||||
for (const book of books) {
|
||||
@@ -1396,7 +1499,7 @@ function renderBookList() {
|
||||
toggleBookSelection(book.id);
|
||||
return;
|
||||
}
|
||||
void openBook(book.id);
|
||||
navigateToBook(book.id);
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1561,7 +1664,11 @@ function setupBookBoxSelect() {
|
||||
|
||||
async function loadPlaySave(bookId, saveId) {
|
||||
try {
|
||||
navigateToBook(bookId);
|
||||
const products = productInstancesForBook(bookId);
|
||||
const run = playRunsForBook(bookId).find((r) => r.id === saveId);
|
||||
const instanceId = run ? resolveRunInstanceId(run, products) : sidebarNav.productId;
|
||||
if (instanceId) navigateToProduct(bookId, instanceId);
|
||||
else navigateToBook(bookId);
|
||||
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||
const data = await api(
|
||||
`/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`,
|
||||
@@ -1600,6 +1707,10 @@ async function deletePlaySave(bookId, saveId, label, kindLabel = "游玩存档")
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (activePlaySaveId === saveId) activePlaySaveId = null;
|
||||
if (sidebarNav.level === "product" && sidebarNav.productId === saveId) {
|
||||
sidebarNav = { level: "book", bookId };
|
||||
document.body.dataset.navLevel = "book";
|
||||
}
|
||||
await fetchPlaySavesForBook(bookId, true);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
@@ -1613,16 +1724,14 @@ async function startNewPlayForBook(bookId, instanceId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
navigateToBook(bookId);
|
||||
if (instanceId) navigateToProduct(bookId, instanceId);
|
||||
else navigateToBook(bookId);
|
||||
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||
const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(instanceId ? { instanceId } : {}),
|
||||
});
|
||||
activePlaySaveId = null;
|
||||
if (instanceId) {
|
||||
expandedProductsByBook.set(bookId, new Set([instanceId]));
|
||||
}
|
||||
renderSession(data.session, false);
|
||||
void fetchPlaySavesForBook(bookId, true);
|
||||
} catch (err) {
|
||||
@@ -1645,7 +1754,9 @@ async function openBookDesign(bookId) {
|
||||
}
|
||||
|
||||
async function openCurrentPlay(bookId) {
|
||||
navigateToBook(bookId);
|
||||
const instanceId = playingInstanceIdForBook({ id: bookId });
|
||||
if (instanceId) navigateToProduct(bookId, instanceId);
|
||||
else navigateToBook(bookId);
|
||||
const alreadyHere =
|
||||
activeBookId === bookId &&
|
||||
isPlayNow() &&
|
||||
@@ -2307,10 +2418,7 @@ async function saveCurrentInstance() {
|
||||
}
|
||||
await fetchPlaySavesForBook(activeBookId, true);
|
||||
const newest = productInstancesForBook(activeBookId)[0];
|
||||
if (newest) {
|
||||
expandedProductsByBook.set(activeBookId, new Set([newest.id]));
|
||||
renderBookList();
|
||||
}
|
||||
if (newest) navigateToProduct(activeBookId, newest.id);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
|
||||
+44
-28
@@ -15,36 +15,52 @@
|
||||
<body data-lifecycle="design" data-nav-level="root">
|
||||
<div class="layout">
|
||||
<aside class="panel panel-rail" id="panel-books">
|
||||
<header class="panel-head rail-head">
|
||||
<div class="rail-head-row">
|
||||
<button type="button" class="rail-toggle btn-sm" id="btn-rail-toggle" title="返回作品目录" aria-label="返回作品目录">‹</button>
|
||||
<h1 class="rail-context" id="rail-context">作品</h1>
|
||||
<div class="panel-head-actions rail-head-actions">
|
||||
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
||||
<span class="burst-badge" id="burst-badge" hidden></span>
|
||||
<button
|
||||
type="button"
|
||||
class="rail-expand"
|
||||
id="btn-rail-expand"
|
||||
aria-expanded="true"
|
||||
aria-controls="rail-body"
|
||||
title="展开侧栏"
|
||||
>
|
||||
<span class="rail-expand-label" id="rail-expand-label">书</span>
|
||||
</button>
|
||||
<div class="rail-body" id="rail-body">
|
||||
<header class="panel-head rail-head">
|
||||
<div class="rail-head-row">
|
||||
<nav class="path-bar rail-path" id="book-path-bar" aria-label="位置">
|
||||
<ol class="path-bar-list">
|
||||
<li class="path-bar-item path-bar-item-current">
|
||||
<span class="path-seg current" aria-current="page">目录</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="panel-head-actions rail-head-actions">
|
||||
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
||||
<span class="burst-badge" id="burst-badge" hidden></span>
|
||||
<button type="button" class="btn-sm rail-collapse" id="btn-rail-collapse" title="收起侧栏" aria-label="收起侧栏">‹</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="本书侧栏" hidden>
|
||||
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="本书">
|
||||
<span class="rail-tab-icon" aria-hidden="true">书</span>
|
||||
<span class="rail-tab-label">本书</span>
|
||||
</button>
|
||||
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
||||
<span class="rail-tab-icon" aria-hidden="true">志</span>
|
||||
<span class="rail-tab-label">调度</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="本书侧栏" hidden>
|
||||
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="本书">
|
||||
<span class="rail-tab-label">本书</span>
|
||||
</button>
|
||||
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
||||
<span class="rail-tab-label">调度</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="rail-panels">
|
||||
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
|
||||
<div class="book-list" id="book-list"></div>
|
||||
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="rail-tab-panel" id="rail-panel-log" role="tabpanel" data-rail-panel="log" hidden>
|
||||
<div id="agent-focus" class="agent-focus"></div>
|
||||
<div id="tool-trace" class="tool-trace" hidden></div>
|
||||
<div id="agent-timeline" class="agent-timeline"></div>
|
||||
<div class="rail-panels">
|
||||
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
|
||||
<div class="book-list" id="book-list"></div>
|
||||
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="rail-tab-panel" id="rail-panel-log" role="tabpanel" data-rail-panel="log" hidden>
|
||||
<div id="agent-focus" class="agent-focus"></div>
|
||||
<div id="tool-trace" class="tool-trace" hidden></div>
|
||||
<div id="agent-timeline" class="agent-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -81,6 +81,7 @@ function normalizeQuestions(raw) {
|
||||
|
||||
/** @returns {{ questions: any[], workerId?: string, pageSize: number, assessment?: string, optional?: boolean, skipLabel?: string } | null} */
|
||||
export function getActiveQuestions(view) {
|
||||
if (view?.playLayerActive || view?.lifecycleStage === "play") return null;
|
||||
const wr = view?.waitingReason;
|
||||
if (!wr) return null;
|
||||
// 默认每页 1 题,左右切换;勿一次堆多题
|
||||
|
||||
+68
-73
@@ -12,7 +12,7 @@
|
||||
--warn: #ca8a04;
|
||||
--danger: #dc2626;
|
||||
--books-w: 216px;
|
||||
--books-w-collapsed: 52px;
|
||||
--books-w-collapsed: 40px;
|
||||
--coord-w: clamp(280px, 24vw, 340px);
|
||||
--coord-w-collapsed: 40px;
|
||||
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
||||
@@ -82,46 +82,86 @@ a.link:hover { text-decoration: underline; }
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
padding: 6px 8px 6px 6px;
|
||||
padding: 6px 8px 6px 10px;
|
||||
}
|
||||
body[data-nav-level="root"] .rail-head-row {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.rail-context {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rail-toggle {
|
||||
.rail-collapse {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
||||
body[data-nav-level="root"] .rail-toggle { display: none; }
|
||||
body[data-nav-level="book"] .rail-head-actions #btn-new-book,
|
||||
body[data-nav-level="root"] .burst-badge { display: none; }
|
||||
body[data-rail-can-collapse="1"]:not(.rail-collapsed) .rail-collapse {
|
||||
display: inline-flex;
|
||||
}
|
||||
.rail-expand {
|
||||
display: none;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-right: 1px solid transparent;
|
||||
background: var(--surface);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 12px 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.rail-expand:hover {
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
.rail-expand:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.rail-expand-label {
|
||||
writing-mode: vertical-rl;
|
||||
letter-spacing: 0.12em;
|
||||
user-select: none;
|
||||
}
|
||||
.rail-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
body.rail-collapsed .rail-expand { display: flex; }
|
||||
body.rail-collapsed .rail-body { display: none; }
|
||||
.rail-path {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0 4px 0 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rail-path .path-bar-list {
|
||||
width: 100%;
|
||||
}
|
||||
.rail-tabs {
|
||||
display: none;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
body[data-nav-level="book"] .rail-tabs {
|
||||
body:not([data-nav-level="root"]) .rail-tabs {
|
||||
display: flex;
|
||||
}
|
||||
body:not([data-nav-level="root"]) .rail-head-actions #btn-new-book,
|
||||
body[data-nav-level="root"] .burst-badge { display: none; }
|
||||
.rail-tabs[hidden] { display: none !important; }
|
||||
.rail-tab {
|
||||
flex: 1 1 0;
|
||||
@@ -213,40 +253,6 @@ body[data-nav-level="book"] .rail-tabs {
|
||||
}
|
||||
.rail-tab-panel[hidden] { display: none !important; }
|
||||
|
||||
body.rail-collapsed .rail-tab-label,
|
||||
body.rail-collapsed .rail-context,
|
||||
body.rail-collapsed .rail-head-actions,
|
||||
body.rail-collapsed .burst-badge { display: none !important; }
|
||||
body.rail-collapsed .rail-tab-icon { display: block; }
|
||||
body.rail-collapsed .rail-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 6px 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
body.rail-collapsed .rail-head-row {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
body.rail-collapsed[data-nav-level="book"] .rail-tabs:not([hidden]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
body.rail-collapsed .rail-tab {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
body.rail-collapsed .rail-tab.active {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
body.rail-collapsed .rail-panels { display: none; }
|
||||
body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
||||
.panel-main {
|
||||
@@ -1520,24 +1526,18 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
.panel-head-actions { display: flex; gap: 4px; align-items: center; }
|
||||
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* Books — pill breadcrumb + list */
|
||||
/* Books — breadcrumb + list */
|
||||
.path-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
margin: 8px 8px 0;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
background: var(--bg);
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.path-bar[hidden] { display: none; }
|
||||
.path-bar-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1579,6 +1579,9 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.rail-path .path-seg.current {
|
||||
font-size: 13px;
|
||||
}
|
||||
.path-seg:hover { color: var(--accent); }
|
||||
.path-seg:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
@@ -1737,7 +1740,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
color: var(--muted); letter-spacing: 0.04em;
|
||||
}
|
||||
.explorer-hint {
|
||||
margin: 4px 8px 4px 28px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
||||
margin: 4px 8px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
||||
}
|
||||
.explorer-product {
|
||||
margin: 2px 0 6px;
|
||||
@@ -4750,14 +4753,6 @@ body.design-workspace .af-mosaic {
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
||||
.dialog-head-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.layout { grid-template-columns: var(--books-w-collapsed) 1fr; }
|
||||
body:not(.rail-collapsed) { --books-w: var(--books-w-collapsed); }
|
||||
body:not(.rail-collapsed) .rail-tab-label,
|
||||
body:not(.rail-collapsed) .rail-context { display: none !important; }
|
||||
body:not(.rail-collapsed) .rail-panels { display: none; }
|
||||
}
|
||||
|
||||
/* —— 游玩呈现壳 present.v1 ——
|
||||
* 主工作面 = body(随 500~2000 字撑高)。
|
||||
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
||||
|
||||
Reference in New Issue
Block a user