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:
@@ -130,7 +130,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
readPlayTurnQueue,
|
readPlayTurnQueue,
|
||||||
withPlayTurnQueue,
|
withPlayTurnQueue,
|
||||||
|
isPlayLayerActive,
|
||||||
} from "../skills/play-turn.js";
|
} from "../skills/play-turn.js";
|
||||||
|
import {
|
||||||
|
PRESENT_TAG,
|
||||||
|
isPlayPresentWorker,
|
||||||
|
parseShellAdaptationFromReplyFormat,
|
||||||
|
presentOutputFromFallback,
|
||||||
|
} from "../skills/present-packet.js";
|
||||||
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||||||
import {
|
import {
|
||||||
executeChance,
|
executeChance,
|
||||||
@@ -1001,6 +1008,37 @@ export class PhaseRuntime {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游玩层不走创作 HITL:转述必须交出用户可见正文,其它执行单元空产物也继续。
|
||||||
|
*/
|
||||||
|
private settlePlayWorkerResult(
|
||||||
|
workerId: string,
|
||||||
|
result: import("../worker/executor.js").WorkerRunResult,
|
||||||
|
): import("../worker/executor.js").WorkerRunResult {
|
||||||
|
if (!isPlayLayerActive(this.session.slots)) return result;
|
||||||
|
const outputs = { ...result.outputs };
|
||||||
|
if (Object.keys(outputs).length === 0 && isPlayPresentWorker(workerId)) {
|
||||||
|
const shell =
|
||||||
|
parseShellAdaptationFromReplyFormat(
|
||||||
|
this.blackboard.getContentByTag("设计.正文组成") ??
|
||||||
|
this.blackboard.getContentByTag("设计.回复格式"),
|
||||||
|
)?.shell_id ?? "prose";
|
||||||
|
const preview = result.preview?.trim() ?? "";
|
||||||
|
const looksAsk =
|
||||||
|
/可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview);
|
||||||
|
outputs[PRESENT_TAG] = presentOutputFromFallback(
|
||||||
|
looksAsk ? "" : preview,
|
||||||
|
shell,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
outputs,
|
||||||
|
askUser: undefined,
|
||||||
|
askAssessment: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** worker 调用 ask_user 能力时,由外层触发此事件(仅无产物时阻塞) */
|
/** worker 调用 ask_user 能力时,由外层触发此事件(仅无产物时阻塞) */
|
||||||
async workerAsk(
|
async workerAsk(
|
||||||
questions: import("../types/questions.js").QuestionItem[] | string[],
|
questions: import("../types/questions.js").QuestionItem[] | string[],
|
||||||
@@ -1802,20 +1840,22 @@ export class PhaseRuntime {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.askUser?.length && Object.keys(result.outputs).length === 0) {
|
const settled = this.settlePlayWorkerResult(workerId, result);
|
||||||
|
|
||||||
|
if (settled.askUser?.length && Object.keys(settled.outputs).length === 0) {
|
||||||
// 无产物:阻塞追问(信息不足,必须补)
|
// 无产物:阻塞追问(信息不足,必须补)
|
||||||
if (await this.restoreRevisionTargetForReview(workerId, result.askUser)) {
|
if (await this.restoreRevisionTargetForReview(workerId, settled.askUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.workerAsk(result.askUser);
|
await this.workerAsk(settled.askUser);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
workerId === "design-step" &&
|
workerId === "design-step" &&
|
||||||
Object.keys(result.outputs).length === 0
|
Object.keys(settled.outputs).length === 0
|
||||||
) {
|
) {
|
||||||
const retryQuestions = result.askUser?.length
|
const retryQuestions = settled.askUser?.length
|
||||||
? result.askUser
|
? settled.askUser
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "design-step-retry",
|
id: "design-step-retry",
|
||||||
@@ -1834,7 +1874,7 @@ export class PhaseRuntime {
|
|||||||
|
|
||||||
slots = { ...this.session.slots };
|
slots = { ...this.session.slots };
|
||||||
const writtenTags: string[] = [];
|
const writtenTags: string[] = [];
|
||||||
for (const [tag, content] of Object.entries(result.outputs)) {
|
for (const [tag, content] of Object.entries(settled.outputs)) {
|
||||||
if (isProgressPointerTag(tag)) continue;
|
if (isProgressPointerTag(tag)) continue;
|
||||||
const written = this.writeWorkerTagContent(tag, content, workerId);
|
const written = this.writeWorkerTagContent(tag, content, workerId);
|
||||||
slots[tag] = written;
|
slots[tag] = written;
|
||||||
@@ -1848,16 +1888,16 @@ export class PhaseRuntime {
|
|||||||
outputTags: (writtenTags.length > 0 ? writtenTags : worker.outputTags).filter(
|
outputTags: (writtenTags.length > 0 ? writtenTags : worker.outputTags).filter(
|
||||||
(t) => !isProgressPointerTag(t),
|
(t) => !isProgressPointerTag(t),
|
||||||
),
|
),
|
||||||
summary: result.summary,
|
summary: settled.summary,
|
||||||
});
|
});
|
||||||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||||||
|
|
||||||
const hasPlayVisible = Object.keys(result.outputs).some(
|
const hasPlayVisible = Object.keys(settled.outputs).some(
|
||||||
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
||||||
);
|
);
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
`[Worker] ${workerId} 已完成\n\n${result.preview}${
|
`[Worker] ${workerId} 已完成\n\n${settled.preview}${
|
||||||
!hasPlayVisible && result.preview.length >= 4000 ? "\n\n…" : ""
|
!hasPlayVisible && settled.preview.length >= 4000 ? "\n\n…" : ""
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1866,8 +1906,8 @@ export class PhaseRuntime {
|
|||||||
payload: {
|
payload: {
|
||||||
artifactId: artifact.id,
|
artifactId: artifact.id,
|
||||||
// 有产物时 askUser 挂到验收态,不阻断 Accept
|
// 有产物时 askUser 挂到验收态,不阻断 Accept
|
||||||
questions: this.freshSidecarQuestions(result.askUser),
|
questions: this.freshSidecarQuestions(settled.askUser),
|
||||||
assessment: result.askAssessment?.trim() || undefined,
|
assessment: settled.askAssessment?.trim() || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.maybeCompressAcceptedArtifact(artifact.id);
|
this.maybeCompressAcceptedArtifact(artifact.id);
|
||||||
|
|||||||
@@ -851,7 +851,8 @@ export class SessionManager {
|
|||||||
kind !== "approve_step" &&
|
kind !== "approve_step" &&
|
||||||
kind !== "revision" &&
|
kind !== "revision" &&
|
||||||
kind !== "next_intent" &&
|
kind !== "next_intent" &&
|
||||||
kind !== "pick_creation_step"
|
kind !== "pick_creation_step" &&
|
||||||
|
kind !== "worker_questions"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1875,12 +1876,23 @@ export class SessionManager {
|
|||||||
this.captureCheckpoint(managed),
|
this.captureCheckpoint(managed),
|
||||||
);
|
);
|
||||||
const usage = managed.trackingRef.current.pendingUsage;
|
const usage = managed.trackingRef.current.pendingUsage;
|
||||||
const thinking = managed.trackingRef.current.pendingReasoning;
|
const sessionId = managed.trackingRef.current.sessionId;
|
||||||
|
const liveThinking = sessionId
|
||||||
|
? this.workerLive.get(sessionId)?.thinking?.trim()
|
||||||
|
: "";
|
||||||
managed.trackingRef.current.pendingUsage = undefined;
|
managed.trackingRef.current.pendingUsage = undefined;
|
||||||
|
const pendingReasoning = managed.trackingRef.current.pendingReasoning;
|
||||||
managed.trackingRef.current.pendingReasoning = undefined;
|
managed.trackingRef.current.pendingReasoning = undefined;
|
||||||
|
|
||||||
const classified =
|
const classified =
|
||||||
text.trim().length > 0 ? classifyAgentMessage(text) : null;
|
text.trim().length > 0 ? classifyAgentMessage(text) : null;
|
||||||
|
const thinking =
|
||||||
|
pendingReasoning ||
|
||||||
|
(classified?.kind === "worker_output" ||
|
||||||
|
classified?.kind === "worker_questions"
|
||||||
|
? liveThinking
|
||||||
|
: "") ||
|
||||||
|
undefined;
|
||||||
const pendingTrace = managed.trackingRef.current.pendingContextTrace;
|
const pendingTrace = managed.trackingRef.current.pendingContextTrace;
|
||||||
let contextTrace: LlmContextTrace | undefined;
|
let contextTrace: LlmContextTrace | undefined;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -197,6 +197,75 @@ export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
|||||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
|
/** 游玩期把裁决写成用户可见终稿的执行单元 */
|
||||||
|
export function isPlayPresentWorker(workerId: string): boolean {
|
||||||
|
const id = workerId.trim();
|
||||||
|
return id === "narrator" || id === "round-present";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */
|
||||||
|
export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["schema", "shell", "blocks", "meta"],
|
||||||
|
properties: {
|
||||||
|
schema: { type: "string" },
|
||||||
|
shell: { type: "string" },
|
||||||
|
blocks: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["body", "monitor", "header", "footer", "aside"],
|
||||||
|
properties: {
|
||||||
|
body: { type: "string" },
|
||||||
|
monitor: { type: "string" },
|
||||||
|
header: { type: "string" },
|
||||||
|
footer: { type: "string" },
|
||||||
|
aside: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["suggested_actions"],
|
||||||
|
properties: {
|
||||||
|
suggested_actions: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 游玩转述:按探测到的 structured 投递写出 present.v1,不要创作追问 */
|
||||||
|
export function playPresentOutputInstruction(shell: PresentShellId): string {
|
||||||
|
return `
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 运行时输出协议(游玩正文)
|
||||||
|
|
||||||
|
本步产物是给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**:
|
||||||
|
|
||||||
|
{"schema":"present.v1","shell":"${shell}","blocks":{"body":"用户可读正文","monitor":"","header":"","footer":"","aside":""},"meta":{"suggested_actions":[]}}
|
||||||
|
|
||||||
|
\`shell\` 必须是 \`${shell}\`。只填该壳已有区域,没有的键留空字符串。
|
||||||
|
prose 壳也可直接输出 Markdown,程序会落入 body。
|
||||||
|
材料不够也先写一版场面。不要写追问、askUser、自评或「可验收产物」。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyPresentPacket(packet: PresentPacket): string {
|
||||||
|
return JSON.stringify(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function presentOutputFromFallback(
|
||||||
|
text: string,
|
||||||
|
shell: PresentShellId = "prose",
|
||||||
|
): string {
|
||||||
|
const body = stripPresentSourceFences(text).trim() || "(本轮场面未写完)";
|
||||||
|
return stringifyPresentPacket(presentFromPlainText(body, shell));
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePresentPacket(
|
export function parsePresentPacket(
|
||||||
raw: string | undefined | null,
|
raw: string | undefined | null,
|
||||||
fallbackShell: PresentShellId = "prose",
|
fallbackShell: PresentShellId = "prose",
|
||||||
|
|||||||
+195
-17
@@ -25,7 +25,18 @@ import { loadAppSettings } from "../config/settings.js";
|
|||||||
import { resolveActivePreset } from "../preset/store.js";
|
import { resolveActivePreset } from "../preset/store.js";
|
||||||
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
||||||
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
||||||
import { PRESENT_TAG } from "../skills/present-packet.js";
|
import {
|
||||||
|
PRESENT_TAG,
|
||||||
|
PRESENT_JSON_SCHEMA,
|
||||||
|
isPlayPresentWorker,
|
||||||
|
isPresentLikeObject,
|
||||||
|
parsePresentPacket,
|
||||||
|
parseShellAdaptationFromReplyFormat,
|
||||||
|
playPresentOutputInstruction,
|
||||||
|
presentOutputFromFallback,
|
||||||
|
stringifyPresentPacket,
|
||||||
|
type PresentShellId,
|
||||||
|
} from "../skills/present-packet.js";
|
||||||
import {
|
import {
|
||||||
coerceToFlatArtifact,
|
coerceToFlatArtifact,
|
||||||
flatArtifactToFragmentDoc,
|
flatArtifactToFragmentDoc,
|
||||||
@@ -44,6 +55,7 @@ import {
|
|||||||
extractFragmentAskSidecar,
|
extractFragmentAskSidecar,
|
||||||
isUsableContextFragment,
|
isUsableContextFragment,
|
||||||
looksLikeFragmentDoc,
|
looksLikeFragmentDoc,
|
||||||
|
parseContextFragment,
|
||||||
} from "../skills/context-fragment.js";
|
} from "../skills/context-fragment.js";
|
||||||
import {
|
import {
|
||||||
isProgressPointerTag,
|
isProgressPointerTag,
|
||||||
@@ -200,13 +212,66 @@ function existingOutputIsProduct(text: string | undefined): boolean {
|
|||||||
if (!text?.trim()) return false;
|
if (!text?.trim()) return false;
|
||||||
if (isUsableContextFragment(text)) return true;
|
if (isUsableContextFragment(text)) return true;
|
||||||
const parsed = tryParseJsonDoc(text);
|
const parsed = tryParseJsonDoc(text);
|
||||||
return looksLikeCreationFlowDoc(parsed) || looksLikeFragmentDoc(parsed);
|
return (
|
||||||
|
looksLikeCreationFlowDoc(parsed) ||
|
||||||
|
looksLikeFragmentDoc(parsed) ||
|
||||||
|
isPresentLikeObject(parsed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usablePlayFallbackText(raw?: string): string {
|
||||||
|
if (!raw?.trim()) return "";
|
||||||
|
if (isQuestionOnlyText(raw)) return "";
|
||||||
|
const parsed = tryParseJsonDoc(raw);
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playVisibleTarget(outputTags: string[]): string | undefined {
|
||||||
|
return productOutputTags(outputTags).find((t) => PLAY_FEED_OUTPUT_TAGS.has(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalizePresentOutput(
|
||||||
|
value: unknown,
|
||||||
|
fallbackShell: PresentShellId,
|
||||||
|
): string | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const view = parsePresentPacket(value, fallbackShell);
|
||||||
|
if (!view.packet.blocks.body && view.fallbackPlain && !value.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return stringifyPresentPacket(view.packet);
|
||||||
|
}
|
||||||
|
if (typeof value === "object") {
|
||||||
|
if (isPresentLikeObject(value)) {
|
||||||
|
const view = parsePresentPacket(JSON.stringify(value), fallbackShell);
|
||||||
|
return stringifyPresentPacket(view.packet);
|
||||||
|
}
|
||||||
|
const view = parsePresentPacket(JSON.stringify(value), fallbackShell);
|
||||||
|
if (!view.fallbackPlain) return stringifyPresentPacket(view.packet);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fragmentSceneText(doc: unknown): string | undefined {
|
||||||
|
const frag =
|
||||||
|
typeof doc === "string"
|
||||||
|
? parseContextFragment(doc)
|
||||||
|
: parseContextFragment(JSON.stringify(doc));
|
||||||
|
if (!frag) return undefined;
|
||||||
|
if (typeof frag.正文 === "string" && frag.正文.trim()) return frag.正文.trim();
|
||||||
|
if (typeof frag.brief === "string" && frag.brief.trim()) return frag.brief.trim();
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recoverFragmentOutputs(
|
function recoverFragmentOutputs(
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
outputs: Record<string, string>,
|
outputs: Record<string, string>,
|
||||||
outputTags: string[],
|
outputTags: string[],
|
||||||
|
fallbackShell: PresentShellId = "prose",
|
||||||
): void {
|
): void {
|
||||||
const target = productOutputTags(outputTags)[0];
|
const target = productOutputTags(outputTags)[0];
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
@@ -225,7 +290,8 @@ function recoverFragmentOutputs(
|
|||||||
if (
|
if (
|
||||||
isUsableContextFragment(text) ||
|
isUsableContextFragment(text) ||
|
||||||
looksLikeFragmentDoc(val) ||
|
looksLikeFragmentDoc(val) ||
|
||||||
looksLikeCreationFlowDoc(val)
|
looksLikeCreationFlowDoc(val) ||
|
||||||
|
isPresentLikeObject(val)
|
||||||
) {
|
) {
|
||||||
outputs[target] = text;
|
outputs[target] = text;
|
||||||
return;
|
return;
|
||||||
@@ -238,6 +304,14 @@ function recoverFragmentOutputs(
|
|||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
if (!protocolKeys.has(k)) rest[k] = v;
|
if (!protocolKeys.has(k)) rest[k] = v;
|
||||||
}
|
}
|
||||||
|
if (isPresentLikeObject(obj) || isPresentLikeObject(rest)) {
|
||||||
|
const source = isPresentLikeObject(obj) ? obj : rest;
|
||||||
|
const present = canonicalizePresentOutput(source, fallbackShell);
|
||||||
|
if (present) {
|
||||||
|
outputs[target] = present;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (looksLikeFragmentDoc(obj) || looksLikeFragmentDoc(rest)) {
|
if (looksLikeFragmentDoc(obj) || looksLikeFragmentDoc(rest)) {
|
||||||
const source = looksLikeFragmentDoc(obj) ? obj : rest;
|
const source = looksLikeFragmentDoc(obj) ? obj : rest;
|
||||||
outputs[target] = JSON.stringify(source);
|
outputs[target] = JSON.stringify(source);
|
||||||
@@ -267,6 +341,7 @@ function looksLikeStructuredDraft(text: string): boolean {
|
|||||||
if (t.startsWith("{") || t.startsWith("[")) return true;
|
if (t.startsWith("{") || t.startsWith("[")) return true;
|
||||||
if (/```(?:json)?/i.test(t) && t.includes("{")) return true;
|
if (/```(?:json)?/i.test(t) && t.includes("{")) return true;
|
||||||
if (/"schema"\s*:/.test(t)) return true;
|
if (/"schema"\s*:/.test(t)) return true;
|
||||||
|
if (/"blocks"\s*:/.test(t) && /"body"\s*:/.test(t)) return true;
|
||||||
if (/"技能"\s*:/.test(t) || /"正文"\s*:/.test(t)) return true;
|
if (/"技能"\s*:/.test(t) || /"正文"\s*:/.test(t)) return true;
|
||||||
if (/"inserts"\s*:/.test(t) || /"agents"\s*:/.test(t) || /"play_slots"\s*:/.test(t)) {
|
if (/"inserts"\s*:/.test(t) || /"agents"\s*:/.test(t) || /"play_slots"\s*:/.test(t)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -321,10 +396,26 @@ function lookupOutputValue(
|
|||||||
function parseWorkerResponse(
|
function parseWorkerResponse(
|
||||||
raw: string,
|
raw: string,
|
||||||
outputTags: string[],
|
outputTags: string[],
|
||||||
|
opts?: { fallbackShell?: PresentShellId },
|
||||||
): WorkerRunResult {
|
): WorkerRunResult {
|
||||||
|
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||||
|
const playTarget = playVisibleTarget(outputTags);
|
||||||
|
|
||||||
const parsed = tryParseJsonDoc(raw);
|
const parsed = tryParseJsonDoc(raw);
|
||||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
const target = playTarget ?? productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
|
if (playTarget) {
|
||||||
|
const body =
|
||||||
|
raw.trim() && !isQuestionOnlyText(raw)
|
||||||
|
? raw
|
||||||
|
: "";
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [playTarget]: presentOutputFromFallback(body, fallbackShell) },
|
||||||
|
{ summary: body.slice(0, 80) || "游玩正文" },
|
||||||
|
outputTags,
|
||||||
|
{ playVisible: true, fallbackShell },
|
||||||
|
);
|
||||||
|
}
|
||||||
if (target && looksLikeStructuredDraft(raw)) {
|
if (target && looksLikeStructuredDraft(raw)) {
|
||||||
return finalizeParsedOutputs(
|
return finalizeParsedOutputs(
|
||||||
{ [target]: raw },
|
{ [target]: raw },
|
||||||
@@ -357,6 +448,18 @@ function parseWorkerResponse(
|
|||||||
|
|
||||||
const obj = parsed as Record<string, unknown>;
|
const obj = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (playTarget && isPresentLikeObject(obj)) {
|
||||||
|
const present = canonicalizePresentOutput(obj, fallbackShell);
|
||||||
|
if (present) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [playTarget]: present },
|
||||||
|
obj,
|
||||||
|
outputTags,
|
||||||
|
{ playVisible: true, fallbackShell },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创作流程 DAG:根对象就是产物,不能先收成扁形(brief 会被当成稿、steps 丢掉)
|
// 创作流程 DAG:根对象就是产物,不能先收成扁形(brief 会被当成稿、steps 丢掉)
|
||||||
if (looksLikeCreationFlowDoc(obj)) {
|
if (looksLikeCreationFlowDoc(obj)) {
|
||||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||||
@@ -369,8 +472,21 @@ function parseWorkerResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 游玩正文:片段里的场面字收成 present,不要扁形成创作稿
|
||||||
|
if (playTarget && !isPresentLikeObject(obj)) {
|
||||||
|
const scene = fragmentSceneText(obj);
|
||||||
|
if (scene) {
|
||||||
|
return finalizeParsedOutputs(
|
||||||
|
{ [playTarget]: presentOutputFromFallback(scene, fallbackShell) },
|
||||||
|
obj,
|
||||||
|
outputTags,
|
||||||
|
{ playVisible: true, fallbackShell },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 扁形产物 → 旧 fragment 外壳。已是 context-fragment 深树则原样收下(保留自评与嵌套正文)。
|
// 扁形产物 → 旧 fragment 外壳。已是 context-fragment 深树则原样收下(保留自评与嵌套正文)。
|
||||||
if (!looksLikeFragmentDoc(obj)) {
|
if (!looksLikeFragmentDoc(obj) && !isPresentLikeObject(obj) && !playTarget) {
|
||||||
const asFlat = coerceToFlatArtifact(obj);
|
const asFlat = coerceToFlatArtifact(obj);
|
||||||
if (asFlat) {
|
if (asFlat) {
|
||||||
const frag = flatArtifactToFragmentDoc(asFlat);
|
const frag = flatArtifactToFragmentDoc(asFlat);
|
||||||
@@ -391,22 +507,53 @@ function parseWorkerResponse(
|
|||||||
if (outputsRaw && typeof outputsRaw === "object" && !Array.isArray(outputsRaw)) {
|
if (outputsRaw && typeof outputsRaw === "object" && !Array.isArray(outputsRaw)) {
|
||||||
const bag = outputsRaw as Record<string, unknown>;
|
const bag = outputsRaw as Record<string, unknown>;
|
||||||
for (const tag of productOutputTags(outputTags)) {
|
for (const tag of productOutputTags(outputTags)) {
|
||||||
assignOutput(outputs, tag, lookupOutputValue(bag, tag));
|
const val = lookupOutputValue(bag, tag);
|
||||||
|
if (playTarget && tag === playTarget) {
|
||||||
|
const present = canonicalizePresentOutput(val, fallbackShell);
|
||||||
|
if (present) {
|
||||||
|
outputs[tag] = present;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scene = fragmentSceneText(val);
|
||||||
|
if (scene) {
|
||||||
|
outputs[tag] = presentOutputFromFallback(scene, fallbackShell);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recoverFragmentOutputs(obj, outputs, outputTags);
|
assignOutput(outputs, tag, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recoverFragmentOutputs(obj, outputs, outputTags, fallbackShell);
|
||||||
|
|
||||||
return finalizeParsedOutputs(outputs, obj, outputTags);
|
if (playTarget && outputs[playTarget]) {
|
||||||
|
const present = canonicalizePresentOutput(outputs[playTarget], fallbackShell);
|
||||||
|
if (present) outputs[playTarget] = present;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalizeParsedOutputs(outputs, obj, outputTags, {
|
||||||
|
playVisible: Boolean(playTarget),
|
||||||
|
fallbackShell,
|
||||||
|
rawFallback: raw,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeParsedOutputs(
|
function finalizeParsedOutputs(
|
||||||
outputs: Record<string, string>,
|
outputs: Record<string, string>,
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
_outputTags: string[],
|
outputTags: string[],
|
||||||
|
opts?: {
|
||||||
|
playVisible?: boolean;
|
||||||
|
fallbackShell?: PresentShellId;
|
||||||
|
rawFallback?: string;
|
||||||
|
},
|
||||||
): WorkerRunResult {
|
): WorkerRunResult {
|
||||||
const isMetaAskToken = (s: string) => /^ask[_-]?user$/i.test(s.trim());
|
const isMetaAskToken = (s: string) => /^ask[_-]?user$/i.test(s.trim());
|
||||||
|
const playTarget = playVisibleTarget(outputTags);
|
||||||
|
const playVisible = Boolean(opts?.playVisible || playTarget);
|
||||||
|
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||||
|
|
||||||
let askUser: QuestionItem[] | undefined;
|
let askUser: QuestionItem[] | undefined;
|
||||||
|
if (!playVisible) {
|
||||||
if (Array.isArray(obj.askUser)) {
|
if (Array.isArray(obj.askUser)) {
|
||||||
askUser = normalizeQuestions(
|
askUser = normalizeQuestions(
|
||||||
obj.askUser.filter((q) => {
|
obj.askUser.filter((q) => {
|
||||||
@@ -434,10 +581,12 @@ function finalizeParsedOutputs(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 先抽追问;半残稿仍留在 outputs,追问挂验收卡(不因缺字段丢掉正文)
|
// 先抽追问;半残稿仍留在 outputs,追问挂验收卡(不因缺字段丢掉正文)
|
||||||
let askAssessment: string | undefined;
|
let askAssessment: string | undefined;
|
||||||
const fragQuestions: QuestionItem[] = [];
|
const fragQuestions: QuestionItem[] = [];
|
||||||
|
if (!playVisible) {
|
||||||
for (const content of Object.values(outputs)) {
|
for (const content of Object.values(outputs)) {
|
||||||
const side = extractFragmentAskSidecar(content);
|
const side = extractFragmentAskSidecar(content);
|
||||||
if (side.assessment && !askAssessment) askAssessment = side.assessment;
|
if (side.assessment && !askAssessment) askAssessment = side.assessment;
|
||||||
@@ -450,10 +599,24 @@ function finalizeParsedOutputs(
|
|||||||
if (fragQuestions.length) {
|
if (fragQuestions.length) {
|
||||||
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dropProgressPointerOutputs(outputs);
|
dropProgressPointerOutputs(outputs);
|
||||||
canonicalizeKnownOutputs(outputs);
|
canonicalizeKnownOutputs(outputs);
|
||||||
|
|
||||||
|
if (playVisible && playTarget && !outputs[playTarget]?.trim()) {
|
||||||
|
const scene = fragmentSceneText(obj);
|
||||||
|
const summaryText =
|
||||||
|
typeof obj.summary === "string" && !/[??]/.test(obj.summary)
|
||||||
|
? obj.summary.trim()
|
||||||
|
: "";
|
||||||
|
const fallback =
|
||||||
|
scene ||
|
||||||
|
summaryText ||
|
||||||
|
usablePlayFallbackText(opts?.rawFallback);
|
||||||
|
outputs[playTarget] = presentOutputFromFallback(fallback, fallbackShell);
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||||
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
||||||
}
|
}
|
||||||
@@ -470,8 +633,8 @@ function finalizeParsedOutputs(
|
|||||||
outputs,
|
outputs,
|
||||||
summary,
|
summary,
|
||||||
preview,
|
preview,
|
||||||
askUser: askUser?.length ? askUser : undefined,
|
askUser: playVisible ? undefined : askUser?.length ? askUser : undefined,
|
||||||
askAssessment,
|
askAssessment: playVisible ? undefined : askAssessment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,9 +781,20 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
)?.play_slots,
|
)?.play_slots,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const playPresent =
|
||||||
|
isPlayLayerActive(params.slots) && isPlayPresentWorker(worker.id);
|
||||||
|
const playShell: PresentShellId | undefined = playPresent
|
||||||
|
? parseShellAdaptationFromReplyFormat(
|
||||||
|
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||||
|
params.blackboard.getContentByTag("设计.回复格式"),
|
||||||
|
)?.shell_id ?? "prose"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const systemContent =
|
const systemContent =
|
||||||
promptBody +
|
promptBody +
|
||||||
WORKER_OUTPUT_INSTRUCTION +
|
(playPresent && playShell
|
||||||
|
? playPresentOutputInstruction(playShell)
|
||||||
|
: WORKER_OUTPUT_INSTRUCTION) +
|
||||||
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
|
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
|
||||||
const playPreset = isPlayLayerActive(params.slots)
|
const playPreset = isPlayLayerActive(params.slots)
|
||||||
? resolveActivePreset(loadAppSettings().activePresetId)
|
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||||
@@ -663,7 +837,9 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
stream: params.stream,
|
stream: params.stream,
|
||||||
caller: `worker:${worker.id}`,
|
caller: `worker:${worker.id}`,
|
||||||
});
|
});
|
||||||
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags);
|
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags, {
|
||||||
|
fallbackShell: playShell,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamCallbacks: StreamCallbacks | undefined = params.stream
|
const streamCallbacks: StreamCallbacks | undefined = params.stream
|
||||||
@@ -674,12 +850,12 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const result = await completeStructured(params.llm, messages, {
|
const result = await completeStructured(params.llm, messages, {
|
||||||
schema: {
|
schema: playPresent ? PRESENT_JSON_SCHEMA : {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: true,
|
additionalProperties: true,
|
||||||
},
|
},
|
||||||
name: "submit_worker_result",
|
name: playPresent ? "submit_present_packet" : "submit_worker_result",
|
||||||
schemaIsLoose: true,
|
schemaIsLoose: !playPresent,
|
||||||
capabilities: resolveActiveProfile()?.capabilities,
|
capabilities: resolveActiveProfile()?.capabilities,
|
||||||
caller: `worker:${worker.id}`,
|
caller: `worker:${worker.id}`,
|
||||||
stream: streamCallbacks,
|
stream: streamCallbacks,
|
||||||
@@ -692,6 +868,7 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
return parseWorkerResponse(
|
return parseWorkerResponse(
|
||||||
selectJsonPayload(raw, result.reasoning) || raw,
|
selectJsonPayload(raw, result.reasoning) || raw,
|
||||||
worker.outputTags,
|
worker.outputTags,
|
||||||
|
{ fallbackShell: playShell },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,8 +876,9 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
|||||||
export function parseWorkerResponseForTest(
|
export function parseWorkerResponseForTest(
|
||||||
raw: string,
|
raw: string,
|
||||||
outputTags: string[],
|
outputTags: string[],
|
||||||
|
opts?: { fallbackShell?: PresentShellId },
|
||||||
): WorkerRunResult {
|
): WorkerRunResult {
|
||||||
return parseWorkerResponse(raw, outputTags);
|
return parseWorkerResponse(raw, outputTags, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal 供单测验证 inputTags → inputs 拼接 */
|
/** @internal 供单测验证 inputTags → inputs 拼接 */
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
parsePresentPacket,
|
parsePresentPacket,
|
||||||
parseShellAdaptationFromReplyFormat,
|
parseShellAdaptationFromReplyFormat,
|
||||||
presentFromPlainText,
|
presentFromPlainText,
|
||||||
|
presentOutputFromFallback,
|
||||||
|
isPlayPresentWorker,
|
||||||
shellDefaultRegions,
|
shellDefaultRegions,
|
||||||
} from "../src/skills/present-packet.js";
|
} from "../src/skills/present-packet.js";
|
||||||
|
|
||||||
@@ -114,3 +116,18 @@ describe("shell adaptation from reply-format", () => {
|
|||||||
expect(adapt?.shell_id).toBe("prose");
|
expect(adapt?.shell_id).toBe("prose");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("play present worker", () => {
|
||||||
|
it("recognizes narrator and round-present", () => {
|
||||||
|
expect(isPlayPresentWorker("narrator")).toBe(true);
|
||||||
|
expect(isPlayPresentWorker("round-present")).toBe(true);
|
||||||
|
expect(isPlayPresentWorker("world-simulator")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fallback packet uses the given shell", () => {
|
||||||
|
const raw = presentOutputFromFallback("她点了点头。", "chat_monitor");
|
||||||
|
const view = parsePresentPacket(raw, "prose");
|
||||||
|
expect(view.packet.shell).toBe("chat_monitor");
|
||||||
|
expect(view.packet.blocks.body).toBe("她点了点头。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
parseWorkerResponseForTest,
|
parseWorkerResponseForTest,
|
||||||
sanitizeWorkerSetOutputs,
|
sanitizeWorkerSetOutputs,
|
||||||
} from "../src/worker/executor.js";
|
} from "../src/worker/executor.js";
|
||||||
|
import { parsePresentPacket } from "../src/skills/present-packet.js";
|
||||||
import { selectJsonPayload } from "../src/parse/json-doc.js";
|
import { selectJsonPayload } from "../src/parse/json-doc.js";
|
||||||
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
import { parseCreationFlow } from "../src/skills/creation-flow.js";
|
||||||
|
|
||||||
@@ -396,4 +397,52 @@ describe("sanitizeWorkerSetOutputs", () => {
|
|||||||
expect(result.preview).not.toContain("### ");
|
expect(result.preview).not.toContain("### ");
|
||||||
expect(result.preview.length).toBeGreaterThan(4000);
|
expect(result.preview.length).toBeGreaterThan(4000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("play narrator: present root without askUser", () => {
|
||||||
|
const result = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({
|
||||||
|
schema: "present.v1",
|
||||||
|
shell: "prose",
|
||||||
|
blocks: { body: "她端起碗,热气模糊了视线。" },
|
||||||
|
meta: { suggested_actions: [] },
|
||||||
|
}),
|
||||||
|
["输出.用户展示"],
|
||||||
|
{ fallbackShell: "prose" },
|
||||||
|
);
|
||||||
|
expect(result.askUser).toBeUndefined();
|
||||||
|
expect(result.outputs["输出.用户展示"]).toContain("她端起碗");
|
||||||
|
const view = parsePresentPacket(result.outputs["输出.用户展示"], "prose");
|
||||||
|
expect(view.packet.blocks.body).toContain("她端起碗");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("play narrator: fragment with 追问 does not surface askUser", () => {
|
||||||
|
const result = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "叙事转述",
|
||||||
|
brief: "吃饭场面",
|
||||||
|
正文: "他决定先吃饭。",
|
||||||
|
追问: {
|
||||||
|
导语: "还差体验",
|
||||||
|
题目: [{ 问: "你要什么节奏?", 建议选项: ["慢", "快"] }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
["输出.用户展示"],
|
||||||
|
{ fallbackShell: "prose" },
|
||||||
|
);
|
||||||
|
expect(result.askUser).toBeUndefined();
|
||||||
|
expect(result.outputs["输出.用户展示"]).toContain("他决定先吃饭");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("play narrator: empty output falls back to present body", () => {
|
||||||
|
const result = parseWorkerResponseForTest(
|
||||||
|
JSON.stringify({ askUser: ["这一步还没写出可验收的产物"] }),
|
||||||
|
["输出.用户展示"],
|
||||||
|
{ fallbackShell: "turn_panel" },
|
||||||
|
);
|
||||||
|
expect(result.askUser).toBeUndefined();
|
||||||
|
expect(result.outputs["输出.用户展示"]).toBeTruthy();
|
||||||
|
const view = parsePresentPacket(result.outputs["输出.用户展示"], "prose");
|
||||||
|
expect(view.packet.shell).toBe("turn_panel");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+45
-29
@@ -647,7 +647,19 @@ function formatPlayPresentHtml(body, view) {
|
|||||||
if (presentView) {
|
if (presentView) {
|
||||||
return renderPresentShellHtml(presentView.packet, esc, { tweaks });
|
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,
|
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 } = {}) {
|
export function setRailTab(tab, { expand = false } = {}) {
|
||||||
activeRailTab = tab === "log" ? "log" : "books";
|
activeRailTab = tab === "log" ? "log" : "books";
|
||||||
if (expand) railUserExpanded = true;
|
if (expand) railUserExpanded = true;
|
||||||
@@ -717,6 +739,7 @@ export function setRailTab(tab, { expand = false } = {}) {
|
|||||||
panel.classList.toggle("active", active);
|
panel.classList.toggle("active", active);
|
||||||
panel.toggleAttribute("hidden", !active);
|
panel.toggleAttribute("hidden", !active);
|
||||||
});
|
});
|
||||||
|
syncExpandLabel();
|
||||||
syncRailCollapse();
|
syncRailCollapse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,25 +751,23 @@ function wireRailChrome() {
|
|||||||
const btn = e.target.closest(".rail-tab");
|
const btn = e.target.closest(".rail-tab");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const tab = btn.getAttribute("data-rail-tab") ?? "books";
|
const tab = btn.getAttribute("data-rail-tab") ?? "books";
|
||||||
const collapsed = document.body.classList.contains("rail-collapsed");
|
setRailTab(tab);
|
||||||
setRailTab(tab, { expand: collapsed });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const toggle = document.getElementById("btn-rail-toggle");
|
const expand = document.getElementById("btn-rail-expand");
|
||||||
if (toggle && !toggle.dataset.wired) {
|
if (expand && !expand.dataset.wired) {
|
||||||
toggle.dataset.wired = "1";
|
expand.dataset.wired = "1";
|
||||||
toggle.addEventListener("click", () => {
|
expand.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;
|
railUserExpanded = true;
|
||||||
} else {
|
syncRailCollapse();
|
||||||
railUserExpanded = !railUserExpanded;
|
});
|
||||||
}
|
}
|
||||||
|
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();
|
syncRailCollapse();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -762,7 +783,7 @@ function hasStartedCreation(view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncRailCollapse(view) {
|
function syncRailCollapse(view) {
|
||||||
const inBook = document.body.dataset.navLevel === "book";
|
const inBook = isWorkspaceNav();
|
||||||
if (!inBook) {
|
if (!inBook) {
|
||||||
document.body.dataset.railCanCollapse = "0";
|
document.body.dataset.railCanCollapse = "0";
|
||||||
railUserExpanded = true;
|
railUserExpanded = true;
|
||||||
@@ -772,18 +793,12 @@ function syncRailCollapse(view) {
|
|||||||
const canCollapse = document.body.dataset.railCanCollapse === "1";
|
const canCollapse = document.body.dataset.railCanCollapse === "1";
|
||||||
const collapsed = canCollapse && !railUserExpanded;
|
const collapsed = canCollapse && !railUserExpanded;
|
||||||
document.body.classList.toggle("rail-collapsed", collapsed);
|
document.body.classList.toggle("rail-collapsed", collapsed);
|
||||||
const toggle = document.getElementById("btn-rail-toggle");
|
const expand = document.getElementById("btn-rail-expand");
|
||||||
if (toggle) {
|
if (expand) {
|
||||||
if (inBook && !collapsed) {
|
expand.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||||
toggle.title = "返回作品目录";
|
expand.title = collapsed ? "展开侧栏" : "收起侧栏";
|
||||||
toggle.setAttribute("aria-label", "返回作品目录");
|
|
||||||
toggle.textContent = "‹";
|
|
||||||
} else {
|
|
||||||
toggle.title = collapsed ? "展开侧栏" : "收起侧栏";
|
|
||||||
toggle.setAttribute("aria-label", collapsed ? "展开侧栏" : "收起侧栏");
|
|
||||||
toggle.textContent = collapsed ? "›" : "‹";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
syncExpandLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetRailChrome() {
|
export function resetRailChrome() {
|
||||||
@@ -831,6 +846,7 @@ function shouldShowInFeed(msg, view) {
|
|||||||
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
||||||
if (msg.compressed) return false;
|
if (msg.compressed) return false;
|
||||||
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
|
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
|
||||||
|
if (isPlayView(view) && kind === "worker_questions") return false;
|
||||||
if (isSkillSelectionMessage(msg)) return false;
|
if (isSkillSelectionMessage(msg)) return false;
|
||||||
if (isReviewSidecarQuestionStub(msg)) return false;
|
if (isReviewSidecarQuestionStub(msg)) return false;
|
||||||
if (
|
if (
|
||||||
@@ -852,7 +868,7 @@ function shouldShowInFeed(msg, view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeSyncRail(view) {
|
function maybeSyncRail(view) {
|
||||||
if (document.body.dataset.navLevel !== "book") {
|
if (!isWorkspaceNav()) {
|
||||||
railUserExpanded = true;
|
railUserExpanded = true;
|
||||||
document.body.dataset.railCanCollapse = "0";
|
document.body.dataset.railCanCollapse = "0";
|
||||||
syncRailCollapse(view);
|
syncRailCollapse(view);
|
||||||
|
|||||||
+205
-97
@@ -2,6 +2,7 @@ import {
|
|||||||
renderWorkspace,
|
renderWorkspace,
|
||||||
updateLiveStreamPanel,
|
updateLiveStreamPanel,
|
||||||
resetRailChrome,
|
resetRailChrome,
|
||||||
|
setRailTab,
|
||||||
fillStepArtifactDialog,
|
fillStepArtifactDialog,
|
||||||
canOfferSaveProduct,
|
canOfferSaveProduct,
|
||||||
canOfferEnterPlay,
|
canOfferEnterPlay,
|
||||||
@@ -32,7 +33,6 @@ let selectedBookIds = new Set();
|
|||||||
let activePlaySaveId = null;
|
let activePlaySaveId = null;
|
||||||
const playSavesByBook = new Map();
|
const playSavesByBook = new Map();
|
||||||
const playWorkingInstanceByBook = new Map();
|
const playWorkingInstanceByBook = new Map();
|
||||||
const expandedProductsByBook = new Map();
|
|
||||||
const playSavesLoading = new Set();
|
const playSavesLoading = new Set();
|
||||||
let bookMenuBookId = null;
|
let bookMenuBookId = null;
|
||||||
let livePollTimer = null;
|
let livePollTimer = null;
|
||||||
@@ -99,7 +99,7 @@ function resolveUserTask(view, loading) {
|
|||||||
const hasQuestions = Boolean(getActiveQuestions(view)?.questions?.length);
|
const hasQuestions = Boolean(getActiveQuestions(view)?.questions?.length);
|
||||||
const moduleOpening = isModuleOpeningWaiting(view);
|
const moduleOpening = isModuleOpeningWaiting(view);
|
||||||
|
|
||||||
if (isPlayView(view) && wr?.kind !== "worker_questions") {
|
if (isPlayView(view)) {
|
||||||
return {
|
return {
|
||||||
id: "speak",
|
id: "speak",
|
||||||
label: USER_TASK.speak.label,
|
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) {
|
function bookPlayReady(bookId) {
|
||||||
if (lastView?.bookId === bookId && Boolean(lastView?.playReady || lastView?.hasProduct)) {
|
if (lastView?.bookId === bookId && Boolean(lastView?.playReady || lastView?.hasProduct)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1070,7 +1056,7 @@ async function fetchPlaySavesForBook(bookId, force = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
||||||
if (on && sidebarNav.level === "book") return;
|
if (on && sidebarNav.level !== "root") return;
|
||||||
bookSelectMode = on;
|
bookSelectMode = on;
|
||||||
if (!on) selectedBookIds = new Set();
|
if (!on) selectedBookIds = new Set();
|
||||||
else if (!keepSelection) selectedBookIds = new Set();
|
else if (!keepSelection) selectedBookIds = new Set();
|
||||||
@@ -1134,26 +1120,93 @@ function showBookMenu(bookId, x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCatalogNav() {
|
function isCatalogNav() {
|
||||||
return sidebarNav.level !== "book";
|
return sidebarNav.level === "root";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWorkspaceNav() {
|
||||||
|
return sidebarNav.level === "book" || sidebarNav.level === "product";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNavBook() {
|
function getNavBook() {
|
||||||
if (sidebarNav.level !== "book") return null;
|
if (!isWorkspaceNav() || !sidebarNav.bookId) return null;
|
||||||
return books.find((b) => b.id === sidebarNav.bookId) ?? null;
|
return books.find((b) => b.id === sidebarNav.bookId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncRailContext() {
|
function getNavProduct() {
|
||||||
const el = $("rail-context");
|
if (sidebarNav.level !== "product" || !sidebarNav.productId || !sidebarNav.bookId) return null;
|
||||||
if (!el) return;
|
return productInstancesForBook(sidebarNav.bookId).find((p) => p.id === sidebarNav.productId) ?? null;
|
||||||
const book = getNavBook();
|
|
||||||
if (sidebarNav.level === "book" && book) {
|
|
||||||
el.textContent = book.title;
|
|
||||||
el.title = book.title;
|
|
||||||
} else {
|
|
||||||
el.textContent = "作品";
|
|
||||||
el.removeAttribute("title");
|
|
||||||
}
|
}
|
||||||
$("rail-tabs")?.toggleAttribute("hidden", sidebarNav.level !== "book");
|
|
||||||
|
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();
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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) {
|
function navigateToBook(bookId) {
|
||||||
@@ -1162,7 +1215,20 @@ function navigateToBook(bookId) {
|
|||||||
document.body.dataset.navLevel = "book";
|
document.body.dataset.navLevel = "book";
|
||||||
bookSelectMode = false;
|
bookSelectMode = false;
|
||||||
selectedBookIds = new Set();
|
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();
|
renderBookList();
|
||||||
void fetchPlaySavesForBook(bookId);
|
void fetchPlaySavesForBook(bookId);
|
||||||
}
|
}
|
||||||
@@ -1175,7 +1241,7 @@ function showCatalog() {
|
|||||||
bookSelectMode = false;
|
bookSelectMode = false;
|
||||||
selectedBookIds = new Set();
|
selectedBookIds = new Set();
|
||||||
closeWorkspace();
|
closeWorkspace();
|
||||||
syncRailContext();
|
renderPathBar();
|
||||||
renderBookList();
|
renderBookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,17 +1314,15 @@ function renderExplorerRow({
|
|||||||
return row;
|
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 loading = playSavesLoading.has(book.id);
|
||||||
const saves = playSavesByBook.get(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) {
|
if (loading && saves === undefined) {
|
||||||
const hint = document.createElement("p");
|
const hint = document.createElement("p");
|
||||||
hint.className = "explorer-hint";
|
hint.className = "explorer-hint";
|
||||||
@@ -1267,42 +1331,17 @@ function renderBookBranch(book, list) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const product of products) {
|
const playHere = book.id === activeBookId && isPlayNow();
|
||||||
const isLatest = product.id === latestId;
|
const playingInstanceId = playingInstanceIdForBook(book);
|
||||||
const isPlayingHere = playHere && playingInstanceId === product.id;
|
|
||||||
const hasWorking = playingInstanceId === product.id;
|
const hasWorking = playingInstanceId === product.id;
|
||||||
const expanded = isProductExpanded(book.id, product.id, isPlayingHere);
|
const runs = runsForProduct(book.id, product.id, productInstancesForBook(book.id));
|
||||||
const runs = runsForProduct(book.id, product.id, products);
|
|
||||||
const wrap = document.createElement("div");
|
|
||||||
wrap.className = `explorer-product${expanded ? " is-open" : ""}`;
|
|
||||||
|
|
||||||
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) {
|
if (hasWorking) {
|
||||||
drop.appendChild(
|
list.appendChild(
|
||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: "当前游玩",
|
name: "当前游玩",
|
||||||
meta: isPlayingHere && !activePlaySaveId ? "进行中" : "工作副本",
|
meta: playHere && !activePlaySaveId ? "进行中" : "工作副本",
|
||||||
active: isPlayingHere && !activePlaySaveId,
|
active: playHere && !activePlaySaveId,
|
||||||
child: true,
|
|
||||||
onClick: () => void openCurrentPlay(book.id),
|
onClick: () => void openCurrentPlay(book.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1310,19 +1349,18 @@ function renderBookBranch(book, list) {
|
|||||||
|
|
||||||
if (!runs.length && !hasWorking) {
|
if (!runs.length && !hasWorking) {
|
||||||
const hint = document.createElement("p");
|
const hint = document.createElement("p");
|
||||||
hint.className = "explorer-drop-empty";
|
hint.className = "explorer-hint";
|
||||||
hint.textContent = "还没有游玩存档";
|
hint.textContent = "还没有游玩存档";
|
||||||
drop.appendChild(hint);
|
list.appendChild(hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const run of runs) {
|
for (const run of runs) {
|
||||||
const runWhen = formatSaveWhen(run.createdAt);
|
const runWhen = formatSaveWhen(run.createdAt);
|
||||||
drop.appendChild(
|
list.appendChild(
|
||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: run.label,
|
name: run.label,
|
||||||
meta: runWhen || "游玩进度",
|
meta: runWhen || "游玩进度",
|
||||||
active: playHere && activePlaySaveId === run.id,
|
active: playHere && activePlaySaveId === run.id,
|
||||||
child: true,
|
|
||||||
actionClass: "explorer-file",
|
actionClass: "explorer-file",
|
||||||
onClick: () => void loadPlaySave(book.id, run.id),
|
onClick: () => void loadPlaySave(book.id, run.id),
|
||||||
onRename: () => void renamePlaySave(book.id, run.id, run.label),
|
onRename: () => void renamePlaySave(book.id, run.id, run.label),
|
||||||
@@ -1331,19 +1369,13 @@ function renderBookBranch(book, list) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drop.appendChild(
|
list.appendChild(
|
||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: "+ 新开一局",
|
name: "+ 新开一局",
|
||||||
actionClass: "explorer-action",
|
actionClass: "explorer-action",
|
||||||
child: true,
|
|
||||||
onClick: () => void startNewPlayForBook(book.id, product.id),
|
onClick: () => void startNewPlayForBook(book.id, product.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
wrap.appendChild(drop);
|
|
||||||
}
|
|
||||||
|
|
||||||
list.appendChild(wrap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBookContents(book, list) {
|
function renderBookContents(book, list) {
|
||||||
@@ -1356,13 +1388,56 @@ function renderBookContents(book, list) {
|
|||||||
onClick: () => void openBookDesign(book.id),
|
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() {
|
function renderBookList() {
|
||||||
const list = $("book-list");
|
const list = $("book-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
syncRailContext();
|
renderPathBar();
|
||||||
if (!books.length) {
|
if (!books.length) {
|
||||||
list.innerHTML = `<p class="sidebar-empty">暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button></p>`;
|
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);
|
$("btn-new-inline")?.addEventListener("click", openNewBookDialog);
|
||||||
@@ -1370,6 +1445,34 @@ function renderBookList() {
|
|||||||
}
|
}
|
||||||
list.innerHTML = "";
|
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") {
|
if (sidebarNav.level === "book") {
|
||||||
const book = getNavBook();
|
const book = getNavBook();
|
||||||
if (book) {
|
if (book) {
|
||||||
@@ -1378,7 +1481,7 @@ function renderBookList() {
|
|||||||
}
|
}
|
||||||
sidebarNav = { level: "root" };
|
sidebarNav = { level: "root" };
|
||||||
document.body.dataset.navLevel = "root";
|
document.body.dataset.navLevel = "root";
|
||||||
syncRailContext();
|
renderPathBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const book of books) {
|
for (const book of books) {
|
||||||
@@ -1396,7 +1499,7 @@ function renderBookList() {
|
|||||||
toggleBookSelection(book.id);
|
toggleBookSelection(book.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void openBook(book.id);
|
navigateToBook(book.id);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1561,7 +1664,11 @@ function setupBookBoxSelect() {
|
|||||||
|
|
||||||
async function loadPlaySave(bookId, saveId) {
|
async function loadPlaySave(bookId, saveId) {
|
||||||
try {
|
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);
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
const data = await api(
|
const data = await api(
|
||||||
`/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`,
|
`/api/books/${encodeURIComponent(bookId)}/saves/${encodeURIComponent(saveId)}/load`,
|
||||||
@@ -1600,6 +1707,10 @@ async function deletePlaySave(bookId, saveId, label, kindLabel = "游玩存档")
|
|||||||
{ method: "DELETE" },
|
{ method: "DELETE" },
|
||||||
);
|
);
|
||||||
if (activePlaySaveId === saveId) activePlaySaveId = null;
|
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);
|
await fetchPlaySavesForBook(bookId, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
@@ -1613,16 +1724,14 @@ async function startNewPlayForBook(bookId, instanceId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
navigateToBook(bookId);
|
if (instanceId) navigateToProduct(bookId, instanceId);
|
||||||
|
else navigateToBook(bookId);
|
||||||
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, {
|
const data = await api(`/api/books/${encodeURIComponent(bookId)}/play/new`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(instanceId ? { instanceId } : {}),
|
body: JSON.stringify(instanceId ? { instanceId } : {}),
|
||||||
});
|
});
|
||||||
activePlaySaveId = null;
|
activePlaySaveId = null;
|
||||||
if (instanceId) {
|
|
||||||
expandedProductsByBook.set(bookId, new Set([instanceId]));
|
|
||||||
}
|
|
||||||
renderSession(data.session, false);
|
renderSession(data.session, false);
|
||||||
void fetchPlaySavesForBook(bookId, true);
|
void fetchPlaySavesForBook(bookId, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1645,7 +1754,9 @@ async function openBookDesign(bookId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openCurrentPlay(bookId) {
|
async function openCurrentPlay(bookId) {
|
||||||
navigateToBook(bookId);
|
const instanceId = playingInstanceIdForBook({ id: bookId });
|
||||||
|
if (instanceId) navigateToProduct(bookId, instanceId);
|
||||||
|
else navigateToBook(bookId);
|
||||||
const alreadyHere =
|
const alreadyHere =
|
||||||
activeBookId === bookId &&
|
activeBookId === bookId &&
|
||||||
isPlayNow() &&
|
isPlayNow() &&
|
||||||
@@ -2307,10 +2418,7 @@ async function saveCurrentInstance() {
|
|||||||
}
|
}
|
||||||
await fetchPlaySavesForBook(activeBookId, true);
|
await fetchPlaySavesForBook(activeBookId, true);
|
||||||
const newest = productInstancesForBook(activeBookId)[0];
|
const newest = productInstancesForBook(activeBookId)[0];
|
||||||
if (newest) {
|
if (newest) navigateToProduct(activeBookId, newest.id);
|
||||||
expandedProductsByBook.set(activeBookId, new Set([newest.id]));
|
|
||||||
renderBookList();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -15,22 +15,37 @@
|
|||||||
<body data-lifecycle="design" data-nav-level="root">
|
<body data-lifecycle="design" data-nav-level="root">
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<aside class="panel panel-rail" id="panel-books">
|
<aside class="panel panel-rail" id="panel-books">
|
||||||
|
<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">
|
<header class="panel-head rail-head">
|
||||||
<div class="rail-head-row">
|
<div class="rail-head-row">
|
||||||
<button type="button" class="rail-toggle btn-sm" id="btn-rail-toggle" title="返回作品目录" aria-label="返回作品目录">‹</button>
|
<nav class="path-bar rail-path" id="book-path-bar" aria-label="位置">
|
||||||
<h1 class="rail-context" id="rail-context">作品</h1>
|
<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">
|
<div class="panel-head-actions rail-head-actions">
|
||||||
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
|
||||||
<span class="burst-badge" id="burst-badge" hidden></span>
|
<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>
|
</div>
|
||||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="本书侧栏" hidden>
|
<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="本书">
|
<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>
|
<span class="rail-tab-label">本书</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
<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>
|
<span class="rail-tab-label">调度</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -47,6 +62,7 @@
|
|||||||
<div id="agent-timeline" class="agent-timeline"></div>
|
<div id="agent-timeline" class="agent-timeline"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="panel panel-main">
|
<main class="panel panel-main">
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ function normalizeQuestions(raw) {
|
|||||||
|
|
||||||
/** @returns {{ questions: any[], workerId?: string, pageSize: number, assessment?: string, optional?: boolean, skipLabel?: string } | null} */
|
/** @returns {{ questions: any[], workerId?: string, pageSize: number, assessment?: string, optional?: boolean, skipLabel?: string } | null} */
|
||||||
export function getActiveQuestions(view) {
|
export function getActiveQuestions(view) {
|
||||||
|
if (view?.playLayerActive || view?.lifecycleStage === "play") return null;
|
||||||
const wr = view?.waitingReason;
|
const wr = view?.waitingReason;
|
||||||
if (!wr) return null;
|
if (!wr) return null;
|
||||||
// 默认每页 1 题,左右切换;勿一次堆多题
|
// 默认每页 1 题,左右切换;勿一次堆多题
|
||||||
|
|||||||
+68
-73
@@ -12,7 +12,7 @@
|
|||||||
--warn: #ca8a04;
|
--warn: #ca8a04;
|
||||||
--danger: #dc2626;
|
--danger: #dc2626;
|
||||||
--books-w: 216px;
|
--books-w: 216px;
|
||||||
--books-w-collapsed: 52px;
|
--books-w-collapsed: 40px;
|
||||||
--coord-w: clamp(280px, 24vw, 340px);
|
--coord-w: clamp(280px, 24vw, 340px);
|
||||||
--coord-w-collapsed: 40px;
|
--coord-w-collapsed: 40px;
|
||||||
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
||||||
@@ -82,46 +82,86 @@ a.link:hover { text-decoration: underline; }
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 6px 8px 6px 6px;
|
padding: 6px 8px 6px 10px;
|
||||||
}
|
}
|
||||||
body[data-nav-level="root"] .rail-head-row {
|
.rail-collapse {
|
||||||
padding-left: 10px;
|
display: none;
|
||||||
}
|
|
||||||
.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 {
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
body.rail-collapsed .rail-toggle { transform: rotate(180deg); }
|
body[data-rail-can-collapse="1"]:not(.rail-collapsed) .rail-collapse {
|
||||||
body[data-nav-level="root"] .rail-toggle { display: none; }
|
display: inline-flex;
|
||||||
body[data-nav-level="book"] .rail-head-actions #btn-new-book,
|
}
|
||||||
body[data-nav-level="root"] .burst-badge { display: none; }
|
.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 {
|
.rail-tabs {
|
||||||
display: none;
|
display: none;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
body[data-nav-level="book"] .rail-tabs {
|
body:not([data-nav-level="root"]) .rail-tabs {
|
||||||
display: flex;
|
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-tabs[hidden] { display: none !important; }
|
||||||
.rail-tab {
|
.rail-tab {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
@@ -213,40 +253,6 @@ body[data-nav-level="book"] .rail-tabs {
|
|||||||
}
|
}
|
||||||
.rail-tab-panel[hidden] { display: none !important; }
|
.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; }
|
body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||||
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
||||||
.panel-main {
|
.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; }
|
.panel-head-actions { display: flex; gap: 4px; align-items: center; }
|
||||||
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||||||
|
|
||||||
/* Books — pill breadcrumb + list */
|
/* Books — breadcrumb + list */
|
||||||
.path-bar {
|
.path-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin: 8px 8px 0;
|
|
||||||
padding: 7px 10px;
|
|
||||||
border: none;
|
|
||||||
background: var(--bg);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.path-bar[hidden] { display: none; }
|
|
||||||
.path-bar-list {
|
.path-bar-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1579,6 +1579,9 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.rail-path .path-seg.current {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.path-seg:hover { color: var(--accent); }
|
.path-seg:hover { color: var(--accent); }
|
||||||
.path-seg:focus-visible {
|
.path-seg:focus-visible {
|
||||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
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;
|
color: var(--muted); letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.explorer-hint {
|
.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 {
|
.explorer-product {
|
||||||
margin: 2px 0 6px;
|
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-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
||||||
.dialog-head-row { display: flex; justify-content: space-between; align-items: center; }
|
.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 ——
|
/* —— 游玩呈现壳 present.v1 ——
|
||||||
* 主工作面 = body(随 500~2000 字撑高)。
|
* 主工作面 = body(随 500~2000 字撑高)。
|
||||||
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
||||||
|
|||||||
Reference in New Issue
Block a user