新建作品改为直接进入对话落盘,按本局创作方案写能力,并把楼层呈现和零层卡分开。
输入栏同时可以按分组切换模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+130
-39
@@ -188,6 +188,30 @@ function canRefreshMessage(msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩 0 层开场:多条时用 ‹ › 切换,选中的一条替换开场消息。
|
||||
* 与消息版本 swipe 分开,不重 roll。
|
||||
*/
|
||||
function renderOpeningSwipeHtml(view, msg) {
|
||||
if (!isPlayView(view)) return "";
|
||||
if (String(msg?.title ?? "").trim() !== "开场白") return "";
|
||||
const openings = view.playAux?.openings ?? [];
|
||||
const total = openings.length;
|
||||
if (total < 2) return "";
|
||||
const current = Math.min(
|
||||
total - 1,
|
||||
Math.max(0, Number(view.playAux?.selectedOpeningIndex) || 0),
|
||||
);
|
||||
const id = esc(msg.id);
|
||||
const prev = current - 1;
|
||||
const next = current + 1;
|
||||
return `<span class="msg-variant-nav" title="‹ › 切换开场,选中的一条作为 0 层">
|
||||
<button type="button" class="msg-action" data-msg-action="opening-prev" data-msg-id="${id}" data-opening-index="${prev}"${prev < 0 ? " disabled" : ""} aria-label="上一条开场">‹</button>
|
||||
<span class="msg-variant-count">${current + 1}/${total}</span>
|
||||
<button type="button" class="msg-action" data-msg-action="opening-next" data-msg-id="${id}" data-opening-index="${next}"${next >= total ? " disabled" : ""} aria-label="下一条开场">›</button>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
/** ‹ n/total › 多版切换;末页 › 与 ↻ 均可重 roll;单版只显示 ↻ */
|
||||
function renderVariantNavHtml(msg) {
|
||||
if (!msg?.id) return "";
|
||||
@@ -741,10 +765,7 @@ function openContentPeek(payload) {
|
||||
}
|
||||
|
||||
function formatDictatePeekHtml(content) {
|
||||
const trimmed = String(content ?? "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
// 与侧栏同一路径:勿走 formatArtifactBodyHtml(## 分段会丢掉前置 JSON/正文)
|
||||
return `<div class="dictate-product-body">${safeBodyHtml(trimmed)}</div>`;
|
||||
return renderStoredProductHtml(content);
|
||||
}
|
||||
|
||||
const dictateProductContentByEl = new WeakMap();
|
||||
@@ -1144,6 +1165,13 @@ function wireMessageFeedActions(feed, handlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "opening-prev" || action === "opening-next") {
|
||||
const index = Number(btn.getAttribute("data-opening-index"));
|
||||
if (!Number.isFinite(index) || index < 0) return;
|
||||
h.onSelectOpening?.(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "variant-prev") {
|
||||
h.onSwitchVariant?.(messageId, "prev");
|
||||
return;
|
||||
@@ -1231,9 +1259,64 @@ function isPlayProcessMessage(msg, view) {
|
||||
return !isPlayFinalReply(msg, view);
|
||||
}
|
||||
|
||||
/** 开场回退把多份产物拼成 `【设计.tag】` + JSON。拆开后按产物卡渲染,不把结构化原文当散文。 */
|
||||
function splitBracketProductSections(text) {
|
||||
const trimmed = String(text || "").trim();
|
||||
const re = /^【((?:设计|用户)\.[^】]+)】[ \t]*$/gm;
|
||||
const hits = [];
|
||||
let m;
|
||||
while ((m = re.exec(trimmed))) {
|
||||
hits.push({
|
||||
title: m[1].trim(),
|
||||
index: m.index,
|
||||
headerEnd: m.index + m[0].length,
|
||||
});
|
||||
}
|
||||
if (!hits.length) return null;
|
||||
const sections = [];
|
||||
if (hits[0].index > 0) {
|
||||
const preamble = trimmed.slice(0, hits[0].index).trim();
|
||||
if (preamble) sections.push({ title: "", content: preamble });
|
||||
}
|
||||
for (let i = 0; i < hits.length; i++) {
|
||||
const h = hits[i];
|
||||
const end = i + 1 < hits.length ? hits[i + 1].index : trimmed.length;
|
||||
sections.push({
|
||||
title: h.title,
|
||||
content: trimmed.slice(h.headerEnd, end).trim(),
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderStoredProductHtml(content) {
|
||||
const trimmed = String(content ?? "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
const parsed = tryParseJsonDoc(trimmed);
|
||||
if (parsed != null && typeof parsed === "object") {
|
||||
return formatArtifactBodyHtml(trimmed, {
|
||||
flat: true,
|
||||
hideAskSidecar: true,
|
||||
hideScores: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return `<div class="dictate-product-body">${safeBodyHtml(trimmed)}</div>`;
|
||||
}
|
||||
|
||||
function formatPlayPresentHtml(body, view) {
|
||||
const trimmed = stripPresentSourceFences((body || "").trim());
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
const bundled = splitBracketProductSections(trimmed);
|
||||
if (bundled) {
|
||||
return bundled
|
||||
.map((sec) => {
|
||||
if (!sec.title) return `<p class="msg-bundle-lead">${esc(sec.content)}</p>`;
|
||||
return renderStoredProductHtml(sec.content);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
const tweaks = view?.presentationTweaks;
|
||||
const fallbackShell =
|
||||
tweaks?.shell_id && PRESENT_SHELL_IDS?.includes?.(tweaks.shell_id)
|
||||
@@ -2041,6 +2124,7 @@ function normalizeContextFragmentDoc(doc) {
|
||||
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
|
||||
const row = { ...doc };
|
||||
const hasBody = row.正文 != null || row.body != null;
|
||||
if (row.schema === "context-fragment-v1") row.schema = "context-fragment.v1";
|
||||
const looksFrag =
|
||||
row.schema === "context-fragment.v1" ||
|
||||
(typeof row.技能 === "string" && row.技能.trim()) ||
|
||||
@@ -2625,27 +2709,36 @@ function collectOpeningCandidates(body) {
|
||||
const sharedVars = openingVariablesFrom(body);
|
||||
const rows = [];
|
||||
const seen = new Set();
|
||||
const push = (text, persona, variables) => {
|
||||
const push = (text, persona, variables, id) => {
|
||||
const value = openingTextFrom(text);
|
||||
if (!value || seen.has(value)) return;
|
||||
seen.add(value);
|
||||
rows.push({
|
||||
id: String(id || "").trim(),
|
||||
text: value,
|
||||
persona: persona || fallback,
|
||||
variables: variables?.length ? variables : sharedVars,
|
||||
});
|
||||
};
|
||||
push(body.开场白全文, fallback, sharedVars);
|
||||
if (Array.isArray(body.开场白候选)) {
|
||||
for (const item of body.开场白候选) {
|
||||
if (typeof item === "string") push(item, fallback, sharedVars);
|
||||
else if (item && typeof item === "object") {
|
||||
push(
|
||||
item.正文 || item.全文 || item.text || item.开场白 || item.内容,
|
||||
openingPersonaFrom(item, fallback),
|
||||
openingVariablesFrom(item),
|
||||
);
|
||||
}
|
||||
const listed = Array.isArray(body.开场白候选) ? body.开场白候选 : [];
|
||||
if (!listed.length) push(body.开场白全文, fallback, sharedVars, "");
|
||||
for (const item of listed) {
|
||||
if (typeof item === "string") push(item, fallback, sharedVars, "");
|
||||
else if (item && typeof item === "object") {
|
||||
push(
|
||||
item.正文 || item.全文 || item.text || item.开场白 || item.内容,
|
||||
openingPersonaFrom(item, fallback),
|
||||
openingVariablesFrom(item),
|
||||
item.id || item.候选id,
|
||||
);
|
||||
}
|
||||
}
|
||||
const defaultId = String(body.默认候选id || body.默认候选ID || "").trim();
|
||||
if (defaultId) {
|
||||
const index = rows.findIndex((row) => row.id === defaultId);
|
||||
if (index > 0) {
|
||||
const [picked] = rows.splice(index, 1);
|
||||
rows.unshift(picked);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
@@ -5690,24 +5783,22 @@ function renderDictateProductsList(view) {
|
||||
.replace(/#/g, " · ");
|
||||
return `<div class="dictate-product dictate-product--rail" role="listitem" data-dictate-product>
|
||||
<button type="button" class="dictate-product-sum" aria-expanded="false">
|
||||
<span class="dictate-product-sum-inner">
|
||||
<span class="dictate-product-sum-top">
|
||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||
${
|
||||
order != null
|
||||
? `<span class="dictate-product-order" title="相对序">${order}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="dictate-product-chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="dictate-product-sum-top">
|
||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||
${
|
||||
preview
|
||||
? `<span class="dictate-product-preview">${esc(preview)}${
|
||||
content.length > 72 ? "…" : ""
|
||||
}</span>`
|
||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||
order != null
|
||||
? `<span class="dictate-product-order" title="相对序">${order}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="dictate-product-chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
${
|
||||
preview
|
||||
? `<span class="dictate-product-preview">${esc(preview)}${
|
||||
content.length > 72 ? "…" : ""
|
||||
}</span>`
|
||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||
}
|
||||
</button>
|
||||
<div class="dictate-product-body" hidden data-pending="1"></div>
|
||||
</div>`;
|
||||
@@ -5720,9 +5811,7 @@ function hydrateDictateProductBody(card) {
|
||||
const body = card.querySelector(".dictate-product-body");
|
||||
if (!body || body.dataset.pending !== "1") return;
|
||||
const content = readDictateProductContent(card);
|
||||
body.innerHTML = content.trim()
|
||||
? safeBodyHtml(content)
|
||||
: `<p class="empty-sm">(无正文)</p>`;
|
||||
body.innerHTML = renderStoredProductHtml(content);
|
||||
delete body.dataset.pending;
|
||||
}
|
||||
|
||||
@@ -6050,9 +6139,7 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
? `${view.uiPrompt}${
|
||||
view.selectedRecipe?.name
|
||||
? `\n\n已选配方:${view.selectedRecipe.name}`
|
||||
: view.recipes?.length
|
||||
? "\n\n(请先在新建作品时选定配方)"
|
||||
: ""
|
||||
: ""
|
||||
}`
|
||||
: view.waitingReason?.kind === "worker_questions"
|
||||
? "在下方回答提问。"
|
||||
@@ -6122,11 +6209,12 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
!label.includes(subtitle) &&
|
||||
subtitle !== label;
|
||||
const headInner = isUser
|
||||
? `<span class="msg-time">${fmtTime(msg.createdAt)}</span>${renderMsgVariantBadge(msg)}`
|
||||
? renderMsgVariantBadge(msg)
|
||||
: `<div class="msg-head-main">
|
||||
<span class="msg-tag">${esc(label)}</span>
|
||||
${showSubtitle ? `<span class="msg-subtitle">${esc(subtitle)}</span>` : ""}
|
||||
</div>
|
||||
${renderOpeningSwipeHtml(view, msg)}
|
||||
${renderMsgVariantBadge(msg)}
|
||||
<span class="msg-time">${fmtTime(msg.createdAt)}</span>`;
|
||||
const menuTrigger = `<button type="button" class="msg-menu-trigger" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>`;
|
||||
@@ -6136,7 +6224,10 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
card.dataset.originalText = body;
|
||||
|
||||
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
|
||||
const showThinking = !isUser && msg.thinking && isPlayView(view);
|
||||
const showThinking =
|
||||
!isUser &&
|
||||
Boolean(msg.thinking) &&
|
||||
(isPlayView(view) || kind === "dictate_reply");
|
||||
const innerBody = questionsActive
|
||||
? `<p class="msg-q-index">${esc(body)}</p>`
|
||||
: kind === "worker_questions"
|
||||
|
||||
+272
-195
@@ -10,7 +10,7 @@ import {
|
||||
} from "./agent-ui.js";
|
||||
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { displayCreationModeLabel, displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import { displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import {
|
||||
clearQuestionCardState,
|
||||
collectQuestionAnswers,
|
||||
@@ -34,6 +34,16 @@ import {
|
||||
initMarkdownRender,
|
||||
isMarkdownRenderEnabled,
|
||||
} from "./markdown.js";
|
||||
import {
|
||||
filterUsedApiProfiles,
|
||||
forgetApiProfileUsage,
|
||||
loadApiProfileUsage,
|
||||
membersUsageRank,
|
||||
pruneApiProfileUsage,
|
||||
recordApiProfileUsage,
|
||||
seedApiProfileUsage,
|
||||
sortByApiProfileUsage,
|
||||
} from "./api-profile-usage.js";
|
||||
import { wireDirectiveAutocomplete } from "./directive-autocomplete.js";
|
||||
import {
|
||||
refreshDirectiveHighlight,
|
||||
@@ -79,18 +89,61 @@ let livePollFailCount = 0;
|
||||
let inflightSeq = 0;
|
||||
/** 底栏菜单:只切当前 API,不编辑 */
|
||||
let apiProfileList = [];
|
||||
let apiProfileGroups = [];
|
||||
let activeApiProfileId = null;
|
||||
/** idle | loading | ready | error */
|
||||
let apiProfileMenuStatus = "idle";
|
||||
let apiProfileMenuError = "";
|
||||
let apiProfileSwitchingId = null;
|
||||
let apiProfileSwitchingLabel = "";
|
||||
|
||||
const LIVE_POLL_MS = 280;
|
||||
const WORKSPACE_SESSION_KEY = "wa-workspace-session-v1";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" };
|
||||
|
||||
function readWorkspaceSession() {
|
||||
try {
|
||||
const value = JSON.parse(sessionStorage.getItem(WORKSPACE_SESSION_KEY) || "null");
|
||||
if (!value || typeof value !== "object" || typeof value.bookId !== "string") {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberWorkspaceSession() {
|
||||
if (!activeBookId || !sessionId || lastView?.bookId !== activeBookId) return;
|
||||
try {
|
||||
const mode = isPlayView(lastView) ? "play" : "design";
|
||||
sessionStorage.setItem(
|
||||
WORKSPACE_SESSION_KEY,
|
||||
JSON.stringify({
|
||||
bookId: activeBookId,
|
||||
sessionId,
|
||||
mode,
|
||||
playSaveId:
|
||||
mode === "play" ? activePlaySaveId || "play-working" : null,
|
||||
navLevel: sidebarNav.level === "book" ? "book" : "root",
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* sessionStorage 不可用时维持无记忆模式 */
|
||||
}
|
||||
}
|
||||
|
||||
function forgetWorkspaceSession() {
|
||||
try {
|
||||
sessionStorage.removeItem(WORKSPACE_SESSION_KEY);
|
||||
} catch {
|
||||
/* sessionStorage 不可用 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户任务三态(表层);底层 waitingReason 映射进来 */
|
||||
const USER_TASK = {
|
||||
speak: { id: "speak", label: "说话", title: "继续说" },
|
||||
@@ -158,7 +211,8 @@ function resolveUserTask(view, loading) {
|
||||
if (canOfferSaveProduct(view)) {
|
||||
return {
|
||||
id: "speak",
|
||||
label: "可保存",
|
||||
label: USER_TASK.speak.label,
|
||||
hideChip: true,
|
||||
title: "落档产物",
|
||||
hint: view.hasProduct
|
||||
? "可再保存一份新定稿,或用已有产物开玩。"
|
||||
@@ -300,6 +354,10 @@ function esc(s) {
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function attr(s) {
|
||||
return esc(s).replace(/"/g, """);
|
||||
}
|
||||
|
||||
function proposedOutputCopy(proposed) {
|
||||
const params = proposed?.params;
|
||||
const target =
|
||||
@@ -585,8 +643,10 @@ function resolveComposer(view, loading) {
|
||||
|
||||
function composerModeChip(spec) {
|
||||
const id = spec.task?.id;
|
||||
if (spec.task?.hideChip) return "";
|
||||
if (id !== "speak" && id !== "answer" && id !== "review" && id !== "busy") return "";
|
||||
const label = spec.task.label || "";
|
||||
if (!label) return "";
|
||||
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" — ");
|
||||
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
|
||||
return `<span class="composer-mode-chip" data-task="${esc(id)}"${tone} title="${esc(tip)}">${esc(label)}</span>`;
|
||||
@@ -888,6 +948,7 @@ function summarizeApiProfile(profile) {
|
||||
id: profile.id,
|
||||
name: profile.name || "未命名",
|
||||
model: profile.model || "",
|
||||
groupId: profile.groupId || "",
|
||||
hasKey: Boolean(String(profile.apiKey ?? "").trim()),
|
||||
};
|
||||
}
|
||||
@@ -903,8 +964,7 @@ function composerMoreMenuTitle() {
|
||||
|
||||
function composerApiSubmenuMeta() {
|
||||
if (apiProfileSwitchingId) {
|
||||
const p = apiProfileList.find((x) => x.id === apiProfileSwitchingId);
|
||||
return { desc: p ? `切换到 ${p.name}…` : "切换中…", title: "API" };
|
||||
return { desc: apiProfileSwitchingLabel || "切换中…", title: "API" };
|
||||
}
|
||||
if (apiProfileMenuStatus === "loading" || apiProfileMenuStatus === "idle") {
|
||||
return { desc: "加载中…", title: "API" };
|
||||
@@ -916,12 +976,47 @@ function composerApiSubmenuMeta() {
|
||||
return { desc: "未配置", title: "API" };
|
||||
}
|
||||
const active = activeApiProfile();
|
||||
const group = apiProfileGroups.find((item) => item.id === active?.groupId);
|
||||
const groupName = group?.name ? `${group.name} · ` : "";
|
||||
const model = active?.model ? ` · ${active.model}` : "";
|
||||
return {
|
||||
desc: active?.name || "选择配置",
|
||||
title: active ? `API · ${active.name}` : "API",
|
||||
desc: active ? `${groupName}${active.name}${model}` : "选择配置",
|
||||
title: active ? `API · ${active.name}${model}` : "API",
|
||||
};
|
||||
}
|
||||
|
||||
function composerProfileButtonHtml(profile, switching) {
|
||||
const active = profile.id === activeApiProfileId;
|
||||
const disabled = !profile.hasKey || switching;
|
||||
const model = profile.model || profile.name || "未指定模型";
|
||||
const nameNote =
|
||||
profile.name && profile.name !== profile.model ? profile.name : "";
|
||||
const desc = !profile.hasKey ? "未设置 Key" : nameNote;
|
||||
const forget =
|
||||
!active && profile.id
|
||||
? `<button type="button" class="more-profile-forget" data-forget-profile="${attr(profile.id)}" title="移出曾用记录,配置仍保留">移出</button>`
|
||||
: "";
|
||||
return `<div class="more-profile-row">
|
||||
<button
|
||||
type="button"
|
||||
class="more-menu-item more-theme-item more-profile-item${active ? " is-active" : ""}"
|
||||
data-activate-profile="${attr(profile.id)}"
|
||||
role="menuitemradio"
|
||||
aria-checked="${active ? "true" : "false"}"
|
||||
${disabled ? "disabled" : ""}
|
||||
${profile.id === apiProfileSwitchingId ? "aria-busy=\"true\"" : ""}
|
||||
title="${attr(model)}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(model)}</span>
|
||||
${desc ? `<span class="more-theme-desc">${esc(desc)}</span>` : ""}
|
||||
</span>
|
||||
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||
</button>
|
||||
${forget}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function composerApiProfileItemsHtml() {
|
||||
if (apiProfileMenuStatus === "loading" || apiProfileMenuStatus === "idle") {
|
||||
return `<p class="more-menu-empty">加载 API 配置…</p>`;
|
||||
@@ -933,50 +1028,60 @@ function composerApiProfileItemsHtml() {
|
||||
return `<p class="more-menu-empty">还没有 API 配置。<a href="/settings.html">去设置添加</a></p>`;
|
||||
}
|
||||
const switching = Boolean(apiProfileSwitchingId);
|
||||
return apiProfileList
|
||||
.map((p) => {
|
||||
const active = p.id === activeApiProfileId;
|
||||
const disabled = !p.hasKey || switching;
|
||||
const desc = !p.hasKey ? "未设置 Key" : p.model || "未指定模型";
|
||||
return `<button
|
||||
type="button"
|
||||
class="more-menu-item more-theme-item more-profile-item${active ? " is-active" : ""}"
|
||||
data-activate-profile="${esc(p.id)}"
|
||||
role="menuitemradio"
|
||||
aria-checked="${active ? "true" : "false"}"
|
||||
${disabled ? "disabled" : ""}
|
||||
${p.id === apiProfileSwitchingId ? "aria-busy=\"true\"" : ""}
|
||||
title="${esc(p.name)}${p.model ? ` · ${esc(p.model)}` : ""}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(p.name)}</span>
|
||||
<span class="more-theme-desc">${esc(desc)}</span>
|
||||
</span>
|
||||
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||
</button>`;
|
||||
const usage = loadApiProfileUsage();
|
||||
const buckets = new Map();
|
||||
for (const profile of apiProfileList) {
|
||||
const key = profile.groupId || "";
|
||||
if (!buckets.has(key)) buckets.set(key, []);
|
||||
buckets.get(key).push(profile);
|
||||
}
|
||||
for (const [key, members] of buckets) {
|
||||
buckets.set(key, filterUsedApiProfiles(members, usage, activeApiProfileId));
|
||||
}
|
||||
const ordered = [
|
||||
...apiProfileGroups.filter((group) => buckets.has(group.id)),
|
||||
...[...buckets.keys()]
|
||||
.filter((id) => id && !apiProfileGroups.some((group) => group.id === id))
|
||||
.map((id) => ({ id, name: "未命名组" })),
|
||||
];
|
||||
if (buckets.has("")) ordered.push({ id: "", name: "未分组" });
|
||||
const sortedGroups = sortByApiProfileUsage(ordered, (group) =>
|
||||
membersUsageRank(buckets.get(group.id) || [], usage),
|
||||
);
|
||||
return sortedGroups
|
||||
.map((group) => {
|
||||
const members = buckets.get(group.id) || [];
|
||||
if (!members.length) return "";
|
||||
if (members.length === 1) {
|
||||
return composerProfileButtonHtml(members[0], switching);
|
||||
}
|
||||
const containsActive = members.some((profile) => profile.id === activeApiProfileId);
|
||||
const existing = document.querySelector(
|
||||
`#composer-api-menu [data-api-group="${CSS.escape(group.id)}"]`,
|
||||
);
|
||||
const open = existing ? existing.hasAttribute("open") : containsActive;
|
||||
const activeModel = members.find((profile) => profile.id === activeApiProfileId)?.model;
|
||||
const desc = activeModel || `${members.length} 个曾用`;
|
||||
return `<details class="api-fold" data-api-group="${attr(group.id)}"${open ? " open" : ""}>
|
||||
<summary class="more-menu-item more-theme-item api-fold-sum${containsActive ? " is-active" : ""}">
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">${esc(group.name)}</span>
|
||||
<span class="more-theme-desc">${esc(desc)}</span>
|
||||
</span>
|
||||
<span class="api-fold-chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="api-fold-list" role="group" aria-label="${attr(group.name)}">
|
||||
${members.map((profile) => composerProfileButtonHtml(profile, switching)).join("")}
|
||||
</div>
|
||||
</details>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function composerApiSubmenuHtml() {
|
||||
function composerApiSectionHtml() {
|
||||
const meta = composerApiSubmenuMeta();
|
||||
const wasSubOpen = $("composer-api-submenu")?.hasAttribute("open");
|
||||
return `<details class="more-submenu" id="composer-api-submenu"${wasSubOpen ? " open" : ""}>
|
||||
<summary
|
||||
class="more-menu-item more-theme-item more-submenu-sum"
|
||||
title="${esc(meta.title)}"
|
||||
aria-label="${esc(meta.title)}"
|
||||
>
|
||||
<span class="more-theme-copy">
|
||||
<span class="more-theme-name">API</span>
|
||||
<span class="more-theme-desc" id="composer-api-submenu-desc">${esc(meta.desc)}</span>
|
||||
</span>
|
||||
<span class="more-submenu-chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-submenu-panel" id="composer-api-menu" role="menu">
|
||||
${composerApiProfileItemsHtml()}
|
||||
</div>
|
||||
</details>`;
|
||||
return `<div class="more-menu-label" id="composer-api-label" title="${attr(meta.title)}">API</div>
|
||||
<div id="composer-api-menu" role="group" aria-label="API 配置">${composerApiProfileItemsHtml()}</div>`;
|
||||
}
|
||||
|
||||
function composerMoreMenuHtml() {
|
||||
@@ -1006,7 +1111,7 @@ function composerMoreMenuHtml() {
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel composer-more-panel" role="menu">
|
||||
${composerApiSubmenuHtml()}
|
||||
${composerApiSectionHtml()}
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">显示</div>
|
||||
<button
|
||||
@@ -1059,13 +1164,8 @@ function syncApiProfileMenuUi() {
|
||||
const host = $("composer-api-menu");
|
||||
if (host) host.innerHTML = composerApiProfileItemsHtml();
|
||||
const meta = composerApiSubmenuMeta();
|
||||
const desc = $("composer-api-submenu-desc");
|
||||
if (desc) desc.textContent = meta.desc;
|
||||
const subSum = document.querySelector("#composer-api-submenu > summary");
|
||||
if (subSum) {
|
||||
subSum.title = meta.title;
|
||||
subSum.setAttribute("aria-label", meta.title);
|
||||
}
|
||||
const label = $("composer-api-label");
|
||||
if (label) label.title = meta.title;
|
||||
const sum = document.querySelector("#composer-more-menu .more-menu-sum");
|
||||
if (sum) {
|
||||
const title = composerMoreMenuTitle();
|
||||
@@ -1081,7 +1181,10 @@ async function loadApiProfiles() {
|
||||
try {
|
||||
const data = await api("/api/profiles");
|
||||
apiProfileList = (data.profiles || []).map(summarizeApiProfile);
|
||||
apiProfileGroups = data.groups || [];
|
||||
activeApiProfileId = data.activeProfileId ?? apiProfileList[0]?.id ?? null;
|
||||
pruneApiProfileUsage(new Set(apiProfileList.map((p) => p.id)));
|
||||
seedApiProfileUsage(activeApiProfileId);
|
||||
apiProfileMenuStatus = "ready";
|
||||
syncApiProfileMenuUi();
|
||||
} catch (err) {
|
||||
@@ -1093,7 +1196,6 @@ async function loadApiProfiles() {
|
||||
|
||||
function closeComposerMoreMenu(menu) {
|
||||
menu?.removeAttribute("open");
|
||||
menu?.querySelector("#composer-api-submenu")?.removeAttribute("open");
|
||||
}
|
||||
|
||||
async function activateApiProfileFromMenu(id, menu) {
|
||||
@@ -1104,17 +1206,21 @@ async function activateApiProfileFromMenu(id, menu) {
|
||||
const profile = apiProfileList.find((p) => p.id === id);
|
||||
if (!profile?.hasKey) return;
|
||||
apiProfileSwitchingId = id;
|
||||
apiProfileSwitchingLabel = profile ? `切换到 ${profile.name}…` : "切换中…";
|
||||
syncApiProfileMenuUi();
|
||||
try {
|
||||
const result = await api(`/api/profiles/${encodeURIComponent(id)}/activate`, {
|
||||
method: "POST",
|
||||
});
|
||||
activeApiProfileId = result.activeProfileId || id;
|
||||
recordApiProfileUsage(activeApiProfileId);
|
||||
apiProfileSwitchingId = null;
|
||||
apiProfileSwitchingLabel = "";
|
||||
syncApiProfileMenuUi();
|
||||
closeComposerMoreMenu(menu);
|
||||
} catch (err) {
|
||||
apiProfileSwitchingId = null;
|
||||
apiProfileSwitchingLabel = "";
|
||||
syncApiProfileMenuUi();
|
||||
alert(err.message || "切换失败");
|
||||
}
|
||||
@@ -2104,6 +2210,7 @@ function showCatalog({ closeSession = true } = {}) {
|
||||
selectedBookIds = new Set();
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
if (!closeSession) rememberWorkspaceSession();
|
||||
}
|
||||
|
||||
function renderExplorerRow({
|
||||
@@ -2230,15 +2337,15 @@ function renderSpecChildren(book, product, list) {
|
||||
}
|
||||
}
|
||||
|
||||
function appendNewPlayRow(book, list, instanceId, { child = false } = {}) {
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: "+ 新开一局",
|
||||
actionClass: "explorer-action",
|
||||
child,
|
||||
onClick: () => void startNewPlayForBook(book.id, instanceId),
|
||||
}),
|
||||
);
|
||||
function appendNewPlayFromSnapshot(book, list, instanceId) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "explorer-add child";
|
||||
btn.textContent = "+";
|
||||
btn.title = "从这份创作快照新开一局";
|
||||
btn.setAttribute("aria-label", "新开一局");
|
||||
btn.addEventListener("click", () => void startNewPlayForBook(book.id, instanceId));
|
||||
list.appendChild(btn);
|
||||
}
|
||||
|
||||
function appendCurrentDesignRow(book, list) {
|
||||
@@ -2250,7 +2357,7 @@ function appendCurrentDesignRow(book, list) {
|
||||
meta: live ? "进行中" : "继续",
|
||||
active: live,
|
||||
actionClass: "explorer-design",
|
||||
title: "打开创作会话;定稿会出现在下方",
|
||||
title: "继续创作;定稿会出现在下方",
|
||||
onClick: () => void resumeDesignSession(book.id),
|
||||
}),
|
||||
);
|
||||
@@ -2275,13 +2382,11 @@ function renderBookContents(book, list) {
|
||||
if (!products.length) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "explorer-hint";
|
||||
hint.textContent = bookPlayReady(book.id) ? "还没有规格" : "定稿后会出现在这里";
|
||||
hint.textContent = bookPlayReady(book.id) ? "还没有创作快照" : "定稿后会出现在这里";
|
||||
list.appendChild(hint);
|
||||
if (bookPlayReady(book.id)) appendNewPlayRow(book, list);
|
||||
return;
|
||||
}
|
||||
|
||||
let anyExpanded = false;
|
||||
for (const product of products) {
|
||||
const expanded = specIsExpanded(book.id, product.id);
|
||||
const bits = [];
|
||||
@@ -2290,7 +2395,7 @@ function renderBookContents(book, list) {
|
||||
renderExplorerRow({
|
||||
name: product.label,
|
||||
meta: bits.join(" · "),
|
||||
title: "展开或收起;双击改名;右键编辑产物",
|
||||
title: "已保存的创作快照;展开可看游玩",
|
||||
actionClass: "explorer-spec",
|
||||
twistie: true,
|
||||
expanded,
|
||||
@@ -2303,14 +2408,8 @@ function renderBookContents(book, list) {
|
||||
showSaveMenu(book.id, product.id, product.label, e.clientX, e.clientY),
|
||||
}),
|
||||
);
|
||||
if (expanded) {
|
||||
anyExpanded = true;
|
||||
renderSpecChildren(book, product, list);
|
||||
appendNewPlayRow(book, list, product.id, { child: true });
|
||||
}
|
||||
}
|
||||
if (!anyExpanded) {
|
||||
appendNewPlayRow(book, list, playingInstanceId || products[0]?.id);
|
||||
if (expanded) renderSpecChildren(book, product, list);
|
||||
appendNewPlayFromSnapshot(book, list, product.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2524,7 +2623,7 @@ function setupBookBoxSelect() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPlaySave(bookId, saveId) {
|
||||
async function loadPlaySave(bookId, saveId, { silent = false } = {}) {
|
||||
try {
|
||||
const products = productInstancesForBook(bookId);
|
||||
const run = playRunsForBook(bookId).find((r) => r.id === saveId);
|
||||
@@ -2538,9 +2637,11 @@ async function loadPlaySave(bookId, saveId) {
|
||||
);
|
||||
activePlaySaveId = saveId === "play-working" ? null : saveId;
|
||||
renderSession(data.session, false);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (lastView) renderSession(lastView, false);
|
||||
alert(err.message);
|
||||
if (!silent) alert(err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2697,6 +2798,7 @@ function renderHeader(view, loading) {
|
||||
function closeWorkspace({ emptyCopy = "" } = {}) {
|
||||
hideEnterPicker();
|
||||
stopLivePoll();
|
||||
forgetWorkspaceSession();
|
||||
sessionId = null;
|
||||
lastView = null;
|
||||
activeBookId = null;
|
||||
@@ -3003,6 +3105,7 @@ function renderSession(view, loading = false) {
|
||||
},
|
||||
});
|
||||
renderComposer(view, loading);
|
||||
rememberWorkspaceSession();
|
||||
if (loading) startLivePoll();
|
||||
else stopLivePoll();
|
||||
}
|
||||
@@ -3386,130 +3489,20 @@ async function loadBooks() {
|
||||
|
||||
function openNewBookDialog() {
|
||||
$("input-book-title").value = "";
|
||||
void populateDirectorSelect();
|
||||
$("dialog-new-book").showModal();
|
||||
$("input-book-title").focus();
|
||||
}
|
||||
|
||||
async function populateDirectorSelect() {
|
||||
const sel = $("select-director");
|
||||
const desc = $("director-desc");
|
||||
const modeSel = $("select-creation-mode");
|
||||
const modeDesc = $("creation-mode-desc");
|
||||
const fieldDirector = $("field-director");
|
||||
let recipeDirectors = [];
|
||||
let dictateDirectors = [];
|
||||
|
||||
const fillOptions = (list) => {
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
if (!list.length) {
|
||||
sel.innerHTML = `<option value="">暂无配方</option>`;
|
||||
sel.required = false;
|
||||
return;
|
||||
}
|
||||
for (const d of list) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = d.id;
|
||||
opt.textContent = d.name || d.id;
|
||||
if (d.declaration) opt.dataset.declaration = d.declaration;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
sel.value = list[0].id;
|
||||
sel.required = true;
|
||||
};
|
||||
|
||||
const syncDesc = () => {
|
||||
if (!sel || !desc) return;
|
||||
if (!modeSel?.value) {
|
||||
desc.hidden = true;
|
||||
desc.textContent = "";
|
||||
return;
|
||||
}
|
||||
const list =
|
||||
modeSel.value === "dictate" ? dictateDirectors : recipeDirectors;
|
||||
const cur = list.find((d) => d.id === sel.value);
|
||||
const text = (cur?.declaration ?? "").trim();
|
||||
desc.hidden = !text;
|
||||
desc.textContent = text;
|
||||
};
|
||||
|
||||
const syncMode = () => {
|
||||
const mode = modeSel?.value?.trim() || "";
|
||||
if (!mode) {
|
||||
if (fieldDirector) fieldDirector.hidden = true;
|
||||
if (modeDesc) modeDesc.hidden = true;
|
||||
if (desc) desc.hidden = true;
|
||||
if (sel) {
|
||||
sel.innerHTML = `<option value="">请先选择进料方式</option>`;
|
||||
sel.required = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const dictate = mode === "dictate";
|
||||
if (fieldDirector) fieldDirector.hidden = false;
|
||||
fillOptions(dictate ? dictateDirectors : recipeDirectors);
|
||||
if (modeDesc) {
|
||||
modeDesc.hidden = false;
|
||||
modeDesc.textContent = dictate
|
||||
? `${displayCreationModeLabel("dictate")}:对话写入产物;变量/映射用工具钉死;下方为对应配方。`
|
||||
: `${displayCreationModeLabel("recipe")}:按工作流计划逐步验收技能;下方为对应配方。`;
|
||||
}
|
||||
syncDesc();
|
||||
};
|
||||
|
||||
if (modeSel && !modeSel.dataset.bound) {
|
||||
modeSel.dataset.bound = "1";
|
||||
modeSel.addEventListener("change", syncMode);
|
||||
}
|
||||
if (sel && !sel.dataset.boundDesc) {
|
||||
sel.dataset.boundDesc = "1";
|
||||
sel.addEventListener("change", syncDesc);
|
||||
}
|
||||
if (!sel) return;
|
||||
try {
|
||||
const data = await api("/api/directors");
|
||||
recipeDirectors = data.recipeDirectors ?? data.directors ?? [];
|
||||
dictateDirectors = data.dictateDirectors ?? [];
|
||||
// 打开对话框时重置为「先选进料」
|
||||
if (modeSel) {
|
||||
modeSel.value = "";
|
||||
const placeholder = [...modeSel.options].find((o) => !o.value);
|
||||
if (placeholder) placeholder.selected = true;
|
||||
}
|
||||
syncMode();
|
||||
} catch (err) {
|
||||
sel.innerHTML = `<option value="">加载失败</option>`;
|
||||
if (desc) {
|
||||
desc.hidden = false;
|
||||
desc.textContent = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createBook() {
|
||||
const title = $("input-book-title").value.trim() || "未命名作品";
|
||||
const creationMode = $("select-creation-mode")?.value?.trim() || "";
|
||||
const recipeId = $("select-director")?.value?.trim();
|
||||
if (creationMode !== "recipe" && creationMode !== "dictate") {
|
||||
alert("请先选择进料方式");
|
||||
return;
|
||||
}
|
||||
if (!recipeId) {
|
||||
alert("请选择配方");
|
||||
return;
|
||||
}
|
||||
$("btn-create-book").disabled = true;
|
||||
try {
|
||||
const directorsMeta = await api("/api/directors").catch(() => null);
|
||||
const orchestratorId = directorsMeta?.skillPackId || "world-simulator";
|
||||
const data = await api("/api/books", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
orchestratorId,
|
||||
creationMode,
|
||||
recipeId,
|
||||
orchestratorId: "world-simulator",
|
||||
creationMode: "dictate",
|
||||
}),
|
||||
});
|
||||
$("dialog-new-book").close();
|
||||
@@ -3747,8 +3740,12 @@ async function openModelComparePicker(messageId) {
|
||||
<label class="compare-profile-item">
|
||||
<input type="checkbox" name="compare-profile" value="${esc(p.id)}" ${i < 2 ? "checked" : ""} />
|
||||
<span>
|
||||
<strong>${esc(p.name)}</strong>
|
||||
<div class="muted">${esc(p.model)}</div>
|
||||
<strong>${esc(p.model || p.name)}</strong>
|
||||
<div class="muted">${esc(
|
||||
[p.name && p.name !== p.model ? p.name : "", p.baseUrl || ""]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
)}</div>
|
||||
</span>
|
||||
</label>`,
|
||||
)
|
||||
@@ -4133,6 +4130,16 @@ $("composer")?.addEventListener("click", (e) => {
|
||||
void loadApiProfiles();
|
||||
return;
|
||||
}
|
||||
const forgetBtn = e.target.closest("[data-forget-profile]");
|
||||
if (forgetBtn && menu.contains(forgetBtn)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const id = forgetBtn.getAttribute("data-forget-profile");
|
||||
if (id && id !== activeApiProfileId && forgetApiProfileUsage(id)) {
|
||||
syncApiProfileMenuUi();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const profileBtn = e.target.closest("[data-activate-profile]");
|
||||
if (profileBtn && menu.contains(profileBtn)) {
|
||||
e.preventDefault();
|
||||
@@ -4155,23 +4162,93 @@ document.addEventListener("click", (e) => {
|
||||
document.querySelectorAll("details.more-menu[open]").forEach((d) => {
|
||||
if (!d.contains(e.target)) {
|
||||
d.removeAttribute("open");
|
||||
d.querySelectorAll("details.more-submenu[open]").forEach((sub) => {
|
||||
d.querySelectorAll("details.more-submenu[open], details.api-fold[open]").forEach((sub) => {
|
||||
sub.removeAttribute("open");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** API 组二级菜单:同时只展开一个 */
|
||||
document.addEventListener("toggle", (e) => {
|
||||
const fold = e.target;
|
||||
if (!(fold instanceof HTMLDetailsElement) || !fold.classList.contains("api-fold") || !fold.open) {
|
||||
return;
|
||||
}
|
||||
const root = fold.closest("#composer-api-menu");
|
||||
if (!root) return;
|
||||
root.querySelectorAll("details.api-fold[open]").forEach((other) => {
|
||||
if (other !== fold) other.removeAttribute("open");
|
||||
});
|
||||
}, true);
|
||||
|
||||
async function restoreWorkspaceSession() {
|
||||
const saved = readWorkspaceSession();
|
||||
if (!saved || !books.some((book) => book.id === saved.bookId)) {
|
||||
forgetWorkspaceSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookId = saved.bookId;
|
||||
activeBookId = bookId;
|
||||
activePlaySaveId =
|
||||
saved.playSaveId && saved.playSaveId !== "play-working"
|
||||
? saved.playSaveId
|
||||
: null;
|
||||
if (saved.navLevel === "book") {
|
||||
navigateToBook(bookId);
|
||||
} else {
|
||||
sidebarNav = { level: "root" };
|
||||
document.body.dataset.navLevel = "root";
|
||||
renderPathBar();
|
||||
renderBookList();
|
||||
}
|
||||
|
||||
if (saved.sessionId) {
|
||||
try {
|
||||
const view = await api(
|
||||
`/api/sessions/${encodeURIComponent(saved.sessionId)}`,
|
||||
);
|
||||
if (view?.bookId === bookId) {
|
||||
renderSession(view, false);
|
||||
void fetchPlaySavesForBook(bookId);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
/* 服务端会话已失效时,从作品与存档恢复 */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (saved.mode === "play") {
|
||||
await fetchPlaySavesForBook(bookId, true);
|
||||
const restored = saved.playSaveId
|
||||
? await loadPlaySave(bookId, saved.playSaveId, { silent: true })
|
||||
: false;
|
||||
if (!restored) await openCurrentPlay(bookId);
|
||||
} else {
|
||||
await openBookDesign(bookId);
|
||||
}
|
||||
if (saved.navLevel !== "book") {
|
||||
showCatalog({ closeSession: false });
|
||||
}
|
||||
return Boolean(sessionId && activeBookId === bookId);
|
||||
} catch {
|
||||
forgetWorkspaceSession();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
initColorTheme();
|
||||
initUiFontScale();
|
||||
initMarkdownRender();
|
||||
syncThemeMenuUi();
|
||||
try {
|
||||
void populateDirectorSelect();
|
||||
await Promise.all([loadBooks(), loadPersonas(), loadApiProfiles()]);
|
||||
if (books.length) {
|
||||
showCatalog();
|
||||
const restored = await restoreWorkspaceSession();
|
||||
if (!restored) showCatalog();
|
||||
} else {
|
||||
renderEmpty();
|
||||
openNewBookDialog();
|
||||
|
||||
+67
-80
@@ -10,7 +10,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css?v=20260922-menu-unify" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260928-shell-order" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
@@ -32,7 +32,64 @@
|
||||
</head>
|
||||
<body data-lifecycle="design" data-nav-level="root">
|
||||
<div class="layout">
|
||||
<aside class="panel panel-rail" id="panel-books">
|
||||
<div class="main-chrome" id="main-chrome">
|
||||
<header class="main-head" id="main-head">
|
||||
<div class="main-head-left" aria-hidden="true"></div>
|
||||
<nav class="main-app-nav" aria-label="应用">
|
||||
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
||||
<a href="/modules.html">技能</a>
|
||||
<a href="/shells.html">呈现壳</a>
|
||||
<a href="/libraries.html">库</a>
|
||||
<a href="/stats.html">Token</a>
|
||||
<a href="/settings.html">设置</a>
|
||||
</nav>
|
||||
<div class="main-head-right">
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-instance" hidden>
|
||||
保存定稿
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-play" hidden>
|
||||
保存游玩
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-export" disabled>
|
||||
导出
|
||||
</button>
|
||||
<details class="more-menu theme-menu" id="theme-menu">
|
||||
<summary class="btn-sm more-menu-sum" id="theme-menu-sum" title="主题">
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<div class="more-menu-label">主题</div>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="paper" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">📖</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">仿纸暖黄</span><span class="more-theme-desc">经典护眼</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="modern" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">✨</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">现代清爽</span><span class="more-theme-desc">干净明快</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="dark" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🌙</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">夜间暗色</span><span class="more-theme-desc">黑底灰字</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="green" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🍃</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
|
||||
</button>
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">字号</div>
|
||||
<div class="more-font-scale" role="group" aria-label="字号">
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
|
||||
<span class="more-font-value" id="theme-font-scale-value">100</span>
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="workspace-shell">
|
||||
<aside class="panel panel-rail" id="panel-books">
|
||||
<button
|
||||
type="button"
|
||||
class="rail-expand"
|
||||
@@ -81,65 +138,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
|
||||
<main class="panel panel-main">
|
||||
<div class="main-chrome" id="main-chrome">
|
||||
<header class="main-head" id="main-head">
|
||||
<div class="main-head-left" aria-hidden="true"></div>
|
||||
<nav class="main-app-nav" aria-label="应用">
|
||||
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
||||
<a href="/modules.html">技能</a>
|
||||
<a href="/shells.html">呈现壳</a>
|
||||
<a href="/libraries.html">库</a>
|
||||
<a href="/stats.html">Token</a>
|
||||
<a href="/settings.html">设置</a>
|
||||
</nav>
|
||||
<div class="main-head-right">
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-instance" hidden>
|
||||
保存定稿
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-save-play" hidden>
|
||||
保存游玩
|
||||
</button>
|
||||
<button type="button" class="btn-sm main-head-action" id="btn-export" disabled>
|
||||
导出
|
||||
</button>
|
||||
<details class="more-menu theme-menu" id="theme-menu">
|
||||
<summary class="btn-sm more-menu-sum" id="theme-menu-sum" title="主题">
|
||||
<span class="hamburger-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<div class="more-menu-label">主题</div>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="paper" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">📖</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">仿纸暖黄</span><span class="more-theme-desc">经典护眼</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="modern" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">✨</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">现代清爽</span><span class="more-theme-desc">干净明快</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="dark" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🌙</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">夜间暗色</span><span class="more-theme-desc">黑底灰字</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-color-theme="green" role="menuitem">
|
||||
<span class="more-theme-icon" aria-hidden="true">🍃</span>
|
||||
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
|
||||
</button>
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">字号</div>
|
||||
<div class="more-font-scale" role="group" aria-label="字号">
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
|
||||
<span class="more-font-value" id="theme-font-scale-value">100</span>
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div class="main-stack">
|
||||
<main class="panel panel-main">
|
||||
<div class="main-stack">
|
||||
<div class="main-body">
|
||||
<section class="panel-feed" id="panel-feed">
|
||||
<div id="skill-picker" class="skill-picker" hidden></div>
|
||||
@@ -186,30 +188,15 @@
|
||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
||||
<div id="composer-main"></div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="dialog-new-book" class="dialog">
|
||||
<form method="dialog" id="form-new-book">
|
||||
<h2>新建作品</h2>
|
||||
<p class="dialog-desc">先选进料方式,再选对应配方(两套目录互不共享)。</p>
|
||||
<label class="field">
|
||||
进料方式
|
||||
<select id="select-creation-mode" required>
|
||||
<option value="" selected disabled>请选择…</option>
|
||||
<option value="recipe">工序编排</option>
|
||||
<option value="dictate">对话落盘</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="dialog-desc" id="creation-mode-desc" hidden></p>
|
||||
<label class="field" id="field-director" hidden>
|
||||
配方
|
||||
<select id="select-director">
|
||||
<option value="">请先选择进料方式</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="dialog-desc" id="director-desc" hidden></p>
|
||||
<p class="dialog-desc">创建后直接说你想写什么;助手会在对话中判断合适的创作路径。</p>
|
||||
<label class="field">
|
||||
名称
|
||||
<input id="input-book-title" type="text" placeholder="未命名作品" maxlength="80" autofocus />
|
||||
@@ -337,6 +324,6 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js?v=20260922-menu-unify"></script>
|
||||
<script type="module" src="/app.js?v=20260928-session-restore"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -99,6 +99,55 @@ export function softenJsonText(s) {
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根对象被提前写成 `} , "下一键"` 时丢掉这个 `}`。
|
||||
* 只在 JSON.parse 失败后使用;合法对象不会出现这种根级衔接。
|
||||
*/
|
||||
export function repairPrematureRootClose(s) {
|
||||
const text = String(s || "");
|
||||
let out = "";
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (inString) {
|
||||
out += c;
|
||||
if (escaped) escaped = false;
|
||||
else if (c === "\\") escaped = true;
|
||||
else if (c === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
inString = true;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "{") {
|
||||
depth += 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (c === "}") {
|
||||
if (depth === 1 && continuesAsSiblingKey(text, i + 1)) continue;
|
||||
if (depth > 0) depth -= 1;
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function continuesAsSiblingKey(text, from) {
|
||||
let j = from;
|
||||
while (j < text.length && /\s/.test(text[j])) j += 1;
|
||||
if (text[j] !== ",") return false;
|
||||
j += 1;
|
||||
while (j < text.length && /\s/.test(text[j])) j += 1;
|
||||
return text[j] === '"';
|
||||
}
|
||||
|
||||
/** 截断的 {… 补齐引号/括号,便于验收卡仍能出 mosaic */
|
||||
export function repairTruncatedJsonObject(slice) {
|
||||
let s = String(slice || "").trim();
|
||||
@@ -138,6 +187,10 @@ export function tryParseJsonDoc(text) {
|
||||
}
|
||||
|
||||
const cleaned = softenJsonText(convertStructuralSmartQuotes(stripped));
|
||||
const repaired = repairPrematureRootClose(cleaned);
|
||||
const repairedParsed =
|
||||
tryParse(repaired) ?? tryParse(softenJsonText(repaired));
|
||||
if (repairedParsed != null) return repairedParsed;
|
||||
if (
|
||||
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||||
@@ -153,9 +206,12 @@ export function tryParseJsonDoc(text) {
|
||||
const objEnd = cleaned.lastIndexOf("}");
|
||||
if (objStart >= 0 && objEnd > objStart) {
|
||||
const slice = cleaned.slice(objStart, objEnd + 1);
|
||||
const repairedSlice = repairPrematureRootClose(slice);
|
||||
const parsed =
|
||||
tryParse(slice) ??
|
||||
tryParse(softenJsonText(slice)) ??
|
||||
tryParse(repairedSlice) ??
|
||||
tryParse(softenJsonText(repairedSlice)) ??
|
||||
tryParse(repairTruncatedJsonObject(slice)) ??
|
||||
tryParse(softenJsonText(repairTruncatedJsonObject(cleaned.slice(objStart))));
|
||||
if (parsed != null) return parsed;
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ function setTab(tab, pushUrl = true) {
|
||||
panelTitleEl.textContent = isStyle ? "文风库" : "偏好库";
|
||||
panelSubtitleEl.textContent = isStyle
|
||||
? "跨卡「怎么写」;对话 + 样本提取 → 叙事指南选用"
|
||||
: "跨卡硬约束;对话提取 →「用户需求」节点选用";
|
||||
: "跨卡硬约束;由绑定偏好库的能力按需选用";
|
||||
extractTitleEl.textContent = isStyle ? "用 LLM 提取文风" : "用 LLM 提取偏好";
|
||||
extractHelpEl.textContent = isStyle
|
||||
? "描述想要的写法,或贴样本;助手整理成可入库草稿。点「入库」写入列表。"
|
||||
|
||||
+223
-34
@@ -12,7 +12,7 @@
|
||||
--ok: #16a34a;
|
||||
--warn: #ca8a04;
|
||||
--danger: #dc2626;
|
||||
--books-w: 216px;
|
||||
--books-w: clamp(160px, 17vw, 196px);
|
||||
--books-w-collapsed: 40px;
|
||||
--coord-w: clamp(280px, 24vw, 340px);
|
||||
--coord-w-collapsed: 40px;
|
||||
@@ -113,13 +113,21 @@ a.link:hover { text-decoration: underline; }
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: var(--books-w) 1fr;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
height: 100%;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.workspace-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--books-w) minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel { background: var(--surface); border: 1px solid var(--border); min-height: 0; }
|
||||
.panel-rail,
|
||||
.panel-books {
|
||||
@@ -324,10 +332,10 @@ body[data-nav-level="root"] .burst-badge { display: none; }
|
||||
|
||||
body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
/* composer æÂÂÃ¥ÂÂ
容èªéÂÂåºÂï¼Âä¸Âé´åºåÂÂæÂÂå©ä½Âé«Â度ï¼ÂæÂ´é¡µéÂÂå¨è§Âå£åÂÂ
*/
|
||||
/* panel-main:上=常驻顶栏;下=工作区 */
|
||||
/* panel-main 只承载工作区;全局顶栏位于 workspace-shell 上方 */
|
||||
.panel-main {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -336,6 +344,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
}
|
||||
|
||||
.main-stack {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
@@ -2357,8 +2366,38 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
@media (hover: none) {
|
||||
.explorer-del { opacity: 0.7; }
|
||||
}
|
||||
.explorer-action .explorer-name { color: var(--accent); font-weight: 500; }
|
||||
.book-list > .explorer-action { margin-top: 4px; }
|
||||
.explorer-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 0 2px 8px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.explorer-add.child { margin-left: 28px; }
|
||||
.explorer-add:hover {
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
}
|
||||
.explorer-add:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.explorer-add:disabled {
|
||||
color: var(--muted);
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.explorer-add:disabled:hover { background: transparent; color: var(--muted); }
|
||||
.explorer-design .explorer-name { font-weight: 500; }
|
||||
.book-list > .explorer-design + .explorer-hint { margin-top: 2px; }
|
||||
.book-enter-pop {
|
||||
@@ -2527,6 +2566,14 @@ body.rail-collapsed .message-feed {
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 用户气泡不再单独占一行显示时间;操作菜单叠在右下角,不挤正文 */
|
||||
.msg-user-row .msg-foot {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.msg-head-main { display: flex; gap: 6px; align-items: baseline; min-width: 0; flex: 1; }
|
||||
.msg-subtitle { color: var(--muted); font-weight: normal; }
|
||||
.msg-variant-badge {
|
||||
@@ -2615,7 +2662,7 @@ body.rail-collapsed .message-feed {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.msg-user-row .msg-body { padding-bottom: 4px; }
|
||||
.msg-user-row .msg-body { padding-bottom: 8px; }
|
||||
.msg-assistant-row .msg-head + .msg-body { padding-top: 4px; }
|
||||
.msg-pending { opacity: 0.85; }
|
||||
|
||||
@@ -3261,8 +3308,8 @@ body[data-lifecycle="play"] .msg-assistant-row .msg-head {
|
||||
padding-top: 6px;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
|
||||
width: min(100%, 28rem);
|
||||
max-width: 90%;
|
||||
width: min(100%, 42rem);
|
||||
max-width: 92%;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .msg-bubble {
|
||||
width: 100%;
|
||||
@@ -5812,11 +5859,33 @@ menu.context-menu[hidden],
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
.composer-shell-actions {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 4px;
|
||||
z-index: 3;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-bottom: 2px;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.14s ease;
|
||||
}
|
||||
.composer-input-shell:hover > .composer-shell-actions,
|
||||
.composer-shell-actions:focus-within {
|
||||
opacity: 0.8;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.composer-shell-actions:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
@media (hover: none) {
|
||||
.composer-shell-actions {
|
||||
opacity: 0.8;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
.persona-picker-host {
|
||||
display: flex;
|
||||
@@ -6264,11 +6333,87 @@ menu.context-menu[hidden],
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: calc(100% + 6px);
|
||||
min-width: 220px;
|
||||
max-width: min(80vw, 280px);
|
||||
min-width: 240px;
|
||||
max-width: min(86vw, 340px);
|
||||
/* visible:二级 API 菜单向右飞出,避免被 overflow:auto 裁切 */
|
||||
overflow: visible;
|
||||
z-index: 140;
|
||||
}
|
||||
.api-fold {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.api-fold-sum {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.api-fold-sum .more-theme-copy {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.api-fold-sum::-webkit-details-marker { display: none; }
|
||||
.api-fold-sum::marker { content: ""; }
|
||||
.api-fold-chevron {
|
||||
flex: 0 0 auto;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
.api-fold-chevron::before { content: "›"; }
|
||||
.api-fold[open] > .api-fold-sum .api-fold-chevron,
|
||||
.api-fold-sum:hover .api-fold-chevron {
|
||||
color: var(--accent);
|
||||
}
|
||||
.api-fold-sum:hover,
|
||||
.api-fold[open] > .api-fold-sum {
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
}
|
||||
.api-fold-list {
|
||||
position: absolute;
|
||||
left: calc(100% + 4px);
|
||||
top: 0;
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 220px;
|
||||
max-width: min(80vw, 300px);
|
||||
max-height: min(60vh, 420px);
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.api-fold-list {
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
margin: 2px 0 4px 8px;
|
||||
padding: 2px 0 4px 6px;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
max-height: 240px;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
.api-fold-chevron::before { content: "▾"; }
|
||||
.api-fold[open] > .api-fold-sum .api-fold-chevron::before { content: "▴"; }
|
||||
}
|
||||
.api-fold-sum:focus-visible,
|
||||
.api-fold-list .more-profile-item:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.more-submenu {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -6362,6 +6507,59 @@ menu.context-menu[hidden],
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
.more-profile-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
.more-profile-row .more-profile-item {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.more-profile-forget {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.more-profile-forget:hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg);
|
||||
}
|
||||
.more-profile-forget:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
/* more-profile-models:与 more-submenu 默认向右飞出一致,不再强制向下展开 */
|
||||
.more-model-filter {
|
||||
margin: 2px 4px 6px;
|
||||
width: calc(100% - 8px);
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
.more-model-filter:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.more-model-empty {
|
||||
margin: 0;
|
||||
padding: 6px 10px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.more-menu-empty {
|
||||
margin: 0;
|
||||
padding: 8px 10px 10px;
|
||||
@@ -7123,10 +7321,9 @@ menu.context-menu[hidden],
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
.dictate-products-empty-block {
|
||||
margin: 28px 16px;
|
||||
@@ -7160,9 +7357,11 @@ menu.context-menu[hidden],
|
||||
border-bottom: none;
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
@@ -7173,29 +7372,20 @@ menu.context-menu[hidden],
|
||||
cursor: context-menu;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.dictate-product-sum-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 10px 8px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
pointer-events: none; /* 整行命中落在 button 上,避免子节点吞事件 */
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum:hover .dictate-product-sum-inner {
|
||||
.dictate-product--rail > .dictate-product-sum:hover {
|
||||
background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
|
||||
}
|
||||
.dictate-product--rail > .dictate-product-sum:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.dictate-product--rail.is-open > .dictate-product-sum .dictate-product-sum-inner {
|
||||
.dictate-product--rail.is-open > .dictate-product-sum {
|
||||
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
@@ -7256,7 +7446,6 @@ menu.context-menu[hidden],
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.dictate-product--rail .dictate-product-preview.is-empty {
|
||||
font-style: italic;
|
||||
@@ -7304,7 +7493,7 @@ menu.context-menu[hidden],
|
||||
max-width: 100%;
|
||||
}
|
||||
body[data-coord-rail-mode="products"] .coord-drawer-body {
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user