会话接通游玩期偏好审核,并打磨主界面菜单与消息窥视交互。
开场落盘时同步抬升名片到主角设定;有分支变体也可重滚。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -215,13 +215,15 @@ export function isRefreshableMessage(msg: BranchableMessage): boolean {
|
|||||||
return kind === "worker_questions" || kind === "worker_output";
|
return kind === "worker_questions" || kind === "worker_output";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 与 refreshMessage 同一套门槛:类型对,且有 worker 快照或该条检查点。 */
|
/** 与 refreshMessage 同一套门槛:类型对,且有可回滚快照。 */
|
||||||
export function canAttemptRefresh(
|
export function canAttemptRefresh(
|
||||||
msg: BranchableMessage,
|
msg: BranchableMessage,
|
||||||
opts: {
|
opts: {
|
||||||
lastWorkerId?: string;
|
lastWorkerId?: string;
|
||||||
hasLastWorkerSnapshot: boolean;
|
hasLastWorkerSnapshot: boolean;
|
||||||
hasMessageCheckpoint: boolean;
|
hasMessageCheckpoint: boolean;
|
||||||
|
/** 消息已在分支里(曾重 roll / swipe) */
|
||||||
|
hasBranchVariants?: boolean;
|
||||||
},
|
},
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isRefreshableMessage(msg)) return false;
|
if (!isRefreshableMessage(msg)) return false;
|
||||||
@@ -229,6 +231,7 @@ export function canAttemptRefresh(
|
|||||||
(typeof msg.actor === "string" && msg.actor.trim()) ||
|
(typeof msg.actor === "string" && msg.actor.trim()) ||
|
||||||
opts.lastWorkerId?.trim();
|
opts.lastWorkerId?.trim();
|
||||||
if (!workerId) return false;
|
if (!workerId) return false;
|
||||||
|
if (opts.hasBranchVariants) return true;
|
||||||
return opts.hasMessageCheckpoint || opts.hasLastWorkerSnapshot;
|
return opts.hasMessageCheckpoint || opts.hasLastWorkerSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+237
-24
@@ -20,12 +20,17 @@ import {
|
|||||||
OPENING_OUTPUT_TAG,
|
OPENING_OUTPUT_TAG,
|
||||||
OPENING_PERSONA_CHOICE_TAG,
|
OPENING_PERSONA_CHOICE_TAG,
|
||||||
OPENING_PERSONA_TAG,
|
OPENING_PERSONA_TAG,
|
||||||
|
OPENING_PRODUCT_FAMILY,
|
||||||
SLOT_OPENING_SELECTED_INDEX,
|
SLOT_OPENING_SELECTED_INDEX,
|
||||||
parseOpeningSealPayload,
|
parseOpeningSealPayload,
|
||||||
readSealedOpeningPersona,
|
readSealedOpeningPersona,
|
||||||
serializeOpeningPersona,
|
serializeOpeningPersona,
|
||||||
splitOpeningDocument,
|
splitOpeningDocument,
|
||||||
} from "../skills/opening-seal.js";
|
} from "../skills/opening-seal.js";
|
||||||
|
import { buildPlayAuxView, collectOpenings } from "../skills/play-aux.js";
|
||||||
|
import {
|
||||||
|
dictateProductFamily,
|
||||||
|
} from "../dictate/repeatable-tags.js";
|
||||||
import {
|
import {
|
||||||
PLAY_WORKING_SNAPSHOT_ID,
|
PLAY_WORKING_SNAPSHOT_ID,
|
||||||
PLAY_WORKING_SNAPSHOT_LABEL,
|
PLAY_WORKING_SNAPSHOT_LABEL,
|
||||||
@@ -45,6 +50,16 @@ import {
|
|||||||
reloadDefaultMainAgentLlm,
|
reloadDefaultMainAgentLlm,
|
||||||
type LlmTrackingRef,
|
type LlmTrackingRef,
|
||||||
} from "../runtime/llm-factory.js";
|
} from "../runtime/llm-factory.js";
|
||||||
|
import {
|
||||||
|
acceptPreferenceCandidate,
|
||||||
|
collectPreferenceCandidates,
|
||||||
|
formatRecentDialogueForCollect,
|
||||||
|
} from "../preference/index.js";
|
||||||
|
import {
|
||||||
|
loadAppSettings,
|
||||||
|
normalizeContextTraceKeepLatest,
|
||||||
|
normalizePreferenceCollectEveryTurns,
|
||||||
|
} from "../config/settings.js";
|
||||||
import { isAbortError, runWithAbortSignal } from "../llm/run-abort.js";
|
import { isAbortError, runWithAbortSignal } from "../llm/run-abort.js";
|
||||||
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
|
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
|
||||||
import type { PersistedBookSession } from "../types/book-session.js";
|
import type { PersistedBookSession } from "../types/book-session.js";
|
||||||
@@ -78,10 +93,6 @@ import {
|
|||||||
sessionSkillPackId,
|
sessionSkillPackId,
|
||||||
skillPacksMatch,
|
skillPacksMatch,
|
||||||
} from "../book/skill-id.js";
|
} from "../book/skill-id.js";
|
||||||
import {
|
|
||||||
loadAppSettings,
|
|
||||||
normalizeContextTraceKeepLatest,
|
|
||||||
} from "../config/settings.js";
|
|
||||||
import {
|
import {
|
||||||
pruneContextTraces,
|
pruneContextTraces,
|
||||||
type LlmContextTrace,
|
type LlmContextTrace,
|
||||||
@@ -183,10 +194,16 @@ import {
|
|||||||
personaForLifecycle,
|
personaForLifecycle,
|
||||||
} from "../persona/store.js";
|
} from "../persona/store.js";
|
||||||
import {
|
import {
|
||||||
|
DICTATE_OPENING_REPEATABLE_FAMILIES,
|
||||||
|
ensureProtagonistFromPersona,
|
||||||
|
formatPersonaDescription,
|
||||||
overlayPlayPersona,
|
overlayPlayPersona,
|
||||||
|
parseProtagonistSpec,
|
||||||
|
personaBoundToOpening,
|
||||||
|
PROTAGONIST_TAG,
|
||||||
|
protagonistTagForOpeningSlot,
|
||||||
readSessionProtagonist,
|
readSessionProtagonist,
|
||||||
} from "../persona/protagonist-bind.js";
|
} from "../persona/protagonist-bind.js";
|
||||||
import { buildPlayAuxView } from "../skills/play-aux.js";
|
|
||||||
import type { TableCellPatch } from "../blackboard/table-cells.js";
|
import type { TableCellPatch } from "../blackboard/table-cells.js";
|
||||||
import {
|
import {
|
||||||
appendBranchVariant,
|
appendBranchVariant,
|
||||||
@@ -336,6 +353,8 @@ export type SessionView = {
|
|||||||
openingPersonaChoice?: "opening" | "global" | null;
|
openingPersonaChoice?: "opening" | "global" | null;
|
||||||
/** 游玩辅助 Tab:表格 / 开场与角色 */
|
/** 游玩辅助 Tab:表格 / 开场与角色 */
|
||||||
playAux?: import("../skills/play-aux.js").PlayAuxView;
|
playAux?: import("../skills/play-aux.js").PlayAuxView;
|
||||||
|
/** 待审核偏好候选:展示时可编辑,收下 / 不要 */
|
||||||
|
preferenceReview?: Array<{ id: string; content: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SessionAction =
|
export type SessionAction =
|
||||||
@@ -425,6 +444,10 @@ type ManagedSession = {
|
|||||||
runGen?: number;
|
runGen?: number;
|
||||||
/** 本轮开始时的消息条数,取消时裁掉执行中产生的调度句 */
|
/** 本轮开始时的消息条数,取消时裁掉执行中产生的调度句 */
|
||||||
runMessageCutoff?: number;
|
runMessageCutoff?: number;
|
||||||
|
/** 本局游玩已完成的用户回合数(偏好采集计数;不进快照) */
|
||||||
|
preferencePlayTurns?: number;
|
||||||
|
/** 待审核的偏好候选(可编辑后收下/不要) */
|
||||||
|
preferenceReview?: import("../preference/collect.js").PreferenceCandidate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
@@ -822,9 +845,12 @@ export class SessionManager {
|
|||||||
skillName: s.runtime.getActiveSkill()?.name,
|
skillName: s.runtime.getActiveSkill()?.name,
|
||||||
moduleCatalog: s.moduleCatalog,
|
moduleCatalog: s.moduleCatalog,
|
||||||
});
|
});
|
||||||
const repeatableFamilies = (s.moduleCatalog?.modules ?? [])
|
const repeatableFamilies = [
|
||||||
|
...(s.moduleCatalog?.modules ?? [])
|
||||||
.filter((m) => m.repeatable === true && m.artifact?.trim())
|
.filter((m) => m.repeatable === true && m.artifact?.trim())
|
||||||
.map((m) => m.artifact!.trim());
|
.map((m) => m.artifact!.trim()),
|
||||||
|
...DICTATE_OPENING_REPEATABLE_FAMILIES,
|
||||||
|
];
|
||||||
|
|
||||||
const result = await runDictateTurn({
|
const result = await runDictateTurn({
|
||||||
llm,
|
llm,
|
||||||
@@ -837,6 +863,22 @@ export class SessionManager {
|
|||||||
listProducts: () => this.listDictateProducts(s),
|
listProducts: () => this.listDictateProducts(s),
|
||||||
writeProduct: (tag, content, order) => {
|
writeProduct: (tag, content, order) => {
|
||||||
const board = s.runtime.getBlackboard();
|
const board = s.runtime.getBlackboard();
|
||||||
|
let writeContent = content;
|
||||||
|
if (dictateProductFamily(tag) === OPENING_PRODUCT_FAMILY) {
|
||||||
|
const split = splitOpeningDocument(content);
|
||||||
|
if (split.persona) {
|
||||||
|
ensureProtagonistFromPersona(
|
||||||
|
board,
|
||||||
|
split.persona,
|
||||||
|
protagonistTagForOpeningSlot(tag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 仅有用户角色 meta、无叙事:抬到主角设定,不污染开场白
|
||||||
|
if (split.persona && !split.text.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeContent = split.text.trim() || content;
|
||||||
|
}
|
||||||
const existing = board.getLatestByTag(tag);
|
const existing = board.getLatestByTag(tag);
|
||||||
const prev = existing?.metadata?.[DICTATE_ORDER_META_KEY];
|
const prev = existing?.metadata?.[DICTATE_ORDER_META_KEY];
|
||||||
const resolved =
|
const resolved =
|
||||||
@@ -847,7 +889,7 @@ export class SessionManager {
|
|||||||
: defaultDictateOrder(tag);
|
: defaultDictateOrder(tag);
|
||||||
board.write({
|
board.write({
|
||||||
tag,
|
tag,
|
||||||
content,
|
content: writeContent,
|
||||||
source: "agent",
|
source: "agent",
|
||||||
metadata: {
|
metadata: {
|
||||||
...(existing?.metadata ?? {}),
|
...(existing?.metadata ?? {}),
|
||||||
@@ -1190,6 +1232,8 @@ export class SessionManager {
|
|||||||
s.messages = [];
|
s.messages = [];
|
||||||
}
|
}
|
||||||
s.branchState = createMessageBranchState();
|
s.branchState = createMessageBranchState();
|
||||||
|
s.preferencePlayTurns = 0;
|
||||||
|
s.preferenceReview = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 开玩时记下是否用这条开场自带的用户角色;须在恢复快照之后写,否则会被冲掉。 */
|
/** 开玩时记下是否用这条开场自带的用户角色;须在恢复快照之后写,否则会被冲掉。 */
|
||||||
@@ -1207,7 +1251,7 @@ export class SessionManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 保证黑板上有 输出.开场白,供开玩主区展示。
|
* 保证黑板上有 输出.开场白,供开玩主区展示。
|
||||||
* 优先级:设计.开场白(转述产物)> 已有 输出.开场白 > 其它产物拼装 > 非转述聊天兜底。
|
* 优先级:选中的设计.开场白[#槽] / 开场白与开场变量候选 > 已有 输出.开场白 > 其它产物拼装。
|
||||||
* 切勿用 dictate_reply(常含确认/建议补充)盖过已钉的设计.开场白。
|
* 切勿用 dictate_reply(常含确认/建议补充)盖过已钉的设计.开场白。
|
||||||
* 返回值已展开 @ 指令(进游玩时绑定当前用户角色 / 掷骰)。
|
* 返回值已展开 @ 指令(进游玩时绑定当前用户角色 / 掷骰)。
|
||||||
*/
|
*/
|
||||||
@@ -1217,13 +1261,52 @@ export class SessionManager {
|
|||||||
): string {
|
): string {
|
||||||
this.sealOpeningPersonaFromBoard(s);
|
this.sealOpeningPersonaFromBoard(s);
|
||||||
const board = s.runtime.getBlackboard();
|
const board = s.runtime.getBlackboard();
|
||||||
const fromDesign =
|
const openings = collectOpenings(board);
|
||||||
board.getContentByTag("设计.开场白")?.trim() ||
|
const selectedRaw = s.runtime.getSession().slots[SLOT_OPENING_SELECTED_INDEX];
|
||||||
this.listDictateProducts(s).find((p) => p.tag === "设计.开场白")
|
const selectedIndex =
|
||||||
?.content?.trim() ||
|
typeof selectedRaw === "number"
|
||||||
"";
|
? selectedRaw
|
||||||
if (fromDesign) {
|
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||||||
const body = splitOpeningDocument(fromDesign).text || fromDesign;
|
? Number(selectedRaw)
|
||||||
|
: 0;
|
||||||
|
const picked =
|
||||||
|
openings.length > 0
|
||||||
|
? openings[
|
||||||
|
Math.min(
|
||||||
|
Math.max(0, Math.trunc(selectedIndex) || 0),
|
||||||
|
openings.length - 1,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (picked) {
|
||||||
|
if (picked.persona) {
|
||||||
|
ensureProtagonistFromPersona(
|
||||||
|
board,
|
||||||
|
picked.persona,
|
||||||
|
protagonistTagForOpeningSlot(picked.tag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const body = splitOpeningDocument(
|
||||||
|
board.getContentByTag(picked.tag) ?? picked.text,
|
||||||
|
).text.trim() || picked.text.trim();
|
||||||
|
if (body) {
|
||||||
|
// 若叙事与黑板原文不同(剥了 meta),回写该开场 tag
|
||||||
|
const rawOnBoard = board.getContentByTag(picked.tag)?.trim() ?? "";
|
||||||
|
if (
|
||||||
|
dictateProductFamily(picked.tag) === OPENING_PRODUCT_FAMILY &&
|
||||||
|
rawOnBoard &&
|
||||||
|
body !== rawOnBoard &&
|
||||||
|
splitOpeningDocument(rawOnBoard).persona
|
||||||
|
) {
|
||||||
|
const existingDesign = board.getLatestByTag(picked.tag);
|
||||||
|
board.write({
|
||||||
|
tag: picked.tag,
|
||||||
|
content: body,
|
||||||
|
source: "runtime",
|
||||||
|
metadata: existingDesign?.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
const expanded = this.expandDirectivesText(
|
const expanded = this.expandDirectivesText(
|
||||||
body,
|
body,
|
||||||
this.playDirectiveContext(s),
|
this.playDirectiveContext(s),
|
||||||
@@ -1238,6 +1321,10 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
|
if (picked.persona) {
|
||||||
|
return board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existing = board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
const existing = board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -1274,12 +1361,13 @@ export class SessionManager {
|
|||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 开场 meta 里的用户角色:落到 运行.开场用户角色,不进全局列表。 */
|
/** 选中开场绑定的用户角色 → 运行.开场用户角色,并补写同槽主角设定。 */
|
||||||
private sealOpeningPersonaFromBoard(s: ManagedSession): void {
|
private sealOpeningPersonaFromBoard(s: ManagedSession): void {
|
||||||
const board = s.runtime.getBlackboard();
|
const board = s.runtime.getBlackboard();
|
||||||
if (readSealedOpeningPersona(board.getContentByTag(OPENING_PERSONA_TAG))) {
|
if (readSealedOpeningPersona(board.getContentByTag(OPENING_PERSONA_TAG))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const openings = collectOpenings(board);
|
||||||
const selectedRaw = s.runtime.getSession().slots[SLOT_OPENING_SELECTED_INDEX];
|
const selectedRaw = s.runtime.getSession().slots[SLOT_OPENING_SELECTED_INDEX];
|
||||||
const selectedIndex =
|
const selectedIndex =
|
||||||
typeof selectedRaw === "number"
|
typeof selectedRaw === "number"
|
||||||
@@ -1287,17 +1375,44 @@ export class SessionManager {
|
|||||||
: typeof selectedRaw === "string" && selectedRaw.trim()
|
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||||||
? Number(selectedRaw)
|
? Number(selectedRaw)
|
||||||
: 0;
|
: 0;
|
||||||
|
const picked =
|
||||||
|
openings.length > 0
|
||||||
|
? openings[
|
||||||
|
Math.min(
|
||||||
|
Math.max(0, Math.trunc(selectedIndex) || 0),
|
||||||
|
openings.length - 1,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
const fromPicked = picked
|
||||||
|
? personaBoundToOpening(
|
||||||
|
board,
|
||||||
|
picked.tag,
|
||||||
|
board.getContentByTag(picked.tag) ?? picked.text,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const protagonist = parseProtagonistSpec(
|
||||||
|
board.getContentByTag(PROTAGONIST_TAG),
|
||||||
|
);
|
||||||
const persona =
|
const persona =
|
||||||
parseOpeningSealPayload(
|
fromPicked ??
|
||||||
board.getContentByTag("设计.开场白与开场变量"),
|
(protagonist
|
||||||
selectedIndex,
|
? {
|
||||||
)?.persona ?? splitOpeningDocument(board.getContentByTag("设计.开场白")).persona;
|
name: protagonist.name,
|
||||||
|
description: formatPersonaDescription(protagonist),
|
||||||
|
}
|
||||||
|
: null);
|
||||||
if (!persona) return;
|
if (!persona) return;
|
||||||
board.write({
|
board.write({
|
||||||
tag: OPENING_PERSONA_TAG,
|
tag: OPENING_PERSONA_TAG,
|
||||||
content: serializeOpeningPersona(persona),
|
content: serializeOpeningPersona(persona),
|
||||||
source: "runtime",
|
source: "runtime",
|
||||||
});
|
});
|
||||||
|
ensureProtagonistFromPersona(
|
||||||
|
board,
|
||||||
|
persona,
|
||||||
|
picked ? protagonistTagForOpeningSlot(picked.tag) : PROTAGONIST_TAG,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private personaForManaged(s: ManagedSession): ReturnType<
|
private personaForManaged(s: ManagedSession): ReturnType<
|
||||||
@@ -1366,8 +1481,10 @@ export class SessionManager {
|
|||||||
private deriveOpeningFromDictateProducts(s: ManagedSession): string {
|
private deriveOpeningFromDictateProducts(s: ManagedSession): string {
|
||||||
const products = this.listDictateProducts(s);
|
const products = this.listDictateProducts(s);
|
||||||
if (!products.length) return "";
|
if (!products.length) return "";
|
||||||
// 开场白是「按正文格式生成的内容」,优先设计.开场白;不要把设计.正文组成(格式)当开场白
|
const opening =
|
||||||
const opening = products.find((p) => p.tag === "设计.开场白")?.content?.trim();
|
products.find((p) => p.tag === "设计.开场白")?.content?.trim() ||
|
||||||
|
products.find((p) => dictateProductFamily(p.tag) === OPENING_PRODUCT_FAMILY)
|
||||||
|
?.content?.trim();
|
||||||
if (opening) return opening;
|
if (opening) return opening;
|
||||||
const preferred = products.filter(
|
const preferred = products.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
@@ -1860,11 +1977,90 @@ export class SessionManager {
|
|||||||
console.error("[会话] 发送消息失败", detail);
|
console.error("[会话] 发送消息失败", detail);
|
||||||
await this.recoverFailedRun(s, detail);
|
await this.recoverFailedRun(s, detail);
|
||||||
}
|
}
|
||||||
|
await this.maybeCollectPreferencesAfterPlayTurn(s, {
|
||||||
|
countedPlayInput: Boolean(
|
||||||
|
waitingBefore?.kind === "input" &&
|
||||||
|
isPlayLayerActive(s.runtime.getSession().slots) &&
|
||||||
|
s.runtime.getSession().waitingReason?.kind === "input",
|
||||||
|
),
|
||||||
|
});
|
||||||
this.syncCreationDialogue(s);
|
this.syncCreationDialogue(s);
|
||||||
this.persist(s);
|
this.persist(s);
|
||||||
return this.toView(id);
|
return this.toView(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游玩回合结束后:计数;到期则采集偏好候选(不改本局用户约束)。
|
||||||
|
*/
|
||||||
|
private async maybeCollectPreferencesAfterPlayTurn(
|
||||||
|
s: ManagedSession,
|
||||||
|
opts: { countedPlayInput: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
if (!opts.countedPlayInput) return;
|
||||||
|
if (!isPlayLayerActive(s.runtime.getSession().slots)) return;
|
||||||
|
if (s.preferenceReview?.length) return;
|
||||||
|
|
||||||
|
const every = normalizePreferenceCollectEveryTurns(
|
||||||
|
loadAppSettings().preferenceCollectEveryTurns,
|
||||||
|
);
|
||||||
|
if (every <= 0) return;
|
||||||
|
|
||||||
|
const turns = (s.preferencePlayTurns ?? 0) + 1;
|
||||||
|
s.preferencePlayTurns = turns;
|
||||||
|
if (turns % every !== 0) return;
|
||||||
|
if (!hasRealLlmConfig()) return;
|
||||||
|
|
||||||
|
const dialogue = formatRecentDialogueForCollect(s.messages);
|
||||||
|
if (!dialogue.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const llm = createDefaultMainAgentLlm(s.trackingRef, () =>
|
||||||
|
this.personaForManaged(s),
|
||||||
|
);
|
||||||
|
const candidates = await collectPreferenceCandidates({
|
||||||
|
llm,
|
||||||
|
recentDialogue: dialogue,
|
||||||
|
});
|
||||||
|
if (candidates.length) {
|
||||||
|
s.preferenceReview = candidates;
|
||||||
|
s.messages.push(
|
||||||
|
this.msg(
|
||||||
|
"system",
|
||||||
|
`[偏好] 发现 ${candidates.length} 条可记入偏好库的候选,请在卡片上编辑后收下或不要。`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
"[偏好] 采集失败",
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核偏好候选:收下(可改正文)/ 不要。不改本局「设计.用户约束」。 */
|
||||||
|
resolvePreferenceReview(
|
||||||
|
sessionId: string,
|
||||||
|
candidateId: string,
|
||||||
|
action: "accept" | "reject",
|
||||||
|
content?: string,
|
||||||
|
): SessionView {
|
||||||
|
const s = this.require(sessionId);
|
||||||
|
const list = s.preferenceReview ?? [];
|
||||||
|
const idx = list.findIndex((c) => c.id === candidateId);
|
||||||
|
if (idx < 0) throw new Error("没有这条待审偏好");
|
||||||
|
const cur = list[idx]!;
|
||||||
|
if (action === "accept") {
|
||||||
|
const body = (content ?? cur.content).trim();
|
||||||
|
if (!body) throw new Error("偏好内容不能为空");
|
||||||
|
acceptPreferenceCandidate(body);
|
||||||
|
}
|
||||||
|
const next = list.filter((c) => c.id !== candidateId);
|
||||||
|
s.preferenceReview = next.length ? next : undefined;
|
||||||
|
if (s.bookId) this.persist(s);
|
||||||
|
return this.toView(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结构化追问作答:UI 可省略结构,发给 AI 必须含问+答。
|
* 结构化追问作答:UI 可省略结构,发给 AI 必须含问+答。
|
||||||
* displayText 进气泡;aiText 进 runtime / 黑板。
|
* displayText 进气泡;aiText 进 runtime / 黑板。
|
||||||
@@ -2045,7 +2241,15 @@ export class SessionManager {
|
|||||||
const workerId = target.actor ?? workerSnap?.workerId;
|
const workerId = target.actor ?? workerSnap?.workerId;
|
||||||
if (!workerId) throw new Error("无法识别要刷新的 Worker");
|
if (!workerId) throw new Error("无法识别要刷新的 Worker");
|
||||||
|
|
||||||
const checkpoint = workerSnap ?? s.branchState.preMessageCheckpoints[idx];
|
const groupId = target.branchGroupId ?? target.id;
|
||||||
|
const existingBranch = s.branchState.branches[groupId];
|
||||||
|
const branchVariantCp =
|
||||||
|
existingBranch?.variants[existingBranch.activeIndex]?.checkpoint;
|
||||||
|
|
||||||
|
const checkpoint =
|
||||||
|
workerSnap ??
|
||||||
|
s.branchState.preMessageCheckpoints[idx] ??
|
||||||
|
branchVariantCp;
|
||||||
if (!checkpoint) {
|
if (!checkpoint) {
|
||||||
throw new Error("缺少重 roll 快照,请尝试编辑上一条用户消息");
|
throw new Error("缺少重 roll 快照,请尝试编辑上一条用户消息");
|
||||||
}
|
}
|
||||||
@@ -3547,6 +3751,12 @@ export class SessionManager {
|
|||||||
hasMessageCheckpoint: Boolean(
|
hasMessageCheckpoint: Boolean(
|
||||||
s.branchState.preMessageCheckpoints[idx],
|
s.branchState.preMessageCheckpoints[idx],
|
||||||
),
|
),
|
||||||
|
hasBranchVariants: Boolean(
|
||||||
|
(typeof m.branchTotal === "number" && m.branchTotal > 1) ||
|
||||||
|
(m.branchGroupId &&
|
||||||
|
(s.branchState.branches[m.branchGroupId]?.variants.length ?? 0) >
|
||||||
|
1),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -3675,6 +3885,9 @@ export class SessionManager {
|
|||||||
s.runtime.getBlackboard(),
|
s.runtime.getBlackboard(),
|
||||||
s.runtime.getSession().slots,
|
s.runtime.getSession().slots,
|
||||||
),
|
),
|
||||||
|
preferenceReview: s.preferenceReview?.length
|
||||||
|
? s.preferenceReview.map((c) => ({ id: c.id, content: c.content }))
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,35 @@ const server = createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && sub === "/preferences/review") {
|
||||||
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
|
candidateId?: string;
|
||||||
|
action?: "accept" | "reject";
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
const candidateId =
|
||||||
|
typeof body.candidateId === "string" ? body.candidateId.trim() : "";
|
||||||
|
const action = body.action;
|
||||||
|
if (!candidateId || (action !== "accept" && action !== "reject")) {
|
||||||
|
json(res, 400, { error: "需要 candidateId 与 action=accept|reject" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const view = sessionManager.resolvePreferenceReview(
|
||||||
|
sessionId,
|
||||||
|
candidateId,
|
||||||
|
action,
|
||||||
|
typeof body.content === "string" ? body.content : undefined,
|
||||||
|
);
|
||||||
|
json(res, 200, view);
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : "审核失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && sub === "/dictate/clear-dialogue") {
|
if (req.method === "POST" && sub === "/dictate/clear-dialogue") {
|
||||||
try {
|
try {
|
||||||
const view = await sessionManager.clearDictateDialogue(sessionId);
|
const view = await sessionManager.clearDictateDialogue(sessionId);
|
||||||
|
|||||||
+304
-76
@@ -174,15 +174,21 @@ function canEditMessage(msg) {
|
|||||||
return msg.role === "user";
|
return msg.role === "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 是否显示重 roll(↻ / 末页 ›)。已有多版时即使服务端 canRefresh=false 也允许再点。 */
|
||||||
function canRefreshMessage(msg) {
|
function canRefreshMessage(msg) {
|
||||||
if (!msg?.id) return false;
|
if (!msg?.id) return false;
|
||||||
if (typeof msg.canRefresh === "boolean") return msg.canRefresh;
|
|
||||||
if (msg.role === "user") return false;
|
if (msg.role === "user") return false;
|
||||||
const kind = msg.kind ?? "system_info";
|
const kind = msg.kind ?? "system_info";
|
||||||
return kind === "worker_questions" || kind === "worker_output";
|
const kindOk = kind === "worker_questions" || kind === "worker_output";
|
||||||
|
if (!kindOk) return false;
|
||||||
|
const total = Math.max(1, Number(msg.branchTotal) || 1);
|
||||||
|
// 已经 swipe / 重 roll 出过多版 → 再点应仍可生成
|
||||||
|
if (total > 1) return true;
|
||||||
|
if (typeof msg.canRefresh === "boolean") return msg.canRefresh;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ‹ n/total › 仅在有多版时出现;↻ 仅在这条真能重跑时出现 */
|
/** ‹ n/total › 多版切换;末页 › 与 ↻ 均可重 roll;单版只显示 ↻ */
|
||||||
function renderVariantNavHtml(msg) {
|
function renderVariantNavHtml(msg) {
|
||||||
if (!msg?.id) return "";
|
if (!msg?.id) return "";
|
||||||
const total = Math.max(1, Number(msg.branchTotal) || 1);
|
const total = Math.max(1, Number(msg.branchTotal) || 1);
|
||||||
@@ -191,15 +197,21 @@ function renderVariantNavHtml(msg) {
|
|||||||
const showPager = total > 1;
|
const showPager = total > 1;
|
||||||
if (!showPager && !refreshable) return "";
|
if (!showPager && !refreshable) return "";
|
||||||
const id = esc(msg.id);
|
const id = esc(msg.id);
|
||||||
|
const atLast = index >= total;
|
||||||
|
// 末页 ›:可重 roll 时改为 refresh;不可则禁用
|
||||||
|
const nextAction = atLast && refreshable ? "refresh" : "variant-next";
|
||||||
|
const nextDisabled = atLast && !refreshable;
|
||||||
|
const nextTitle = atLast && refreshable ? "已是最新版,再点重出一版" : "下一版";
|
||||||
|
const nextAria = atLast && refreshable ? "重出一版" : "下一版";
|
||||||
const pager = showPager
|
const pager = showPager
|
||||||
? `<button type="button" class="msg-action" data-msg-action="variant-prev" data-msg-id="${id}"${index <= 1 ? " disabled" : ""} aria-label="上一版">‹</button>
|
? `<button type="button" class="msg-action" data-msg-action="variant-prev" data-msg-id="${id}"${index <= 1 ? " disabled" : ""} aria-label="上一版">‹</button>
|
||||||
<span class="msg-variant-count">${index}/${total}</span>
|
<span class="msg-variant-count">${index}/${total}</span>
|
||||||
<button type="button" class="msg-action" data-msg-action="variant-next" data-msg-id="${id}"${index >= total ? " disabled" : ""} aria-label="下一版">›</button>`
|
<button type="button" class="msg-action${atLast && refreshable ? " msg-action-primary" : ""}" data-msg-action="${nextAction}" data-msg-id="${id}" data-variant-at-last="${atLast ? "1" : "0"}"${nextDisabled ? " disabled" : ""} aria-label="${nextAria}" title="${esc(nextTitle)}">›</button>`
|
||||||
: "";
|
: "";
|
||||||
const refresh = refreshable
|
const refresh = refreshable
|
||||||
? `<button type="button" class="msg-action msg-action-primary" data-msg-action="refresh" data-msg-id="${id}" title="重出一版(新版本)">↻</button>`
|
? `<button type="button" class="msg-action msg-action-primary" data-msg-action="refresh" data-msg-id="${id}" title="重出一版(新版本)">↻</button>`
|
||||||
: "";
|
: "";
|
||||||
return `<span class="msg-variant-nav" title="${showPager ? "切换版本;在此版本上继续输入" : "重出一版"}">
|
return `<span class="msg-variant-nav" title="${showPager ? "‹ › 切换版本;最新版 › 或 ↻ 重出一版" : "重出一版"}">
|
||||||
${pager}
|
${pager}
|
||||||
${refresh}
|
${refresh}
|
||||||
</span>`;
|
</span>`;
|
||||||
@@ -228,23 +240,50 @@ const flowMenuState = {
|
|||||||
handlers: null,
|
handlers: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function placeMenu(menu, x, y) {
|
/** html { zoom } 时:clientX/Y 是视口像素,fixed 的 left/top 是未缩放布局坐标 */
|
||||||
|
function readDocumentCssZoom() {
|
||||||
|
const raw = getComputedStyle(document.documentElement).zoom;
|
||||||
|
if (!raw || raw === "normal") return 1;
|
||||||
|
const z = Number.parseFloat(raw);
|
||||||
|
return Number.isFinite(z) && z > 0 ? z : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把菜单放到光标旁(视口坐标 clientX/Y)。
|
||||||
|
* 自动消化 html zoom,避免菜单相对光标漂移。
|
||||||
|
*/
|
||||||
|
export function placeMenu(menu, clientX, clientY) {
|
||||||
|
if (!menu) return;
|
||||||
|
const zoom = readDocumentCssZoom();
|
||||||
|
const pad = 4;
|
||||||
|
|
||||||
menu.hidden = false;
|
menu.hidden = false;
|
||||||
menu.style.left = `${x}px`;
|
menu.style.position = "fixed";
|
||||||
menu.style.top = `${y}px`;
|
menu.style.display = "block";
|
||||||
const rect = menu.getBoundingClientRect();
|
menu.style.zIndex = "1200";
|
||||||
if (rect.right > window.innerWidth) {
|
menu.style.right = "auto";
|
||||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
menu.style.bottom = "auto";
|
||||||
}
|
menu.style.margin = "0";
|
||||||
if (rect.bottom > window.innerHeight) {
|
// 先放到 0,0 量真实视觉尺寸(getBoundingClientRect 已含 zoom)
|
||||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
menu.style.left = "0";
|
||||||
}
|
menu.style.top = "0";
|
||||||
|
const { width: w, height: h } = menu.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 默认锚在光标处;溢出时只做最小贴边,避免整块翻到光标另一侧(右侧栏会像「差一截」)
|
||||||
|
let visualLeft = Math.min(clientX, window.innerWidth - w - pad);
|
||||||
|
let visualTop = Math.min(clientY, window.innerHeight - h - pad);
|
||||||
|
if (visualLeft < pad) visualLeft = pad;
|
||||||
|
if (visualTop < pad) visualTop = pad;
|
||||||
|
|
||||||
|
menu.style.left = `${visualLeft / zoom}px`;
|
||||||
|
menu.style.top = `${visualTop / zoom}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFlowNodeMenu(node, x, y, handlers) {
|
function showFlowNodeMenu(node, x, y, handlers) {
|
||||||
const menu = document.getElementById("flow-node-menu");
|
const menu = document.getElementById("flow-node-menu");
|
||||||
if (!menu || !node) return;
|
if (!menu || !node) return;
|
||||||
hideMsgMenu();
|
hideMsgMenu();
|
||||||
|
hideDictateProductMenu();
|
||||||
const stepId = node.getAttribute("data-flow-done");
|
const stepId = node.getAttribute("data-flow-done");
|
||||||
if (!stepId) return;
|
if (!stepId) return;
|
||||||
flowMenuState.stepId = stepId;
|
flowMenuState.stepId = stepId;
|
||||||
@@ -330,19 +369,27 @@ const MSG_PEEK_SIZE_KEY = "wa.msg-peek.size";
|
|||||||
const MSG_PEEK_KEEP_VISIBLE = 48;
|
const MSG_PEEK_KEEP_VISIBLE = 48;
|
||||||
/** 标题栏必须可见的顶边距(px)——保证可拖 */
|
/** 标题栏必须可见的顶边距(px)——保证可拖 */
|
||||||
const MSG_PEEK_TOP_MARGIN = 8;
|
const MSG_PEEK_TOP_MARGIN = 8;
|
||||||
|
/** 贴边时与视口边缘的最小间距(px) */
|
||||||
|
const MSG_PEEK_VIEW_PAD = 8;
|
||||||
const MSG_PEEK_MIN_W = 280;
|
const MSG_PEEK_MIN_W = 280;
|
||||||
const MSG_PEEK_MIN_H = 180;
|
const MSG_PEEK_MIN_H = 180;
|
||||||
|
|
||||||
|
const MSG_PEEK_CORNER_HANDLES = new Set(["nw", "ne", "sw", "se"]);
|
||||||
|
|
||||||
const msgPeekState = {
|
const msgPeekState = {
|
||||||
wired: false,
|
wired: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
resizing: false,
|
resizing: false,
|
||||||
|
resizeHandle: "se",
|
||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
resizeStartX: 0,
|
resizeStartX: 0,
|
||||||
resizeStartY: 0,
|
resizeStartY: 0,
|
||||||
resizeStartW: 0,
|
resizeStartW: 0,
|
||||||
resizeStartH: 0,
|
resizeStartH: 0,
|
||||||
|
resizeStartLeft: 0,
|
||||||
|
resizeStartTop: 0,
|
||||||
|
resizeAspect: 1,
|
||||||
lastLeft: null,
|
lastLeft: null,
|
||||||
lastTop: null,
|
lastTop: null,
|
||||||
};
|
};
|
||||||
@@ -386,16 +433,24 @@ function saveMsgPeekSize(width, height) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultMsgPeekSize() {
|
function msgPeekViewportMaxSize() {
|
||||||
return {
|
return {
|
||||||
width: Math.min(680, Math.max(MSG_PEEK_MIN_W, window.innerWidth * 0.92)),
|
maxW: Math.max(MSG_PEEK_MIN_W, window.innerWidth - MSG_PEEK_VIEW_PAD * 2),
|
||||||
height: Math.min(760, Math.max(MSG_PEEK_MIN_H, window.innerHeight * 0.82)),
|
maxH: Math.max(MSG_PEEK_MIN_H, window.innerHeight - MSG_PEEK_TOP_MARGIN - MSG_PEEK_VIEW_PAD),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultMsgPeekSize() {
|
||||||
|
const { maxW, maxH } = msgPeekViewportMaxSize();
|
||||||
|
return {
|
||||||
|
width: Math.min(680, Math.max(MSG_PEEK_MIN_W, Math.min(maxW, window.innerWidth * 0.92))),
|
||||||
|
height: Math.min(760, Math.max(MSG_PEEK_MIN_H, Math.min(maxH, window.innerHeight * 0.82))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局上下限:不小于最小尺寸,不大于当前视口可容纳尺寸 */
|
||||||
function clampMsgPeekSize(width, height) {
|
function clampMsgPeekSize(width, height) {
|
||||||
const maxW = Math.max(MSG_PEEK_MIN_W, window.innerWidth - 16);
|
const { maxW, maxH } = msgPeekViewportMaxSize();
|
||||||
const maxH = Math.max(MSG_PEEK_MIN_H, window.innerHeight - MSG_PEEK_TOP_MARGIN * 2);
|
|
||||||
return {
|
return {
|
||||||
width: Math.min(Math.max(MSG_PEEK_MIN_W, width), maxW),
|
width: Math.min(Math.max(MSG_PEEK_MIN_W, width), maxW),
|
||||||
height: Math.min(Math.max(MSG_PEEK_MIN_H, height), maxH),
|
height: Math.min(Math.max(MSG_PEEK_MIN_H, height), maxH),
|
||||||
@@ -441,6 +496,89 @@ function isMsgPeekFullyOffscreen(panel) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按手柄计算下一帧尺寸与位置。
|
||||||
|
* - 左右边:只改宽,高度不变(内容区滚动)
|
||||||
|
* - 上下边:只改高,宽度不变
|
||||||
|
* - 四角:保持宽高比同时缩放
|
||||||
|
* 尺寸上限贴紧视口,避免直接撑出屏幕。
|
||||||
|
*/
|
||||||
|
function computeMsgPeekResize(handle, start, dw, dh) {
|
||||||
|
const fromLeft = handle === "w" || handle === "nw" || handle === "sw";
|
||||||
|
const fromTop = handle === "n" || handle === "nw" || handle === "ne";
|
||||||
|
const edgeW = handle === "e" || handle === "w";
|
||||||
|
const edgeH = handle === "n" || handle === "s";
|
||||||
|
const aspect = MSG_PEEK_CORNER_HANDLES.has(handle);
|
||||||
|
|
||||||
|
const rightEdge = start.left + start.w;
|
||||||
|
const bottomEdge = start.top + start.h;
|
||||||
|
const minLeft = MSG_PEEK_VIEW_PAD;
|
||||||
|
const maxRight = window.innerWidth - MSG_PEEK_VIEW_PAD;
|
||||||
|
const minTop = MSG_PEEK_TOP_MARGIN;
|
||||||
|
const maxBottom = window.innerHeight - MSG_PEEK_VIEW_PAD;
|
||||||
|
const ratio = start.aspect > 0 ? start.aspect : start.w / Math.max(1, start.h);
|
||||||
|
|
||||||
|
const maxW = fromLeft
|
||||||
|
? Math.max(MSG_PEEK_MIN_W, rightEdge - minLeft)
|
||||||
|
: Math.max(MSG_PEEK_MIN_W, maxRight - start.left);
|
||||||
|
const maxH = fromTop
|
||||||
|
? Math.max(MSG_PEEK_MIN_H, bottomEdge - minTop)
|
||||||
|
: Math.max(MSG_PEEK_MIN_H, maxBottom - start.top);
|
||||||
|
|
||||||
|
let nextW = start.w;
|
||||||
|
let nextH = start.h;
|
||||||
|
|
||||||
|
if (edgeW) {
|
||||||
|
nextW = Math.min(Math.max(MSG_PEEK_MIN_W, start.w + (fromLeft ? -dw : dw)), maxW);
|
||||||
|
nextH = start.h; // 只改宽,高度保持,溢出由内容区滚动消化
|
||||||
|
} else if (edgeH) {
|
||||||
|
nextW = start.w; // 只改高,宽度保持
|
||||||
|
nextH = Math.min(Math.max(MSG_PEEK_MIN_H, start.h + (fromTop ? -dh : dh)), maxH);
|
||||||
|
} else if (aspect) {
|
||||||
|
const rawW = start.w + (fromLeft ? -dw : dw);
|
||||||
|
const rawH = start.h + (fromTop ? -dh : dh);
|
||||||
|
const useWidth =
|
||||||
|
Math.abs(rawW - start.w) / Math.max(1, start.w) >=
|
||||||
|
Math.abs(rawH - start.h) / Math.max(1, start.h);
|
||||||
|
if (useWidth) {
|
||||||
|
nextW = rawW;
|
||||||
|
nextH = nextW / ratio;
|
||||||
|
} else {
|
||||||
|
nextH = rawH;
|
||||||
|
nextW = nextH * ratio;
|
||||||
|
}
|
||||||
|
const upScale = Math.min(maxW / Math.max(1, nextW), maxH / Math.max(1, nextH), 1);
|
||||||
|
nextW *= upScale;
|
||||||
|
nextH *= upScale;
|
||||||
|
if (nextW < MSG_PEEK_MIN_W || nextH < MSG_PEEK_MIN_H) {
|
||||||
|
const downScale = Math.max(
|
||||||
|
MSG_PEEK_MIN_W / Math.max(1, nextW),
|
||||||
|
MSG_PEEK_MIN_H / Math.max(1, nextH),
|
||||||
|
);
|
||||||
|
nextW *= downScale;
|
||||||
|
nextH *= downScale;
|
||||||
|
}
|
||||||
|
nextW = Math.min(Math.max(MSG_PEEK_MIN_W, nextW), maxW);
|
||||||
|
nextH = nextW / ratio;
|
||||||
|
if (nextH > maxH) {
|
||||||
|
nextH = maxH;
|
||||||
|
nextW = Math.min(nextH * ratio, maxW);
|
||||||
|
nextH = nextW / ratio;
|
||||||
|
}
|
||||||
|
if (nextH < MSG_PEEK_MIN_H) {
|
||||||
|
nextH = MSG_PEEK_MIN_H;
|
||||||
|
nextW = Math.min(Math.max(MSG_PEEK_MIN_W, nextH * ratio), maxW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: nextW,
|
||||||
|
height: nextH,
|
||||||
|
left: fromLeft ? rightEdge - nextW : start.left,
|
||||||
|
top: fromTop ? bottomEdge - nextH : start.top,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 每次打开:记忆尺寸 + 屏幕居中(标题栏保证可见) */
|
/** 每次打开:记忆尺寸 + 屏幕居中(标题栏保证可见) */
|
||||||
function placeMsgPeek(panel) {
|
function placeMsgPeek(panel) {
|
||||||
const size = applyMsgPeekSize(panel);
|
const size = applyMsgPeekSize(panel);
|
||||||
@@ -480,21 +618,28 @@ function wireMsgPeekPanel() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
const resizeHandle = panel.querySelector("[data-peek-resize]");
|
const resizeHandles = panel.querySelectorAll("[data-peek-resize]");
|
||||||
resizeHandle?.addEventListener("pointerdown", (e) => {
|
for (const resizeHandle of resizeHandles) {
|
||||||
|
resizeHandle.addEventListener("pointerdown", (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if (msgPeekState.dragging) return;
|
if (msgPeekState.dragging) return;
|
||||||
|
const handle = resizeHandle.getAttribute("data-peek-resize") || "se";
|
||||||
const rect = panel.getBoundingClientRect();
|
const rect = panel.getBoundingClientRect();
|
||||||
msgPeekState.resizing = true;
|
msgPeekState.resizing = true;
|
||||||
|
msgPeekState.resizeHandle = handle;
|
||||||
msgPeekState.resizeStartX = e.clientX;
|
msgPeekState.resizeStartX = e.clientX;
|
||||||
msgPeekState.resizeStartY = e.clientY;
|
msgPeekState.resizeStartY = e.clientY;
|
||||||
msgPeekState.resizeStartW = rect.width;
|
msgPeekState.resizeStartW = rect.width;
|
||||||
msgPeekState.resizeStartH = rect.height;
|
msgPeekState.resizeStartH = rect.height;
|
||||||
|
msgPeekState.resizeStartLeft = rect.left;
|
||||||
|
msgPeekState.resizeStartTop = rect.top;
|
||||||
|
msgPeekState.resizeAspect = rect.width / Math.max(1, rect.height);
|
||||||
panel.classList.add("is-resizing");
|
panel.classList.add("is-resizing");
|
||||||
resizeHandle.setPointerCapture?.(e.pointerId);
|
resizeHandle.setPointerCapture?.(e.pointerId);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const onMove = (e) => {
|
const onMove = (e) => {
|
||||||
if (msgPeekState.dragging) {
|
if (msgPeekState.dragging) {
|
||||||
@@ -510,14 +655,24 @@ function wireMsgPeekPanel() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msgPeekState.resizing) {
|
if (msgPeekState.resizing) {
|
||||||
const dw = e.clientX - msgPeekState.resizeStartX;
|
const frame = computeMsgPeekResize(
|
||||||
const dh = e.clientY - msgPeekState.resizeStartY;
|
msgPeekState.resizeHandle,
|
||||||
const size = clampMsgPeekSize(
|
{
|
||||||
msgPeekState.resizeStartW + dw,
|
w: msgPeekState.resizeStartW,
|
||||||
msgPeekState.resizeStartH + dh,
|
h: msgPeekState.resizeStartH,
|
||||||
|
left: msgPeekState.resizeStartLeft,
|
||||||
|
top: msgPeekState.resizeStartTop,
|
||||||
|
aspect: msgPeekState.resizeAspect,
|
||||||
|
},
|
||||||
|
e.clientX - msgPeekState.resizeStartX,
|
||||||
|
e.clientY - msgPeekState.resizeStartY,
|
||||||
);
|
);
|
||||||
panel.style.width = `${size.width}px`;
|
panel.style.width = `${frame.width}px`;
|
||||||
panel.style.height = `${size.height}px`;
|
panel.style.height = `${frame.height}px`;
|
||||||
|
panel.style.left = `${frame.left}px`;
|
||||||
|
panel.style.top = `${frame.top}px`;
|
||||||
|
msgPeekState.lastLeft = frame.left;
|
||||||
|
msgPeekState.lastTop = frame.top;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onUp = (e) => {
|
const onUp = (e) => {
|
||||||
@@ -533,7 +688,9 @@ function wireMsgPeekPanel() {
|
|||||||
if (msgPeekState.resizing) {
|
if (msgPeekState.resizing) {
|
||||||
msgPeekState.resizing = false;
|
msgPeekState.resizing = false;
|
||||||
panel.classList.remove("is-resizing");
|
panel.classList.remove("is-resizing");
|
||||||
resizeHandle?.releasePointerCapture?.(e.pointerId);
|
for (const handle of resizeHandles) {
|
||||||
|
handle.releasePointerCapture?.(e.pointerId);
|
||||||
|
}
|
||||||
if (!panel.hidden) {
|
if (!panel.hidden) {
|
||||||
const rect = panel.getBoundingClientRect();
|
const rect = panel.getBoundingClientRect();
|
||||||
const size = clampMsgPeekSize(rect.width, rect.height);
|
const size = clampMsgPeekSize(rect.width, rect.height);
|
||||||
@@ -612,6 +769,8 @@ function readDictateProductContent(card) {
|
|||||||
const dictateProductMenuState = {
|
const dictateProductMenuState = {
|
||||||
card: null,
|
card: null,
|
||||||
wired: false,
|
wired: false,
|
||||||
|
openedAt: 0,
|
||||||
|
docWired: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function hideDictateProductMenu() {
|
function hideDictateProductMenu() {
|
||||||
@@ -620,22 +779,47 @@ function hideDictateProductMenu() {
|
|||||||
dictateProductMenuState.card = null;
|
dictateProductMenuState.card = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 右键落在行间/内边距时,按纵向距离就近命中产物行(slack 放宽命中) */
|
||||||
|
function nearestDictateProduct(root, clientX, clientY, slack = 28) {
|
||||||
|
if (!root) return null;
|
||||||
|
let best = null;
|
||||||
|
let bestDist = Infinity;
|
||||||
|
for (const card of root.querySelectorAll("[data-dictate-product]")) {
|
||||||
|
const r = card.getBoundingClientRect();
|
||||||
|
const inX = clientX >= r.left - slack && clientX <= r.right + slack;
|
||||||
|
const inY = clientY >= r.top - slack && clientY <= r.bottom + slack;
|
||||||
|
if (!inX || !inY) continue;
|
||||||
|
const dx = clientX < r.left ? r.left - clientX : clientX > r.right ? clientX - r.right : 0;
|
||||||
|
const dy = clientY < r.top ? r.top - clientY : clientY > r.bottom ? clientY - r.bottom : 0;
|
||||||
|
const dist = dx + dy;
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDictateProductCard(clientX, clientY, fallbackRoot) {
|
||||||
|
const hit = document.elementFromPoint(clientX, clientY);
|
||||||
|
let card =
|
||||||
|
hit instanceof Element ? hit.closest("[data-dictate-product]") : null;
|
||||||
|
if (card) return card;
|
||||||
|
const root =
|
||||||
|
fallbackRoot ||
|
||||||
|
document.getElementById("coord-drawer-body") ||
|
||||||
|
document.querySelector(".dictate-products-list");
|
||||||
|
return nearestDictateProduct(root, clientX, clientY);
|
||||||
|
}
|
||||||
|
|
||||||
function showDictateProductMenu(card, x, y) {
|
function showDictateProductMenu(card, x, y) {
|
||||||
const menu = document.getElementById("dictate-product-menu");
|
const menu = document.getElementById("dictate-product-menu");
|
||||||
if (!menu || !card) return;
|
if (!menu || !card) return;
|
||||||
hideMsgMenu();
|
hideMsgMenu();
|
||||||
hideDictateProductMenu();
|
hideFlowNodeMenu();
|
||||||
dictateProductMenuState.card = card;
|
dictateProductMenuState.card = card;
|
||||||
menu.hidden = false;
|
dictateProductMenuState.openedAt = Date.now();
|
||||||
menu.style.left = `${x}px`;
|
placeMenu(menu, x, y);
|
||||||
menu.style.top = `${y}px`;
|
|
||||||
const rect = menu.getBoundingClientRect();
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
|
||||||
}
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireDictateProductMenu() {
|
function wireDictateProductMenu() {
|
||||||
@@ -673,23 +857,39 @@ function wireDictateProductMenu() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireDictateProductContextMenu(root) {
|
/** 文档级捕获:整行任意位置右键都能打开(不依赖 summary 命中) */
|
||||||
if (!root || root.dataset.dictateProductMenuWired) return;
|
function wireDictateProductContextMenu(_root) {
|
||||||
root.dataset.dictateProductMenuWired = "1";
|
|
||||||
wireDictateProductMenu();
|
wireDictateProductMenu();
|
||||||
root.addEventListener("contextmenu", (e) => {
|
if (dictateProductMenuState.docWired) return;
|
||||||
const card = e.target.closest(".dictate-product");
|
dictateProductMenuState.docWired = true;
|
||||||
if (!card || !root.contains(card)) return;
|
document.addEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
(e) => {
|
||||||
|
const inRail =
|
||||||
|
e.target instanceof Element &&
|
||||||
|
(e.target.closest("#coord-drawer-body") ||
|
||||||
|
e.target.closest(".dictate-products-list") ||
|
||||||
|
e.target.closest("[data-dictate-product]"));
|
||||||
|
if (!inRail) return;
|
||||||
|
const card = resolveDictateProductCard(
|
||||||
|
e.clientX,
|
||||||
|
e.clientY,
|
||||||
|
document.getElementById("coord-drawer-body"),
|
||||||
|
);
|
||||||
|
if (!card) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
showDictateProductMenu(card, e.clientX, e.clientY);
|
showDictateProductMenu(card, e.clientX, e.clientY);
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMsgMenu(card, x, y) {
|
function showMsgMenu(card, x, y) {
|
||||||
const menu = document.getElementById("msg-action-menu");
|
const menu = document.getElementById("msg-action-menu");
|
||||||
if (!menu || !card) return;
|
if (!menu || !card) return;
|
||||||
hideFlowNodeMenu();
|
hideFlowNodeMenu();
|
||||||
|
hideDictateProductMenu();
|
||||||
const pack = card.closest(".coord-pack");
|
const pack = card.closest(".coord-pack");
|
||||||
const rollbackHost = pack || card;
|
const rollbackHost = pack || card;
|
||||||
// 上下文按单次请求(这条消息)区分,不能回落到整包里的另一条
|
// 上下文按单次请求(这条消息)区分,不能回落到整包里的另一条
|
||||||
@@ -716,22 +916,13 @@ function showMsgMenu(card, x, y) {
|
|||||||
}
|
}
|
||||||
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
|
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
|
||||||
if (compareBtn) compareBtn.toggleAttribute("hidden", !hasContext);
|
if (compareBtn) compareBtn.toggleAttribute("hidden", !hasContext);
|
||||||
menu.hidden = false;
|
|
||||||
menu.style.left = `${x}px`;
|
|
||||||
menu.style.top = `${y}px`;
|
|
||||||
msgMenuState.messageId = card.dataset.messageId ?? null;
|
msgMenuState.messageId = card.dataset.messageId ?? null;
|
||||||
msgMenuState.contextMessageId = contextTarget?.dataset.messageId ?? card.dataset.messageId ?? null;
|
msgMenuState.contextMessageId = contextTarget?.dataset.messageId ?? card.dataset.messageId ?? null;
|
||||||
msgMenuState.rollbackMessageId = rollbackMessageId;
|
msgMenuState.rollbackMessageId = rollbackMessageId;
|
||||||
msgMenuState.rollbackMode = rollbackMode;
|
msgMenuState.rollbackMode = rollbackMode;
|
||||||
msgMenuState.restoreDraft = rollbackHost.dataset.restoreDraft ?? "";
|
msgMenuState.restoreDraft = rollbackHost.dataset.restoreDraft ?? "";
|
||||||
msgMenuState.card = card;
|
msgMenuState.card = card;
|
||||||
const rect = menu.getBoundingClientRect();
|
placeMenu(menu, x, y);
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
|
||||||
}
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireMsgActionMenu(handlers) {
|
function wireMsgActionMenu(handlers) {
|
||||||
@@ -804,13 +995,22 @@ function wireMessageContextMenu(feed, handlers) {
|
|||||||
if (!feed || feed.dataset.contextWired) return;
|
if (!feed || feed.dataset.contextWired) return;
|
||||||
feed.dataset.contextWired = "1";
|
feed.dataset.contextWired = "1";
|
||||||
wireMsgActionMenu(handlers);
|
wireMsgActionMenu(handlers);
|
||||||
feed.addEventListener("contextmenu", (e) => {
|
// 与右侧产物菜单一致:捕获阶段 + 光标坐标走 placeMenu(含 html zoom)
|
||||||
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line, .coord-pack");
|
feed.addEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
(e) => {
|
||||||
|
const bubble = e.target.closest(
|
||||||
|
".msg-bubble, .workspace-review, .coord-line, .coord-pack",
|
||||||
|
);
|
||||||
const card = e.target.closest("[data-message-id]");
|
const card = e.target.closest("[data-message-id]");
|
||||||
if (!bubble || !card || card.classList.contains("msg-pending")) return;
|
if (!bubble || !card || card.classList.contains("msg-pending")) return;
|
||||||
|
if (!feed.contains(card)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
showMsgMenu(card, e.clientX, e.clientY);
|
showMsgMenu(card, e.clientX, e.clientY);
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
feed.addEventListener("click", (e) => {
|
feed.addEventListener("click", (e) => {
|
||||||
const contextTrigger = e.target.closest("[data-context-trigger]");
|
const contextTrigger = e.target.closest("[data-context-trigger]");
|
||||||
const contextCard = contextTrigger?.closest("[data-message-id]");
|
const contextCard = contextTrigger?.closest("[data-message-id]");
|
||||||
@@ -950,6 +1150,11 @@ function wireMessageFeedActions(feed, handlers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "variant-next") {
|
if (action === "variant-next") {
|
||||||
|
// 兜底:末页仍走到 next 时改走重 roll
|
||||||
|
if (btn.getAttribute("data-variant-at-last") === "1") {
|
||||||
|
h.onRefreshMessage?.(messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
h.onSwitchVariant?.(messageId, "next");
|
h.onSwitchVariant?.(messageId, "next");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -5483,8 +5688,9 @@ function renderDictateProductsList(view) {
|
|||||||
.replace(/^设计\./, "")
|
.replace(/^设计\./, "")
|
||||||
.replace(/^用户\./, "")
|
.replace(/^用户\./, "")
|
||||||
.replace(/#/g, " · ");
|
.replace(/#/g, " · ");
|
||||||
return `<details class="dictate-product dictate-product--rail" role="listitem">
|
return `<div class="dictate-product dictate-product--rail" role="listitem" data-dictate-product>
|
||||||
<summary class="dictate-product-sum">
|
<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-sum-top">
|
||||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||||
${
|
${
|
||||||
@@ -5501,9 +5707,10 @@ function renderDictateProductsList(view) {
|
|||||||
}</span>`
|
}</span>`
|
||||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||||
}
|
}
|
||||||
</summary>
|
</span>
|
||||||
<div class="dictate-product-body" data-pending="1"></div>
|
</button>
|
||||||
</details>`;
|
<div class="dictate-product-body" hidden data-pending="1"></div>
|
||||||
|
</div>`;
|
||||||
})
|
})
|
||||||
.join("")}</div>`;
|
.join("")}</div>`;
|
||||||
}
|
}
|
||||||
@@ -5519,19 +5726,33 @@ function hydrateDictateProductBody(card) {
|
|||||||
delete body.dataset.pending;
|
delete body.dataset.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDictateProductOpen(card, open) {
|
||||||
|
if (!card) return;
|
||||||
|
const sum = card.querySelector(".dictate-product-sum");
|
||||||
|
const body = card.querySelector(".dictate-product-body");
|
||||||
|
card.classList.toggle("is-open", open);
|
||||||
|
if (sum) sum.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
if (body) body.hidden = !open;
|
||||||
|
if (open) hydrateDictateProductBody(card);
|
||||||
|
}
|
||||||
|
|
||||||
function wireDictateProductAccordion(root) {
|
function wireDictateProductAccordion(root) {
|
||||||
if (!root || root.dataset.dictateProductAccordionWired) return;
|
if (!root || root.dataset.dictateProductAccordionWired) return;
|
||||||
root.dataset.dictateProductAccordionWired = "1";
|
root.dataset.dictateProductAccordionWired = "1";
|
||||||
root.addEventListener("toggle", (e) => {
|
root.addEventListener("click", (e) => {
|
||||||
const card = e.target;
|
const sum = e.target.closest(".dictate-product-sum");
|
||||||
if (!(card instanceof HTMLDetailsElement)) return;
|
if (!sum || !root.contains(sum)) return;
|
||||||
if (!card.classList.contains("dictate-product--rail")) return;
|
// 右键菜单用 pointer,这里只处理左键展开
|
||||||
if (!card.open) return;
|
if (e.button != null && e.button !== 0) return;
|
||||||
hydrateDictateProductBody(card);
|
const card = sum.closest("[data-dictate-product]");
|
||||||
root.querySelectorAll("details.dictate-product--rail[open]").forEach((other) => {
|
if (!card) return;
|
||||||
if (other !== card) other.open = false;
|
e.preventDefault();
|
||||||
|
const willOpen = !card.classList.contains("is-open");
|
||||||
|
root.querySelectorAll("[data-dictate-product].is-open").forEach((other) => {
|
||||||
|
if (other !== card) setDictateProductOpen(other, false);
|
||||||
|
});
|
||||||
|
setDictateProductOpen(card, willOpen);
|
||||||
});
|
});
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillCoordRailProducts(view) {
|
function fillCoordRailProducts(view) {
|
||||||
@@ -6101,7 +6322,14 @@ document.addEventListener("keydown", (e) => {
|
|||||||
hideFlowNodeMenu();
|
hideFlowNodeMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("scroll", () => {
|
document.addEventListener("scroll", (e) => {
|
||||||
|
// 打开菜单后短时间内忽略(避免焦点/布局引起的误关)
|
||||||
|
if (Date.now() - dictateProductMenuState.openedAt < 250) return;
|
||||||
|
// 菜单自身或菜单内滚动不关
|
||||||
|
const menu = document.getElementById("dictate-product-menu");
|
||||||
|
if (menu && !menu.hidden && e.target instanceof Node && menu.contains(e.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
hideMsgMenu();
|
hideMsgMenu();
|
||||||
hideDictateProductMenu();
|
hideDictateProductMenu();
|
||||||
hideFlowNodeMenu();
|
hideFlowNodeMenu();
|
||||||
|
|||||||
+80
-32
@@ -6,6 +6,7 @@ import {
|
|||||||
fillStepArtifactDialog,
|
fillStepArtifactDialog,
|
||||||
canOfferSaveProduct,
|
canOfferSaveProduct,
|
||||||
canOfferEnterPlay,
|
canOfferEnterPlay,
|
||||||
|
placeMenu,
|
||||||
} from "./agent-ui.js";
|
} from "./agent-ui.js";
|
||||||
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
||||||
import { renderIntakePanel } from "./intake-ui.js";
|
import { renderIntakePanel } from "./intake-ui.js";
|
||||||
@@ -1201,12 +1202,83 @@ function endComposerSubmit() {
|
|||||||
skipComposerStash = false;
|
skipComposerStash = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPreferenceReview(view) {
|
||||||
|
const host = $("preference-review");
|
||||||
|
if (!host) return;
|
||||||
|
const items = Array.isArray(view?.preferenceReview) ? view.preferenceReview : [];
|
||||||
|
if (!items.length) {
|
||||||
|
host.hidden = true;
|
||||||
|
host.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
host.hidden = false;
|
||||||
|
host.innerHTML = "";
|
||||||
|
items.forEach((c, i) => {
|
||||||
|
const card = document.createElement("article");
|
||||||
|
card.className = "preference-review-card";
|
||||||
|
card.dataset.candidateId = c.id;
|
||||||
|
|
||||||
|
const head = document.createElement("header");
|
||||||
|
head.className = "preference-review-head";
|
||||||
|
const title = document.createElement("strong");
|
||||||
|
title.textContent = `偏好候选${items.length > 1 ? ` ${i + 1}/${items.length}` : ""}`;
|
||||||
|
const hint = document.createElement("span");
|
||||||
|
hint.className = "preference-review-hint";
|
||||||
|
hint.textContent = "可编辑后收下;本局正文不受影响";
|
||||||
|
head.append(title, hint);
|
||||||
|
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.className = "preference-review-content";
|
||||||
|
ta.rows = 3;
|
||||||
|
ta.value = c.content ?? "";
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "preference-review-actions";
|
||||||
|
const acceptBtn = document.createElement("button");
|
||||||
|
acceptBtn.type = "button";
|
||||||
|
acceptBtn.className = "btn-primary";
|
||||||
|
acceptBtn.textContent = "收下";
|
||||||
|
acceptBtn.addEventListener("click", () => {
|
||||||
|
resolvePreferenceReview(c.id, "accept", ta.value);
|
||||||
|
});
|
||||||
|
const rejectBtn = document.createElement("button");
|
||||||
|
rejectBtn.type = "button";
|
||||||
|
rejectBtn.className = "btn-secondary";
|
||||||
|
rejectBtn.textContent = "不要";
|
||||||
|
rejectBtn.addEventListener("click", () => {
|
||||||
|
resolvePreferenceReview(c.id, "reject");
|
||||||
|
});
|
||||||
|
actions.append(acceptBtn, rejectBtn);
|
||||||
|
|
||||||
|
card.append(head, ta, actions);
|
||||||
|
host.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePreferenceReview(candidateId, action, content) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
try {
|
||||||
|
const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}/preferences/review`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
candidateId,
|
||||||
|
action,
|
||||||
|
...(action === "accept" ? { content } : {}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
renderSession(view);
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message || "审核失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderComposer(view, loading) {
|
function renderComposer(view, loading) {
|
||||||
stashComposerDraftFromDom();
|
stashComposerDraftFromDom();
|
||||||
if (pendingComposerDraft) composerForceInput = true;
|
if (pendingComposerDraft) composerForceInput = true;
|
||||||
const host = $("composer");
|
const host = $("composer");
|
||||||
const root = $("composer-main") || host;
|
const root = $("composer-main") || host;
|
||||||
if (!root || !host) return;
|
if (!root || !host) return;
|
||||||
|
renderPreferenceReview(view);
|
||||||
if (host._enterHandler) {
|
if (host._enterHandler) {
|
||||||
document.removeEventListener("keydown", host._enterHandler);
|
document.removeEventListener("keydown", host._enterHandler);
|
||||||
host._enterHandler = null;
|
host._enterHandler = null;
|
||||||
@@ -1748,18 +1820,8 @@ function showEnterPicker(bookId, anchorEl) {
|
|||||||
}
|
}
|
||||||
pop.hidden = false;
|
pop.hidden = false;
|
||||||
const rect = anchorEl.getBoundingClientRect();
|
const rect = anchorEl.getBoundingClientRect();
|
||||||
const left = rect.right + 8;
|
// 与右键菜单同一套定位(消化 html zoom)
|
||||||
let top = rect.top;
|
placeMenu(pop, rect.right + 8, rect.top);
|
||||||
pop.style.left = `${left}px`;
|
|
||||||
pop.style.top = `${top}px`;
|
|
||||||
const box = pop.getBoundingClientRect();
|
|
||||||
if (box.bottom > window.innerHeight - 8) {
|
|
||||||
top = Math.max(8, window.innerHeight - box.height - 8);
|
|
||||||
pop.style.top = `${top}px`;
|
|
||||||
}
|
|
||||||
if (box.right > window.innerWidth - 8) {
|
|
||||||
pop.style.left = `${Math.max(8, rect.left - box.width - 8)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let saveMenuState = { bookId: null, saveId: null, label: "" };
|
let saveMenuState = { bookId: null, saveId: null, label: "" };
|
||||||
@@ -1770,22 +1832,17 @@ function hideSaveMenu() {
|
|||||||
saveMenuState = { bookId: null, saveId: null, label: "" };
|
saveMenuState = { bookId: null, saveId: null, label: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function placeFixedMenu(menu, x, y) {
|
||||||
|
placeMenu(menu, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
function showSaveMenu(bookId, saveId, label, x, y) {
|
function showSaveMenu(bookId, saveId, label, x, y) {
|
||||||
const menu = $("save-action-menu");
|
const menu = $("save-action-menu");
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
hideEnterPicker();
|
hideEnterPicker();
|
||||||
hideBookMenu();
|
hideBookMenu();
|
||||||
saveMenuState = { bookId, saveId, label: label || "" };
|
saveMenuState = { bookId, saveId, label: label || "" };
|
||||||
menu.hidden = false;
|
placeFixedMenu(menu, x, y);
|
||||||
menu.style.left = `${x}px`;
|
|
||||||
menu.style.top = `${y}px`;
|
|
||||||
const rect = menu.getBoundingClientRect();
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
|
||||||
}
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSnapshotProductsEditor(bookId, saveId, label) {
|
async function openSnapshotProductsEditor(bookId, saveId, label) {
|
||||||
@@ -1896,16 +1953,7 @@ function showBookMenu(bookId, x, y) {
|
|||||||
hideSaveMenu();
|
hideSaveMenu();
|
||||||
bookMenuBookId = bookId;
|
bookMenuBookId = bookId;
|
||||||
refreshBookMenuLabels();
|
refreshBookMenuLabels();
|
||||||
menu.hidden = false;
|
placeFixedMenu(menu, x, y);
|
||||||
menu.style.left = `${x}px`;
|
|
||||||
menu.style.top = `${y}px`;
|
|
||||||
const rect = menu.getBoundingClientRect();
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
|
||||||
}
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCatalogNav() {
|
function isCatalogNav() {
|
||||||
|
|||||||
+12
-3
@@ -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"
|
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"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/styles.css?v=20260921-repeatable-split" />
|
<link rel="stylesheet" href="/styles.css?v=20260922-menu-unify" />
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
|
||||||
<a href="/modules.html">技能</a>
|
<a href="/modules.html">技能</a>
|
||||||
<a href="/shells.html">呈现壳</a>
|
<a href="/shells.html">呈现壳</a>
|
||||||
|
<a href="/libraries.html">库</a>
|
||||||
<a href="/stats.html">Token</a>
|
<a href="/stats.html">Token</a>
|
||||||
<a href="/settings.html">设置</a>
|
<a href="/settings.html">设置</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -181,6 +182,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="composer" id="composer">
|
<footer class="composer" id="composer">
|
||||||
|
<div id="preference-review" class="preference-review" hidden></div>
|
||||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
<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>
|
<div id="composer-main"></div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -263,7 +265,14 @@
|
|||||||
<button type="button" class="msg-peek-close" id="msg-peek-close" aria-label="关闭">×</button>
|
<button type="button" class="msg-peek-close" id="msg-peek-close" aria-label="关闭">×</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="msg-peek-body" id="msg-peek-body"></div>
|
<div class="msg-peek-body" id="msg-peek-body"></div>
|
||||||
<div class="msg-peek-resize" data-peek-resize="se" title="拖动调整大小" aria-hidden="true"></div>
|
<div class="msg-peek-resize msg-peek-resize--n" data-peek-resize="n" title="拖动调整高度" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--s" data-peek-resize="s" title="拖动调整高度" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--e" data-peek-resize="e" title="拖动调整宽度" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--w" data-peek-resize="w" title="拖动调整宽度" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--nw" data-peek-resize="nw" title="等比缩放" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--ne" data-peek-resize="ne" title="等比缩放" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--sw" data-peek-resize="sw" title="等比缩放" aria-hidden="true"></div>
|
||||||
|
<div class="msg-peek-resize msg-peek-resize--se" data-peek-resize="se" title="等比缩放" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<menu id="flow-node-menu" class="context-menu" hidden>
|
<menu id="flow-node-menu" class="context-menu" hidden>
|
||||||
@@ -328,6 +337,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script type="module" src="/app.js?v=20260921-repeatable-split"></script>
|
<script type="module" src="/app.js?v=20260922-menu-unify"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+249
-29
@@ -2372,7 +2372,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
.book-enter-pop[hidden] { display: none; }
|
.book-enter-pop[hidden] { display: none !important; }
|
||||||
.book-enter-item {
|
.book-enter-item {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -3119,6 +3119,80 @@ body.rail-collapsed .message-feed {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
margin: 0.75em 0;
|
margin: 0.75em 0;
|
||||||
}
|
}
|
||||||
|
.msg-body .md-table-wrap,
|
||||||
|
.dictate-product-body .md-table-wrap,
|
||||||
|
.present-region-body .md-table-wrap {
|
||||||
|
margin: 0.55em 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.msg-body .md-table,
|
||||||
|
.dictate-product-body .md-table,
|
||||||
|
.present-region-body .md-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.92em;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.msg-body .md-table th,
|
||||||
|
.msg-body .md-table td,
|
||||||
|
.dictate-product-body .md-table th,
|
||||||
|
.dictate-product-body .md-table td,
|
||||||
|
.present-region-body .md-table th,
|
||||||
|
.present-region-body .md-table td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.35em 0.55em;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.msg-body .md-table th,
|
||||||
|
.dictate-product-body .md-table th,
|
||||||
|
.present-region-body .md-table th {
|
||||||
|
font-weight: 650;
|
||||||
|
background: color-mix(in srgb, var(--surface) 88%, var(--accent));
|
||||||
|
}
|
||||||
|
.msg-body .md-details,
|
||||||
|
.dictate-product-body .md-details,
|
||||||
|
.present-region-body .md-details {
|
||||||
|
margin: 0.55em 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
|
||||||
|
}
|
||||||
|
.msg-body .md-summary,
|
||||||
|
.dictate-product-body .md-summary,
|
||||||
|
.present-region-body .md-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0.45em 0.65em;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.msg-body .md-summary::-webkit-details-marker,
|
||||||
|
.dictate-product-body .md-summary::-webkit-details-marker,
|
||||||
|
.present-region-body .md-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.msg-body .md-summary::before,
|
||||||
|
.dictate-product-body .md-summary::before,
|
||||||
|
.present-region-body .md-summary::before {
|
||||||
|
content: "▸";
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: transform 0.12s ease;
|
||||||
|
}
|
||||||
|
.msg-body .md-details[open] > .md-summary::before,
|
||||||
|
.dictate-product-body .md-details[open] > .md-summary::before,
|
||||||
|
.present-region-body .md-details[open] > .md-summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.msg-body .md-details-body,
|
||||||
|
.dictate-product-body .md-details-body,
|
||||||
|
.present-region-body .md-details-body {
|
||||||
|
padding: 0 0.65em 0.55em;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
.more-theme-icon {
|
.more-theme-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -5350,7 +5424,8 @@ body.design-workspace .af-mosaic {
|
|||||||
.ws-task-planned .ws-task-status { color: var(--accent); }
|
.ws-task-planned .ws-task-status { color: var(--accent); }
|
||||||
.ws-open { color: var(--warn); }
|
.ws-open { color: var(--warn); }
|
||||||
|
|
||||||
/* Floating peek: single entry, no backdrop, drag via header, resize SE */
|
/* Floating peek: single entry, no backdrop, drag via header;
|
||||||
|
edges = width/height only, corners = aspect-locked */
|
||||||
.msg-peek-panel {
|
.msg-peek-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
@@ -5410,6 +5485,8 @@ body.design-workspace .af-mosaic {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
.msg-peek-close {
|
.msg-peek-close {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -5459,27 +5536,92 @@ body.design-workspace .af-mosaic {
|
|||||||
}
|
}
|
||||||
.msg-peek-resize {
|
.msg-peek-resize {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
cursor: nwse-resize;
|
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.msg-peek-resize::after {
|
.msg-peek-resize--n {
|
||||||
|
top: 0;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
height: 6px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--s {
|
||||||
|
bottom: 0;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
height: 6px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--e {
|
||||||
|
right: 0;
|
||||||
|
top: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
width: 6px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--w {
|
||||||
|
left: 0;
|
||||||
|
top: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
width: 6px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--nw,
|
||||||
|
.msg-peek-resize--ne,
|
||||||
|
.msg-peek-resize--sw,
|
||||||
|
.msg-peek-resize--se {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--nw { top: 0; left: 0; cursor: nwse-resize; }
|
||||||
|
.msg-peek-resize--ne { top: 0; right: 0; cursor: nesw-resize; }
|
||||||
|
.msg-peek-resize--sw { bottom: 0; left: 0; cursor: nesw-resize; }
|
||||||
|
.msg-peek-resize--se { bottom: 0; right: 0; cursor: nwse-resize; }
|
||||||
|
.msg-peek-resize--nw::after,
|
||||||
|
.msg-peek-resize--ne::after,
|
||||||
|
.msg-peek-resize--sw::after,
|
||||||
|
.msg-peek-resize--se::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
width: 9px;
|
width: 9px;
|
||||||
height: 9px;
|
height: 9px;
|
||||||
border-right: 2px solid color-mix(in srgb, var(--muted, #888) 70%, transparent);
|
border: 2px solid color-mix(in srgb, var(--muted, #888) 70%, transparent);
|
||||||
border-bottom: 2px solid color-mix(in srgb, var(--muted, #888) 70%, transparent);
|
|
||||||
border-radius: 0 0 2px 0;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.msg-peek-panel.is-resizing .msg-peek-resize::after {
|
.msg-peek-resize--nw::after {
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--ne::after {
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 0 2px 0 0;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--sw::after {
|
||||||
|
bottom: 3px;
|
||||||
|
left: 3px;
|
||||||
|
border-right: none;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 0 2px;
|
||||||
|
}
|
||||||
|
.msg-peek-resize--se::after {
|
||||||
|
right: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
.msg-peek-panel.is-resizing .msg-peek-resize--nw::after,
|
||||||
|
.msg-peek-panel.is-resizing .msg-peek-resize--ne::after,
|
||||||
|
.msg-peek-panel.is-resizing .msg-peek-resize--sw::after,
|
||||||
|
.msg-peek-panel.is-resizing .msg-peek-resize--se::after {
|
||||||
border-color: var(--accent, #2563eb);
|
border-color: var(--accent, #2563eb);
|
||||||
}
|
}
|
||||||
.msg-peek-body .json-pretty,
|
.msg-peek-body .json-pretty,
|
||||||
@@ -5505,12 +5647,26 @@ body.design-workspace .af-mosaic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Context menu */
|
/* Context menu — fixed to viewport; follow cursor via placeMenu */
|
||||||
|
menu.context-menu,
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: fixed; z-index: 1000; min-width: 120px;
|
position: fixed;
|
||||||
margin: 0; padding: 4px; list-style: none;
|
z-index: 1200;
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
inset: auto;
|
||||||
border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
min-width: 120px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px;
|
||||||
|
list-style: none;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
menu.context-menu[hidden],
|
||||||
|
.context-menu[hidden] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
.context-menu-item {
|
.context-menu-item {
|
||||||
display: block; width: 100%; text-align: left;
|
display: block; width: 100%; text-align: left;
|
||||||
@@ -5554,9 +5710,54 @@ body.design-workspace .af-mosaic {
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
.preference-review {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.preference-review[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.preference-review-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--surface, #fff);
|
||||||
|
}
|
||||||
|
.preference-review-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.preference-review-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted, #71717a);
|
||||||
|
}
|
||||||
|
.preference-review-content {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 64px;
|
||||||
|
font: inherit;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.preference-review-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
.composer:has(.composer-more-menu[open]) {
|
.composer:has(.composer-more-menu[open]) {
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
}
|
}
|
||||||
@@ -6959,27 +7160,42 @@ body.design-workspace .af-mosaic {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.dictate-product--rail > .dictate-product-sum {
|
.dictate-product--rail > .dictate-product-sum {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: context-menu;
|
||||||
|
user-select: none;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
.dictate-product-sum-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 8px 10px 8px 12px;
|
padding: 8px 10px 8px 12px;
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
list-style: none;
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
position: relative;
|
pointer-events: none; /* 整行命中落在 button 上,避免子节点吞事件 */
|
||||||
transition: background 0.1s ease;
|
transition: background 0.1s ease;
|
||||||
}
|
}
|
||||||
.dictate-product--rail > .dictate-product-sum::-webkit-details-marker {
|
.dictate-product--rail > .dictate-product-sum:hover .dictate-product-sum-inner {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.dictate-product--rail > .dictate-product-sum:hover {
|
|
||||||
background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
|
background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
|
||||||
}
|
}
|
||||||
.dictate-product--rail > .dictate-product-sum:focus-visible {
|
.dictate-product--rail > .dictate-product-sum:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
.dictate-product--rail[open] > .dictate-product-sum {
|
.dictate-product--rail.is-open > .dictate-product-sum .dictate-product-sum-inner {
|
||||||
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
|
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -7026,7 +7242,7 @@ body.design-workspace .af-mosaic {
|
|||||||
.dictate-product--rail > .dictate-product-sum:hover .dictate-product-chevron {
|
.dictate-product--rail > .dictate-product-sum:hover .dictate-product-chevron {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
.dictate-product--rail[open] .dictate-product-chevron {
|
.dictate-product--rail.is-open .dictate-product-chevron {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
@@ -7046,7 +7262,7 @@ body.design-workspace .af-mosaic {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
.dictate-product--rail[open] .dictate-product-preview {
|
.dictate-product--rail.is-open .dictate-product-preview {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.dictate-product--rail .dictate-product-body {
|
.dictate-product--rail .dictate-product-body {
|
||||||
@@ -7067,6 +7283,10 @@ body.design-workspace .af-mosaic {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.dictate-product--rail .dictate-product-body[hidden] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
.dictate-product--rail .dictate-product-body[data-pending="1"]:empty {
|
.dictate-product--rail .dictate-product-body[data-pending="1"]:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user