@musnows/scriverse 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/dist/ai.js +532 -131
- package/dist/ai.js.map +1 -1
- package/dist/app.js +73 -8
- package/dist/app.js.map +1 -1
- package/dist/database.js +94 -2
- package/dist/database.js.map +1 -1
- package/dist/public/ai-connectivity-test.js +5 -2
- package/dist/public/app.js +448 -72
- package/dist/public/index.html +23 -10
- package/dist/public/roleplay-turn.js +100 -0
- package/dist/public/styles.css +66 -15
- package/dist/public/toast-layer.d.ts +4 -0
- package/dist/public/toast-layer.js +5 -0
- package/dist/roleplay-turn.js +98 -0
- package/dist/roleplay-turn.js.map +1 -0
- package/dist/security.js +33 -7
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +2 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +95 -12
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +36 -6
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -2,6 +2,14 @@ import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate,
|
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
|
|
4
4
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
5
|
+
import {
|
|
6
|
+
emptyRoleplayScenePin,
|
|
7
|
+
normalizeRoleplayScenePin,
|
|
8
|
+
parseRoleplayUserTurn,
|
|
9
|
+
roleplayScenePinHasContent,
|
|
10
|
+
roleplayUserTurnDisplayText,
|
|
11
|
+
roleplayUserTurnTitleSource
|
|
12
|
+
} from "/roleplay-turn.js?v=20260823-ai-roleplay-scene-turn-v2";
|
|
5
13
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
14
|
import { createAiChatTabManager, normalizeAiChatTabLimit } from "/ai-chat-tabs.js?v=20260816-ai-chat-switcher-v2";
|
|
7
15
|
import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260816-ai-chat-tabs-v1";
|
|
@@ -17,7 +25,7 @@ import {
|
|
|
17
25
|
} from "/foreshadow-reminder.js?v=20260812-editor-reminder-v1";
|
|
18
26
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
19
27
|
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=20260822-ai-model-thinking-label-v3&feature=ai-provider-responses-v1";
|
|
20
|
-
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=
|
|
28
|
+
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
21
29
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
22
30
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
23
31
|
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260818-ai-agent-turn-process-v1";
|
|
@@ -100,6 +108,7 @@ import { systemStatusPresentation } from "/system-status.js?v=20260801-system-he
|
|
|
100
108
|
import { collectS3BackupRunTransitions, s3BackupEncryptionKeyFile, s3BackupEncryptionPresentation, s3BackupFailureToast, s3BackupRootPrefix, s3BackupStatusLabel } from "/s3-backup-ui.js?v=20260810-backup-encryption-v1";
|
|
101
109
|
import { createPresenceClientId, stagePresenceClientIdForRelogin } from "/presence-client-id.js?v=20260810-presence-relogin-v1";
|
|
102
110
|
import { normalizeUploadProgress, uploadProgressText } from "/upload-progress.js?v=20260812-upload-progress-v1";
|
|
111
|
+
import { resolveToastRegionHost } from "/toast-layer.js?v=20260822-toast-modal-host-v1";
|
|
103
112
|
import { buildGlobalReplaceRefreshPlan, resolveGlobalReplaceChapterCount } from "/global-replace-refresh.js?v=20260812-global-replace-tree-v2";
|
|
104
113
|
import {
|
|
105
114
|
clampCropRect,
|
|
@@ -461,6 +470,10 @@ function applyWorkAccessMode() {
|
|
|
461
470
|
$(".ai-panel").classList.toggle("permission-hidden", aiHidden);
|
|
462
471
|
$("#ai-prompt").readOnly = aiReadOnly;
|
|
463
472
|
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
473
|
+
$("#ai-scene-direction").readOnly = aiReadOnly;
|
|
474
|
+
$("#ai-scene-location").readOnly = aiReadOnly;
|
|
475
|
+
$("#ai-scene-present").readOnly = aiReadOnly;
|
|
476
|
+
$("#ai-scene-time").readOnly = aiReadOnly;
|
|
464
477
|
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
465
478
|
renderAiRoleplayCharacterSelect();
|
|
466
479
|
updateBackgroundTaskCenterVisibility();
|
|
@@ -2082,7 +2095,7 @@ function createAiChatTabState(input = {}) {
|
|
|
2082
2095
|
roleplayUserCharacter: input.roleplayUserCharacter ?? null,
|
|
2083
2096
|
citations: input.citations ?? [],
|
|
2084
2097
|
references: input.references ?? [],
|
|
2085
|
-
composer: input.composer ?? { text: "", citations: [], references: [], images: [] },
|
|
2098
|
+
composer: input.composer ?? { text: "", citations: [], references: [], images: [], sceneDirection: "", scenePin: emptyRoleplayScenePin() },
|
|
2086
2099
|
contextUsage: input.contextUsage ?? null,
|
|
2087
2100
|
contextWarning: input.contextWarning === true,
|
|
2088
2101
|
lastMessageAt: input.lastMessageAt ?? null,
|
|
@@ -2124,12 +2137,21 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
|
2124
2137
|
text: snapshot.text,
|
|
2125
2138
|
citations: tab.citations.map((citation) => ({ ...citation })),
|
|
2126
2139
|
references: tab.references.map((reference) => ({ ...reference })),
|
|
2127
|
-
images: normalizeAiChatImageAttachments(snapshot.images)
|
|
2140
|
+
images: normalizeAiChatImageAttachments(snapshot.images),
|
|
2141
|
+
sceneDirection: String(snapshot.sceneDirection ?? ""),
|
|
2142
|
+
scenePin: normalizeRoleplayScenePin(snapshot.scenePin)
|
|
2128
2143
|
};
|
|
2129
2144
|
}
|
|
2130
2145
|
|
|
2131
2146
|
function clearAiChatTabComposer(tab) {
|
|
2132
|
-
setAiChatTabComposerSnapshot(tab, {
|
|
2147
|
+
setAiChatTabComposerSnapshot(tab, {
|
|
2148
|
+
text: "",
|
|
2149
|
+
citations: [],
|
|
2150
|
+
references: [],
|
|
2151
|
+
images: [],
|
|
2152
|
+
sceneDirection: "",
|
|
2153
|
+
scenePin: normalizeRoleplayScenePin(tab.composer?.scenePin)
|
|
2154
|
+
});
|
|
2133
2155
|
}
|
|
2134
2156
|
|
|
2135
2157
|
function applyAiChatTabState(tab) {
|
|
@@ -2149,6 +2171,7 @@ function applyAiChatTabState(tab) {
|
|
|
2149
2171
|
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2150
2172
|
syncAiModelPicker();
|
|
2151
2173
|
setAiPromptText(tab.composer.text);
|
|
2174
|
+
restoreAiSceneComposer(tab.composer);
|
|
2152
2175
|
renderAiCitations();
|
|
2153
2176
|
renderAiReferences();
|
|
2154
2177
|
renderAiImageAttachments();
|
|
@@ -2170,7 +2193,9 @@ function aiChatTabIsReplaceable(tab = activeAiChatTab()) {
|
|
|
2170
2193
|
&& !(tab.composer?.text || "").trim()
|
|
2171
2194
|
&& !(tab.composer?.citations?.length)
|
|
2172
2195
|
&& !(tab.composer?.references?.length)
|
|
2173
|
-
&& !(tab.composer?.images?.length)
|
|
2196
|
+
&& !(tab.composer?.images?.length)
|
|
2197
|
+
&& !(tab.composer?.sceneDirection || "").trim()
|
|
2198
|
+
&& !roleplayScenePinHasContent(tab.composer?.scenePin));
|
|
2174
2199
|
}
|
|
2175
2200
|
|
|
2176
2201
|
function aiChatTabOpeningSlot() {
|
|
@@ -2443,7 +2468,7 @@ function resetAiFeed(
|
|
|
2443
2468
|
const roleplayName = roleplayCharacter?.name;
|
|
2444
2469
|
const roleplayUserName = roleplayUserCharacter?.name;
|
|
2445
2470
|
feed.innerHTML = roleplayName
|
|
2446
|
-
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。${roleplayUserName ? `你将以 ${esc(roleplayUserName)} 的身份与我互动。` : "
|
|
2471
|
+
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。${roleplayUserName ? `你将以 ${esc(roleplayUserName)} 的身份与我互动。` : "我可以通过角色卡、人物关系、知情设定和故事正文回答。"}</p></div></div>`
|
|
2447
2472
|
: '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
2448
2473
|
}
|
|
2449
2474
|
|
|
@@ -2521,8 +2546,9 @@ async function retryAiMessage(message) {
|
|
|
2521
2546
|
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2522
2547
|
const userMessage = findAiRetryUserMessage(message);
|
|
2523
2548
|
const prompt = userMessage?.dataset.copyText ?? "";
|
|
2549
|
+
const sceneDirection = userMessage?.dataset.sceneDirection ?? "";
|
|
2524
2550
|
const userMessageId = userMessage?.dataset.messageId ?? "";
|
|
2525
|
-
if (!sourceTab?.conversationId || !userMessageId || !prompt.trim()) {
|
|
2551
|
+
if (!sourceTab?.conversationId || !userMessageId || !(prompt.trim() || sceneDirection.trim())) {
|
|
2526
2552
|
toast("找不到需要重试的用户指令", "error");
|
|
2527
2553
|
return;
|
|
2528
2554
|
}
|
|
@@ -2535,6 +2561,7 @@ async function retryAiMessage(message) {
|
|
|
2535
2561
|
retry: {
|
|
2536
2562
|
message,
|
|
2537
2563
|
prompt,
|
|
2564
|
+
sceneDirection,
|
|
2538
2565
|
citations: aiMessageCitations(userMessage),
|
|
2539
2566
|
images: aiMessageImageAttachments(userMessage),
|
|
2540
2567
|
userMessageId
|
|
@@ -2670,6 +2697,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
2670
2697
|
recall_self: "回忆自身",
|
|
2671
2698
|
image: "读取设定图片",
|
|
2672
2699
|
recall_relationship: "回忆人物关系",
|
|
2700
|
+
recall_other: "回忆相识角色",
|
|
2701
|
+
recall_known: "回忆知情设定",
|
|
2673
2702
|
recall_story: "回忆故事",
|
|
2674
2703
|
calculate_time: "计算日期"
|
|
2675
2704
|
};
|
|
@@ -2683,9 +2712,11 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
2683
2712
|
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。",
|
|
2684
2713
|
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。",
|
|
2685
2714
|
image: "读取设定正文引用的图片附件,并返回多模态模型的理解内容。",
|
|
2686
|
-
recall_relationship: "
|
|
2687
|
-
|
|
2688
|
-
|
|
2715
|
+
recall_relationship: "不传角色列表时读取有关系的角色公开摘要;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。",
|
|
2716
|
+
recall_other: "读取自己通过关系、同一组织或共同参与时间线而认识的其他角色公开摘要。",
|
|
2717
|
+
recall_known: "读取自己所属种族、组织,以及与自己身份相关的世界设定。",
|
|
2718
|
+
recall_story: "查询自己姓名或别名出现过的正文段落,避免全知回忆。",
|
|
2719
|
+
calculate_time: "计算两个 YYYY-MM-DD 日期之间的天数差。"
|
|
2689
2720
|
};
|
|
2690
2721
|
|
|
2691
2722
|
const aiFeedScrollFrames = new WeakMap();
|
|
@@ -3183,7 +3214,7 @@ function renderAiConversationHistory() {
|
|
|
3183
3214
|
}
|
|
3184
3215
|
|
|
3185
3216
|
function defaultAiConversationTitle(prompt) {
|
|
3186
|
-
const normalized = String(prompt ?? "").replace(/\s+/gu, " ").trim();
|
|
3217
|
+
const normalized = roleplayUserTurnTitleSource(String(prompt ?? "")).replace(/\s+/gu, " ").trim();
|
|
3187
3218
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
3188
3219
|
}
|
|
3189
3220
|
|
|
@@ -3191,7 +3222,7 @@ function upsertAiConversationSummary(conversation) {
|
|
|
3191
3222
|
if (!conversation?.id) return;
|
|
3192
3223
|
const current = state.aiConversations.find((item) => item.id === conversation.id);
|
|
3193
3224
|
const lastMessage = Array.isArray(conversation.messages) ? conversation.messages.at(-1) : null;
|
|
3194
|
-
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: lastMessage.content } : {}) };
|
|
3225
|
+
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: roleplayUserTurnDisplayText(lastMessage.content) } : {}) };
|
|
3195
3226
|
delete summary.messages;
|
|
3196
3227
|
delete summary.messagesPage;
|
|
3197
3228
|
if (!current && loadedAiConversationsWorkId === state.work?.id) {
|
|
@@ -3214,7 +3245,7 @@ function updateAiConversationSummaryFromMessage(message) {
|
|
|
3214
3245
|
...current,
|
|
3215
3246
|
title: current.title === "新对话" && message.role === "user" ? defaultAiConversationTitle(message.content) : current.title,
|
|
3216
3247
|
messageCount: Number(current.messageCount ?? 0) + 1,
|
|
3217
|
-
preview: message.content,
|
|
3248
|
+
preview: roleplayUserTurnDisplayText(message.content),
|
|
3218
3249
|
...(aiConversationMessageHasImages(message) ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
|
|
3219
3250
|
updatedAt: message.createdAt ?? current.updatedAt
|
|
3220
3251
|
});
|
|
@@ -3349,7 +3380,14 @@ function applyConversationToAiChatTab(tab, conversation) {
|
|
|
3349
3380
|
tab.roleplayUserCharacter = conversation.roleplayUserCharacter ?? null;
|
|
3350
3381
|
tab.citations = [];
|
|
3351
3382
|
tab.references = [];
|
|
3352
|
-
tab.composer = {
|
|
3383
|
+
tab.composer = {
|
|
3384
|
+
text: "",
|
|
3385
|
+
citations: [],
|
|
3386
|
+
references: [],
|
|
3387
|
+
images: [],
|
|
3388
|
+
sceneDirection: "",
|
|
3389
|
+
scenePin: normalizeRoleplayScenePin(conversation.scenePin)
|
|
3390
|
+
};
|
|
3353
3391
|
tab.contextUsage = null;
|
|
3354
3392
|
tab.contextWarning = conversation.contextWarningPending === true;
|
|
3355
3393
|
resetAiFeed(tab.feed, tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
@@ -3400,16 +3438,30 @@ async function createNewAiConversation(taskType = "chat") {
|
|
|
3400
3438
|
}
|
|
3401
3439
|
}
|
|
3402
3440
|
|
|
3441
|
+
function favoriteCharactersFirst(characters) {
|
|
3442
|
+
const items = Array.isArray(characters) ? characters : [];
|
|
3443
|
+
return [
|
|
3444
|
+
...items.filter((character) => character.isFavorite === true),
|
|
3445
|
+
...items.filter((character) => character.isFavorite !== true)
|
|
3446
|
+
];
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
function roleplayCharacterOptionLabel(character) {
|
|
3450
|
+
const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
|
|
3451
|
+
const deathLabel = character?.isDead ? "(已死亡)" : "";
|
|
3452
|
+
return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3403
3455
|
function renderAiRoleplayCharacterSelect() {
|
|
3404
3456
|
const select = $("#ai-roleplay-character");
|
|
3405
3457
|
const selectedId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3406
|
-
const availableCharacters = state.characters.filter((character) => !character.mergedIntoCharacterId);
|
|
3458
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => !character.mergedIntoCharacterId));
|
|
3407
3459
|
const options = [{ id: "", name: "选择角色卡" }, ...availableCharacters.map((character) => ({
|
|
3408
3460
|
id: String(character.id),
|
|
3409
|
-
name:
|
|
3461
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3410
3462
|
}))];
|
|
3411
3463
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3412
|
-
options.push({ id: selectedId, name:
|
|
3464
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayCharacter) });
|
|
3413
3465
|
}
|
|
3414
3466
|
select.replaceChildren(...options.map((option) => {
|
|
3415
3467
|
const element = document.createElement("option");
|
|
@@ -3425,7 +3477,7 @@ function renderAiRoleplayCharacterSelect() {
|
|
|
3425
3477
|
select.title = canSelectCharacter
|
|
3426
3478
|
? state.aiPromptSent
|
|
3427
3479
|
? aiConversationOptionLockedMessage
|
|
3428
|
-
: "为当前对话选择角色卡;角色扮演时 Agent
|
|
3480
|
+
: "为当前对话选择角色卡;角色扮演时 Agent 可以查询角色记忆、相识角色、知情设定、故事正文和设定图片"
|
|
3429
3481
|
: "当前账户没有角色模块读取权限";
|
|
3430
3482
|
renderAiRoleplayUserCharacterSelect();
|
|
3431
3483
|
}
|
|
@@ -3434,15 +3486,15 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3434
3486
|
const select = $("#ai-roleplay-user-character");
|
|
3435
3487
|
const selectedId = String(state.aiRoleplayUserCharacter?.id ?? "");
|
|
3436
3488
|
const aiCharacterId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3437
|
-
const availableCharacters = state.characters.filter((character) => (
|
|
3489
|
+
const availableCharacters = favoriteCharactersFirst(state.characters.filter((character) => (
|
|
3438
3490
|
!character.mergedIntoCharacterId && String(character.id) !== aiCharacterId
|
|
3439
|
-
));
|
|
3491
|
+
)));
|
|
3440
3492
|
const options = [{ id: "", name: "选择我的角色(可选)" }, ...availableCharacters.map((character) => ({
|
|
3441
3493
|
id: String(character.id),
|
|
3442
|
-
name:
|
|
3494
|
+
name: roleplayCharacterOptionLabel(character)
|
|
3443
3495
|
}))];
|
|
3444
3496
|
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3445
|
-
options.push({ id: selectedId, name:
|
|
3497
|
+
options.push({ id: selectedId, name: roleplayCharacterOptionLabel(state.aiRoleplayUserCharacter) });
|
|
3446
3498
|
}
|
|
3447
3499
|
select.replaceChildren(...options.map((option) => {
|
|
3448
3500
|
const element = document.createElement("option");
|
|
@@ -3461,7 +3513,7 @@ function renderAiRoleplayUserCharacterSelect() {
|
|
|
3461
3513
|
? "请先选择 AI 扮演的角色"
|
|
3462
3514
|
: state.aiPromptSent
|
|
3463
3515
|
? aiConversationOptionLockedMessage
|
|
3464
|
-
: "
|
|
3516
|
+
: "选择后,主输入是该角色的台词或行动;旁白请写在场景框";
|
|
3465
3517
|
}
|
|
3466
3518
|
|
|
3467
3519
|
const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
|
|
@@ -3600,8 +3652,9 @@ function syncAiTaskOptions() {
|
|
|
3600
3652
|
$("#ai-scope").disabled = interactionBusy || roleplaySelected;
|
|
3601
3653
|
$("#ai-scope").title = state.aiPromptSent
|
|
3602
3654
|
? aiConversationOptionLockedMessage
|
|
3603
|
-
: roleplaySelected ? "
|
|
3655
|
+
: roleplaySelected ? "角色扮演模式可以查询角色记忆、相识角色、知情设定、故事正文和设定图片" : "";
|
|
3604
3656
|
syncAiModelPicker();
|
|
3657
|
+
syncAiSceneComposer();
|
|
3605
3658
|
}
|
|
3606
3659
|
|
|
3607
3660
|
function applyAiConversationTaskType(taskType) {
|
|
@@ -3638,8 +3691,8 @@ function applyAiRoleplayCharacter(character, userCharacter = null) {
|
|
|
3638
3691
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
3639
3692
|
$("#ai-prompt").dataset.placeholder = active
|
|
3640
3693
|
? state.aiRoleplayUserCharacter
|
|
3641
|
-
? `以 ${String(state.aiRoleplayUserCharacter.name)}
|
|
3642
|
-
:
|
|
3694
|
+
? `以 ${String(state.aiRoleplayUserCharacter.name)} 的身份输入台词或行动……`
|
|
3695
|
+
: `输入对 ${String(state.aiRoleplayCharacter.name)} 说的台词或行动……`
|
|
3643
3696
|
: "告诉 AI 你想讨论或修改什么……";
|
|
3644
3697
|
renderAiRoleplayCharacterSelect();
|
|
3645
3698
|
syncAiTaskOptions();
|
|
@@ -3946,9 +3999,11 @@ function clearAiPromptComposer() {
|
|
|
3946
3999
|
state.aiReferences = [];
|
|
3947
4000
|
state.aiImageAttachments = [];
|
|
3948
4001
|
setAiPromptText("");
|
|
4002
|
+
setAiSceneDirection("");
|
|
3949
4003
|
renderAiCitations();
|
|
3950
4004
|
renderAiImageAttachments();
|
|
3951
4005
|
hideAiMentionMenu();
|
|
4006
|
+
syncAiSceneComposer();
|
|
3952
4007
|
}
|
|
3953
4008
|
|
|
3954
4009
|
function captureAiPromptComposer() {
|
|
@@ -3956,7 +4011,9 @@ function captureAiPromptComposer() {
|
|
|
3956
4011
|
text: aiPromptText(),
|
|
3957
4012
|
citations: state.aiCitations.map((citation) => ({ ...citation })),
|
|
3958
4013
|
references: state.aiReferences.map((reference) => ({ ...reference })),
|
|
3959
|
-
images: normalizeAiChatImageAttachments(state.aiImageAttachments)
|
|
4014
|
+
images: normalizeAiChatImageAttachments(state.aiImageAttachments),
|
|
4015
|
+
sceneDirection: aiSceneDirectionText(),
|
|
4016
|
+
scenePin: captureAiScenePin()
|
|
3960
4017
|
};
|
|
3961
4018
|
}
|
|
3962
4019
|
|
|
@@ -3965,11 +4022,77 @@ function restoreAiPromptComposer(snapshot) {
|
|
|
3965
4022
|
state.aiReferences = snapshot.references.map((reference) => ({ ...reference }));
|
|
3966
4023
|
state.aiImageAttachments = normalizeAiChatImageAttachments(snapshot.images);
|
|
3967
4024
|
setAiPromptText(snapshot.text);
|
|
4025
|
+
restoreAiSceneComposer(snapshot);
|
|
3968
4026
|
renderAiCitations();
|
|
3969
4027
|
renderAiImageAttachments();
|
|
3970
4028
|
hideAiMentionMenu();
|
|
3971
4029
|
}
|
|
3972
4030
|
|
|
4031
|
+
function aiSceneDirectionText() {
|
|
4032
|
+
return String($("#ai-scene-direction")?.value ?? "");
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function setAiSceneDirection(value) {
|
|
4036
|
+
const input = $("#ai-scene-direction");
|
|
4037
|
+
if (input) input.value = String(value ?? "");
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
function captureAiScenePin() {
|
|
4041
|
+
return normalizeRoleplayScenePin({
|
|
4042
|
+
location: $("#ai-scene-location")?.value ?? "",
|
|
4043
|
+
present: $("#ai-scene-present")?.value ?? "",
|
|
4044
|
+
timeLabel: $("#ai-scene-time")?.value ?? ""
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
function setAiScenePin(pin) {
|
|
4049
|
+
const normalized = normalizeRoleplayScenePin(pin);
|
|
4050
|
+
const location = $("#ai-scene-location");
|
|
4051
|
+
const present = $("#ai-scene-present");
|
|
4052
|
+
const timeLabel = $("#ai-scene-time");
|
|
4053
|
+
if (location) location.value = normalized.location;
|
|
4054
|
+
if (present) present.value = normalized.present;
|
|
4055
|
+
if (timeLabel) timeLabel.value = normalized.timeLabel;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
function restoreAiSceneComposer(snapshot = {}) {
|
|
4059
|
+
setAiSceneDirection(snapshot.sceneDirection ?? "");
|
|
4060
|
+
setAiScenePin(snapshot.scenePin);
|
|
4061
|
+
syncAiSceneComposer();
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
function roleplaySceneComposerVisible() {
|
|
4065
|
+
return $("#ai-task").value === "roleplay" && Boolean(state.aiRoleplayCharacter);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function syncAiSceneComposer() {
|
|
4069
|
+
const button = $("#ai-scene-button");
|
|
4070
|
+
const panel = $("#ai-scene-panel");
|
|
4071
|
+
if (!button || !panel) return;
|
|
4072
|
+
const visible = roleplaySceneComposerVisible();
|
|
4073
|
+
const busy = aiInteractionBusy();
|
|
4074
|
+
const readOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
|
|
4075
|
+
button.classList.toggle("hidden", !visible);
|
|
4076
|
+
button.disabled = !visible || busy || readOnly;
|
|
4077
|
+
button.setAttribute("aria-hidden", String(!visible));
|
|
4078
|
+
if (!visible) {
|
|
4079
|
+
panel.classList.add("hidden");
|
|
4080
|
+
button.setAttribute("aria-expanded", "false");
|
|
4081
|
+
}
|
|
4082
|
+
const hasContent = Boolean(aiSceneDirectionText().trim()) || roleplayScenePinHasContent(captureAiScenePin());
|
|
4083
|
+
button.classList.toggle("is-active", hasContent);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
function toggleAiScenePanel() {
|
|
4087
|
+
const button = $("#ai-scene-button");
|
|
4088
|
+
const panel = $("#ai-scene-panel");
|
|
4089
|
+
if (!button || !panel || button.classList.contains("hidden")) return;
|
|
4090
|
+
const willOpen = panel.classList.contains("hidden");
|
|
4091
|
+
panel.classList.toggle("hidden", !willOpen);
|
|
4092
|
+
button.setAttribute("aria-expanded", String(willOpen));
|
|
4093
|
+
if (willOpen) $("#ai-scene-direction")?.focus();
|
|
4094
|
+
}
|
|
4095
|
+
|
|
3973
4096
|
function aiPromptTextBeforeCursor() {
|
|
3974
4097
|
const prompt = $("#ai-prompt");
|
|
3975
4098
|
const selection = window.getSelection();
|
|
@@ -5109,8 +5232,18 @@ async function initializeAuthentication() {
|
|
|
5109
5232
|
return true;
|
|
5110
5233
|
}
|
|
5111
5234
|
|
|
5235
|
+
function syncToastRegionHost() {
|
|
5236
|
+
const region = $("#toast-region");
|
|
5237
|
+
const host = resolveToastRegionHost([...document.querySelectorAll("dialog[open]")], document.body);
|
|
5238
|
+
if (!host || region.parentElement === host) return false;
|
|
5239
|
+
if (typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
|
|
5240
|
+
host.append(region);
|
|
5241
|
+
return true;
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5112
5244
|
function raiseToastRegion() {
|
|
5113
5245
|
const region = $("#toast-region");
|
|
5246
|
+
syncToastRegionHost();
|
|
5114
5247
|
if (typeof region.showPopover !== "function") return;
|
|
5115
5248
|
if (region.matches(":popover-open")) region.hidePopover();
|
|
5116
5249
|
region.showPopover();
|
|
@@ -5263,9 +5396,6 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5263
5396
|
confirm.textContent = confirmLabel;
|
|
5264
5397
|
actions.append(cancel, confirm);
|
|
5265
5398
|
element.append(heading, description, actions);
|
|
5266
|
-
region.append(element);
|
|
5267
|
-
raiseToastRegion();
|
|
5268
|
-
cancel.focus();
|
|
5269
5399
|
return new Promise((resolve) => {
|
|
5270
5400
|
const finish = (confirmed) => {
|
|
5271
5401
|
element.remove();
|
|
@@ -5280,6 +5410,9 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
5280
5410
|
event.preventDefault();
|
|
5281
5411
|
finish(false);
|
|
5282
5412
|
});
|
|
5413
|
+
region.append(element);
|
|
5414
|
+
raiseToastRegion();
|
|
5415
|
+
cancel.focus();
|
|
5283
5416
|
});
|
|
5284
5417
|
}
|
|
5285
5418
|
|
|
@@ -5340,8 +5473,15 @@ function inputToast(message, { title = "请输入", inputLabel = title, value =
|
|
|
5340
5473
|
|
|
5341
5474
|
document.addEventListener("toggle", (event) => {
|
|
5342
5475
|
const target = event.target;
|
|
5343
|
-
if (target instanceof HTMLDialogElement
|
|
5476
|
+
if (!(target instanceof HTMLDialogElement)) return;
|
|
5477
|
+
const region = $("#toast-region");
|
|
5478
|
+
if (target.open && region.childElementCount) {
|
|
5344
5479
|
raiseToastRegion();
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
if (!target.open && (region.parentElement !== document.body || region.childElementCount)) {
|
|
5483
|
+
const moved = syncToastRegionHost();
|
|
5484
|
+
if (moved && region.childElementCount && typeof region.showPopover === "function") region.showPopover();
|
|
5345
5485
|
}
|
|
5346
5486
|
}, true);
|
|
5347
5487
|
|
|
@@ -8533,6 +8673,116 @@ function pencilIconMarkup() {
|
|
|
8533
8673
|
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="m16.5 3.5 1.4-1.4a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4L16.5 3.5Z"></path></svg>';
|
|
8534
8674
|
}
|
|
8535
8675
|
|
|
8676
|
+
function characterFavoriteIconMarkup() {
|
|
8677
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path></svg>';
|
|
8678
|
+
}
|
|
8679
|
+
|
|
8680
|
+
function characterFavoriteButton(item) {
|
|
8681
|
+
const isFavorite = item.isFavorite === true;
|
|
8682
|
+
const canFavorite = canEditModule("characters");
|
|
8683
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8684
|
+
const title = canFavorite ? action : "当前账户没有角色模块写入权限";
|
|
8685
|
+
return `<button class="character-favorite-button${isFavorite ? " is-favorite" : ""}" type="button" data-character-favorite="${esc(item.id)}" aria-label="${action}角色“${esc(item.name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
|
|
8686
|
+
}
|
|
8687
|
+
|
|
8688
|
+
const recordFavoriteConfigs = Object.freeze({
|
|
8689
|
+
character: { module: "characters", resource: "characters", label: "角色", nameField: "name" },
|
|
8690
|
+
draft: { module: "drafts", resource: "drafts", label: "想法", nameField: "title" },
|
|
8691
|
+
setting: { module: "settings", resource: "settings", label: "设定", nameField: "title" },
|
|
8692
|
+
organization: { module: "organizations", resource: "organizations", label: "组织", nameField: "name" }
|
|
8693
|
+
});
|
|
8694
|
+
|
|
8695
|
+
function recordFavoriteButton(type, item, { cardControl = false } = {}) {
|
|
8696
|
+
const config = recordFavoriteConfigs[type];
|
|
8697
|
+
if (!config) return "";
|
|
8698
|
+
const isFavorite = item.isFavorite === true;
|
|
8699
|
+
const canFavorite = canEditModule(config.module);
|
|
8700
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8701
|
+
const title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8702
|
+
const name = String(item[config.nameField] ?? "");
|
|
8703
|
+
return `<button class="record-favorite-button${cardControl ? " is-card-control" : ""}${isFavorite ? " is-favorite" : ""}" type="button" data-record-favorite="${esc(item.id)}" data-record-favorite-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
async function toggleRecordFavorite(type, item, render) {
|
|
8707
|
+
const config = recordFavoriteConfigs[type];
|
|
8708
|
+
if (!config) return;
|
|
8709
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8710
|
+
method: "PATCH",
|
|
8711
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8712
|
+
});
|
|
8713
|
+
await render();
|
|
8714
|
+
const favoriteButton = [...$("#module-content").querySelectorAll("[data-record-favorite]")].find((button) => (
|
|
8715
|
+
button.dataset.recordFavoriteType === type && button.dataset.recordFavorite === String(updated.id)
|
|
8716
|
+
));
|
|
8717
|
+
favoriteButton?.focus({ preventScroll: true });
|
|
8718
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8719
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8720
|
+
}
|
|
8721
|
+
|
|
8722
|
+
function bindRecordFavoriteButtons(type, items, render) {
|
|
8723
|
+
const config = recordFavoriteConfigs[type];
|
|
8724
|
+
if (!config) return;
|
|
8725
|
+
$("#module-content").querySelectorAll(`[data-record-favorite-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
|
|
8726
|
+
const item = items.find((candidate) => candidate.id === button.dataset.recordFavorite);
|
|
8727
|
+
if (!item || !canEditModule(config.module)) return;
|
|
8728
|
+
button.disabled = true;
|
|
8729
|
+
try {
|
|
8730
|
+
await toggleRecordFavorite(type, item, render);
|
|
8731
|
+
} catch (error) {
|
|
8732
|
+
button.disabled = false;
|
|
8733
|
+
toast(`${config.label}收藏状态更新失败:${error.message}`, "error");
|
|
8734
|
+
}
|
|
8735
|
+
}));
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
function syncEntityDetailFavoriteButton(button, type, item) {
|
|
8739
|
+
const config = recordFavoriteConfigs[type];
|
|
8740
|
+
const visible = Boolean(config && item);
|
|
8741
|
+
button.classList.toggle("hidden", !visible);
|
|
8742
|
+
button.innerHTML = characterFavoriteIconMarkup();
|
|
8743
|
+
if (!visible) {
|
|
8744
|
+
button.disabled = true;
|
|
8745
|
+
button.classList.remove("is-favorite");
|
|
8746
|
+
button.setAttribute("aria-pressed", "false");
|
|
8747
|
+
button.removeAttribute("aria-label");
|
|
8748
|
+
button.removeAttribute("title");
|
|
8749
|
+
return;
|
|
8750
|
+
}
|
|
8751
|
+
const isFavorite = item.isFavorite === true;
|
|
8752
|
+
const canFavorite = canEditModule(config.module);
|
|
8753
|
+
const action = isFavorite ? "取消收藏" : "收藏";
|
|
8754
|
+
const name = String(item[config.nameField] ?? "");
|
|
8755
|
+
button.classList.toggle("is-favorite", isFavorite);
|
|
8756
|
+
button.setAttribute("aria-pressed", String(isFavorite));
|
|
8757
|
+
button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
|
|
8758
|
+
button.title = canFavorite ? action : `当前账户没有${config.label}模块写入权限`;
|
|
8759
|
+
button.disabled = !canFavorite;
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
|
|
8763
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8764
|
+
button.onclick = async () => {
|
|
8765
|
+
const config = recordFavoriteConfigs[type];
|
|
8766
|
+
const item = getItem();
|
|
8767
|
+
if (!config || !item || !canEditModule(config.module)) return;
|
|
8768
|
+
button.disabled = true;
|
|
8769
|
+
try {
|
|
8770
|
+
const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/favorite`, {
|
|
8771
|
+
method: "PATCH",
|
|
8772
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
8773
|
+
});
|
|
8774
|
+
onUpdated(updated);
|
|
8775
|
+
syncEntityDetailFavoriteButton(button, type, getItem() ?? updated);
|
|
8776
|
+
button.focus({ preventScroll: true });
|
|
8777
|
+
const name = String(updated[config.nameField] ?? "");
|
|
8778
|
+
toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
|
|
8779
|
+
} catch (error) {
|
|
8780
|
+
syncEntityDetailFavoriteButton(button, type, getItem());
|
|
8781
|
+
toast(`${config.label}收藏状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
|
|
8782
|
+
}
|
|
8783
|
+
};
|
|
8784
|
+
}
|
|
8785
|
+
|
|
8536
8786
|
function recordCardEditButton(attribute, id, label) {
|
|
8537
8787
|
return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
|
|
8538
8788
|
}
|
|
@@ -8868,9 +9118,10 @@ function openReviewDetailDialog(item) {
|
|
|
8868
9118
|
}
|
|
8869
9119
|
|
|
8870
9120
|
function settingRecordActions(item) {
|
|
8871
|
-
|
|
9121
|
+
const recordAction = canEditModule("settings")
|
|
8872
9122
|
? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
|
|
8873
9123
|
: recordHistoryButton("setting", item.id, item.title);
|
|
9124
|
+
return `${recordFavoriteButton("setting", item)}${recordAction}`;
|
|
8874
9125
|
}
|
|
8875
9126
|
|
|
8876
9127
|
function draftTypeLabel(draftType) {
|
|
@@ -8924,6 +9175,7 @@ async function deleteDraft(item) {
|
|
|
8924
9175
|
}
|
|
8925
9176
|
|
|
8926
9177
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
9178
|
+
let draftDialogItem = item;
|
|
8927
9179
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
8928
9180
|
const volumeOptions = [["", "全局(不绑定分卷)"], ...(state.work?.volumes ?? []).map((volume) => [volume.id, volume.title])];
|
|
8929
9181
|
const settingModuleOptions = [["", "全局(不绑定设定模块)"], ...draftSettingModules];
|
|
@@ -8981,6 +9233,18 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
8981
9233
|
else control.readOnly = true;
|
|
8982
9234
|
});
|
|
8983
9235
|
}
|
|
9236
|
+
if (item && viewOnly) {
|
|
9237
|
+
const headerActions = $("#dialog-context-actions");
|
|
9238
|
+
headerActions.innerHTML = `<button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
|
|
9239
|
+
bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
|
|
9240
|
+
draftDialogItem = updated;
|
|
9241
|
+
void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
|
|
9242
|
+
});
|
|
9243
|
+
$("#draft-dialog-edit")?.addEventListener("click", () => {
|
|
9244
|
+
$("#form-dialog").close();
|
|
9245
|
+
openDraftDialog(draftDialogItem);
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
8984
9248
|
}
|
|
8985
9249
|
|
|
8986
9250
|
async function renderDrafts(page = moduleListPages.drafts) {
|
|
@@ -9014,9 +9278,9 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9014
9278
|
<details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
|
|
9015
9279
|
<div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
9016
9280
|
</section>`;
|
|
9017
|
-
const actions = (item) => canEditModule("drafts")
|
|
9281
|
+
const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
|
|
9018
9282
|
? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
9019
|
-
: recordHistoryButton("draft", item.id, item.title)
|
|
9283
|
+
: recordHistoryButton("draft", item.id, item.title)}`;
|
|
9020
9284
|
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
9021
9285
|
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
9022
9286
|
<small>${esc(draftTypeLabel(item.draftType))} · ${esc(draftBindingLabel(item))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
@@ -9072,6 +9336,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
9072
9336
|
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
9073
9337
|
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
9074
9338
|
}));
|
|
9339
|
+
bindRecordFavoriteButtons("draft", pageResult.items, async () => {
|
|
9340
|
+
moduleListPages.drafts = 1;
|
|
9341
|
+
await renderDrafts(1);
|
|
9342
|
+
});
|
|
9075
9343
|
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
9076
9344
|
}
|
|
9077
9345
|
|
|
@@ -9135,6 +9403,10 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9135
9403
|
bindModulePagination("settings", renderSettingResults);
|
|
9136
9404
|
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
9137
9405
|
$("#setting-filter-results").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
9406
|
+
bindRecordFavoriteButtons("setting", pageResult.items, async () => {
|
|
9407
|
+
moduleListPages.settings = 1;
|
|
9408
|
+
await renderSettings(1);
|
|
9409
|
+
});
|
|
9138
9410
|
bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
|
|
9139
9411
|
};
|
|
9140
9412
|
|
|
@@ -9171,6 +9443,21 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
9171
9443
|
renderSettingResults(page);
|
|
9172
9444
|
}
|
|
9173
9445
|
|
|
9446
|
+
async function toggleCharacterFavorite(item) {
|
|
9447
|
+
const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/favorite`, {
|
|
9448
|
+
method: "PATCH",
|
|
9449
|
+
body: { isFavorite: item.isFavorite !== true }
|
|
9450
|
+
});
|
|
9451
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
9452
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
9453
|
+
renderAiRoleplayCharacterSelect();
|
|
9454
|
+
}
|
|
9455
|
+
characterListPage = 1;
|
|
9456
|
+
await renderCharacters(1);
|
|
9457
|
+
$("#module-content").querySelector(`[data-character-favorite="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
|
|
9458
|
+
toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
|
|
9459
|
+
}
|
|
9460
|
+
|
|
9174
9461
|
async function renderCharacters(page = characterListPage) {
|
|
9175
9462
|
const hasCharacterFilters = characterFilters.raceIds.length > 0
|
|
9176
9463
|
|| characterFilters.organizationIds.length > 0
|
|
@@ -9193,14 +9480,14 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9193
9480
|
[state.races, state.organizations] = [races, organizations];
|
|
9194
9481
|
mountModuleCount(characterPage.total);
|
|
9195
9482
|
const layout = readModuleLayout();
|
|
9196
|
-
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)
|
|
9483
|
+
const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
|
|
9197
9484
|
const characterLockBadge = (item) => item.lockedFields.length
|
|
9198
9485
|
? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
|
|
9199
9486
|
: "";
|
|
9200
9487
|
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
9201
9488
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
9202
9489
|
return `
|
|
9203
|
-
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
9490
|
+
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
9204
9491
|
<div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
|
|
9205
9492
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
9206
9493
|
<div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
|
|
@@ -9266,6 +9553,17 @@ async function renderCharacters(page = characterListPage) {
|
|
|
9266
9553
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
9267
9554
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
9268
9555
|
bindModuleLayoutToggle(renderCharacters);
|
|
9556
|
+
$("#module-content").querySelectorAll("[data-character-favorite]").forEach((button) => button.addEventListener("click", async () => {
|
|
9557
|
+
const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterFavorite);
|
|
9558
|
+
if (!item || !canEditModule("characters")) return;
|
|
9559
|
+
button.disabled = true;
|
|
9560
|
+
try {
|
|
9561
|
+
await toggleCharacterFavorite(item);
|
|
9562
|
+
} catch (error) {
|
|
9563
|
+
button.disabled = false;
|
|
9564
|
+
toast(`角色收藏状态更新失败:${error.message}`, "error");
|
|
9565
|
+
}
|
|
9566
|
+
}));
|
|
9269
9567
|
const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
9270
9568
|
$("#character-race-filter").addEventListener("change", async () => {
|
|
9271
9569
|
characterFiltersPanelOpen = true;
|
|
@@ -9420,14 +9718,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9420
9718
|
moduleListPages.organizations = pageResult.page;
|
|
9421
9719
|
const layout = readModuleLayout();
|
|
9422
9720
|
const canEditOrganizations = canEditModule("organizations");
|
|
9423
|
-
const organizationActions = (item) => canEditOrganizations
|
|
9721
|
+
const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
|
|
9424
9722
|
? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
|
|
9425
|
-
: recordHistoryButton("organization", item.id, item.name)
|
|
9723
|
+
: recordHistoryButton("organization", item.id, item.name)}`;
|
|
9426
9724
|
const organizationCardActions = (item) => canEditOrganizations
|
|
9427
|
-
? organizationActions(item)
|
|
9725
|
+
? organizationActions(item, { cardControl: true })
|
|
9428
9726
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
9429
9727
|
const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
|
|
9430
|
-
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
9728
|
+
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
9431
9729
|
<h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
9432
9730
|
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
9433
9731
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
@@ -9452,6 +9750,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
|
|
|
9452
9750
|
bindModulePagination("organizations", renderOrganizations);
|
|
9453
9751
|
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
9454
9752
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
9753
|
+
bindRecordFavoriteButtons("organization", pageResult.items, async () => {
|
|
9754
|
+
moduleListPages.organizations = 1;
|
|
9755
|
+
await renderOrganizations(1);
|
|
9756
|
+
});
|
|
9455
9757
|
bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
|
|
9456
9758
|
}
|
|
9457
9759
|
|
|
@@ -11743,6 +12045,9 @@ function appendTokenUsageDetailRow(parent, label, usage, isSummary = false) {
|
|
|
11743
12045
|
cell.textContent = tokenUsageDetailCount(usage?.[key]);
|
|
11744
12046
|
row.append(cell);
|
|
11745
12047
|
});
|
|
12048
|
+
const price = document.createElement("td");
|
|
12049
|
+
price.textContent = formatEstimatedCost(usage?.estimatedCost);
|
|
12050
|
+
row.append(price);
|
|
11746
12051
|
parent.append(row);
|
|
11747
12052
|
}
|
|
11748
12053
|
|
|
@@ -11778,7 +12083,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11778
12083
|
const description = document.createElement("p");
|
|
11779
12084
|
description.id = "token-usage-details-description";
|
|
11780
12085
|
description.className = "token-usage-details-note";
|
|
11781
|
-
description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read
|
|
12086
|
+
description.textContent = "Raw Input 为不含缓存的输入 Token;Cache Write 与 Cache Read 也是输入组成部分,并已包含在总计中。估算价格按美元显示,未匹配价格的模型显示为暂无价格。汇总行包含当前范围内全部模型。";
|
|
11782
12087
|
body.append(description);
|
|
11783
12088
|
|
|
11784
12089
|
const tableScroll = document.createElement("div");
|
|
@@ -11790,7 +12095,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11790
12095
|
table.append(caption);
|
|
11791
12096
|
const head = document.createElement("thead");
|
|
11792
12097
|
const headRow = document.createElement("tr");
|
|
11793
|
-
["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用"].forEach((label) => {
|
|
12098
|
+
["模型", "Raw Input", "Cache Write", "Cache Read", "Output", "总计", "调用", "估算调用", "估算价格"].forEach((label) => {
|
|
11794
12099
|
const cell = document.createElement("th");
|
|
11795
12100
|
cell.scope = "col";
|
|
11796
12101
|
cell.textContent = label;
|
|
@@ -11804,7 +12109,7 @@ function showTokenUsageDetails(usage, title, trigger) {
|
|
|
11804
12109
|
if (!models.length) {
|
|
11805
12110
|
const emptyRow = document.createElement("tr");
|
|
11806
12111
|
const emptyCell = document.createElement("td");
|
|
11807
|
-
emptyCell.colSpan =
|
|
12112
|
+
emptyCell.colSpan = 9;
|
|
11808
12113
|
emptyCell.textContent = "还没有模型用量记录。";
|
|
11809
12114
|
emptyRow.append(emptyCell);
|
|
11810
12115
|
bodyRows.append(emptyRow);
|
|
@@ -11977,7 +12282,7 @@ async function renderBookAiSettings() {
|
|
|
11977
12282
|
);
|
|
11978
12283
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
11979
12284
|
"beforeend",
|
|
11980
|
-
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label><label><input name="agent-tool" type="checkbox" value="image" ${agentTools.has("image") ? "checked" : ""}><span><strong>读取设定图片</strong><small>读取设定正文引用的单张图片附件,并由多模态模型返回图片理解内容。</small></span></label><label><input name="agent-tool" type="checkbox" value="calculate_time" ${agentTools.has("calculate_time") ? "checked" : ""}><span><strong>计算日期</strong><small
|
|
12285
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label><label><input name="agent-tool" type="checkbox" value="image" ${agentTools.has("image") ? "checked" : ""}><span><strong>读取设定图片</strong><small>读取设定正文引用的单张图片附件,并由多模态模型返回图片理解内容。</small></span></label><label><input name="agent-tool" type="checkbox" value="calculate_time" ${agentTools.has("calculate_time") ? "checked" : ""}><span><strong>计算日期</strong><small>计算两个 YYYY-MM-DD 日期之间的天数差,不读取作品内容。</small></span></label>`
|
|
11981
12286
|
);
|
|
11982
12287
|
if (!canEditModule("ai-settings")) {
|
|
11983
12288
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -12691,6 +12996,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
12691
12996
|
const submitLabel = options.submitLabel ?? "保存";
|
|
12692
12997
|
let submitting = false;
|
|
12693
12998
|
let disabledStates = [];
|
|
12999
|
+
$("#dialog-context-actions").replaceChildren();
|
|
12694
13000
|
$("#dialog-title").textContent = title;
|
|
12695
13001
|
$("#dialog-title").classList.toggle("hidden", Boolean(options.titleInput));
|
|
12696
13002
|
titleInput.classList.toggle("hidden", !options.titleInput);
|
|
@@ -13160,6 +13466,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
|
|
|
13160
13466
|
const editButton = $("#setting-editor-edit");
|
|
13161
13467
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
|
|
13162
13468
|
editButton.onclick = () => openSettingEditor(settingEditorItem);
|
|
13469
|
+
bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
|
|
13470
|
+
settingEditorItem = updated;
|
|
13471
|
+
state.settings = upsertEntityCollection(state.settings, updated);
|
|
13472
|
+
});
|
|
13163
13473
|
const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
|
|
13164
13474
|
syncSettingEditorChrome(viewOnly);
|
|
13165
13475
|
$("#setting-editor-history").onclick = async () => {
|
|
@@ -14186,11 +14496,12 @@ function renderCharacterEditorFields(item) {
|
|
|
14186
14496
|
(canReadModule("editor")
|
|
14187
14497
|
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
14188
14498
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
14189
|
-
characterEditorSection("profile", "人物档案", "
|
|
14499
|
+
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力、公开人设和便于创作时快速理解的简介。",
|
|
14190
14500
|
field("code", "编号", "text", item?.code) +
|
|
14191
14501
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
14192
14502
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
14193
|
-
field("summary", "人物简介", "textarea", item?.profile?.summary)
|
|
14503
|
+
field("summary", "人物简介", "textarea", item?.profile?.summary) +
|
|
14504
|
+
'<div class="form-field"><span>人设摘要</span><small>关系扮演时作为公开人设注入对方可见的角色卡,不会包含私密档案或 Markdown 章节。</small><textarea name="personaSummary" maxlength="20000" aria-label="人设摘要">' + esc(item?.profile?.personaSummary ?? "") + "</textarea></div>"),
|
|
14194
14505
|
characterEditorSection("settings", "扩展设定", "可用短属性和 Markdown 长章节承载形态、能力、生态、经历与研究记录。",
|
|
14195
14506
|
field("details", "扩展属性", "key-value-list", item?.attributes?.details) +
|
|
14196
14507
|
'<div id="character-markdown-sections" class="character-markdown-sections"></div>'),
|
|
@@ -14238,7 +14549,8 @@ function collectCharacterBody(form) {
|
|
|
14238
14549
|
profile: {
|
|
14239
14550
|
...profile,
|
|
14240
14551
|
motivation: String(form.get("motivation") ?? "").trim(),
|
|
14241
|
-
summary: String(form.get("summary") ?? "").trim()
|
|
14552
|
+
summary: String(form.get("summary") ?? "").trim(),
|
|
14553
|
+
personaSummary: String(form.get("personaSummary") ?? "").trim()
|
|
14242
14554
|
},
|
|
14243
14555
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
14244
14556
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
@@ -14390,6 +14702,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
|
14390
14702
|
const editButton = $("#character-editor-edit");
|
|
14391
14703
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
|
|
14392
14704
|
editButton.onclick = () => void openCharacterEditor(characterEditorItem);
|
|
14705
|
+
bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
|
|
14706
|
+
characterEditorItem = updated;
|
|
14707
|
+
if (loadedAiReferencesWorkId === state.work?.id) {
|
|
14708
|
+
state.characters = upsertEntityCollection(state.characters, updated);
|
|
14709
|
+
renderAiRoleplayCharacterSelect();
|
|
14710
|
+
}
|
|
14711
|
+
});
|
|
14393
14712
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
14394
14713
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
14395
14714
|
});
|
|
@@ -14576,6 +14895,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
|
14576
14895
|
editButton.textContent = `编辑${label}`;
|
|
14577
14896
|
editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
|
|
14578
14897
|
editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
|
|
14898
|
+
bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
|
|
14899
|
+
knowledgeEditorItem = updated;
|
|
14900
|
+
state.organizations = upsertEntityCollection(state.organizations, updated);
|
|
14901
|
+
});
|
|
14579
14902
|
const form = $("#knowledge-editor-form");
|
|
14580
14903
|
form.onsubmit = async (event) => {
|
|
14581
14904
|
event.preventDefault();
|
|
@@ -15679,10 +16002,25 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15679
16002
|
if (aiRequestManager.hasActive(tab.id)) return;
|
|
15680
16003
|
const composerSnapshot = captureAiPromptComposer();
|
|
15681
16004
|
const requestComposerSnapshot = retry
|
|
15682
|
-
? {
|
|
16005
|
+
? {
|
|
16006
|
+
text: retry.prompt,
|
|
16007
|
+
citations: retry.citations ?? [],
|
|
16008
|
+
references: [],
|
|
16009
|
+
images: retry.images ?? [],
|
|
16010
|
+
sceneDirection: retry.sceneDirection ?? "",
|
|
16011
|
+
scenePin: captureAiScenePin()
|
|
16012
|
+
}
|
|
15683
16013
|
: composerSnapshot;
|
|
15684
16014
|
const instruction = requestComposerSnapshot.text.trim();
|
|
15685
|
-
|
|
16015
|
+
const sceneDirection = $("#ai-task").value === "roleplay"
|
|
16016
|
+
? String(requestComposerSnapshot.sceneDirection ?? "").trim()
|
|
16017
|
+
: "";
|
|
16018
|
+
const scenePin = $("#ai-task").value === "roleplay"
|
|
16019
|
+
? normalizeRoleplayScenePin(requestComposerSnapshot.scenePin)
|
|
16020
|
+
: emptyRoleplayScenePin();
|
|
16021
|
+
if (!instruction && !sceneDirection) {
|
|
16022
|
+
return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入指令", "error");
|
|
16023
|
+
}
|
|
15686
16024
|
if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
|
|
15687
16025
|
const requestScope = currentAiRequestScope();
|
|
15688
16026
|
if (!requestScope) return toast("请先选择章节", "error");
|
|
@@ -15779,6 +16117,8 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
15779
16117
|
if (taskType === "chat") {
|
|
15780
16118
|
const streamed = await streamChat(requestHolder, aiRetryStreamRequestBody({
|
|
15781
16119
|
instruction,
|
|
16120
|
+
...(sceneDirection ? { sceneDirection } : {}),
|
|
16121
|
+
...($("#ai-task").value === "roleplay" ? { scenePin } : {}),
|
|
15782
16122
|
scope,
|
|
15783
16123
|
modelId,
|
|
15784
16124
|
citations,
|
|
@@ -16269,9 +16609,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16269
16609
|
const isInterrupted = role === "assistant" && metadata?.interrupted === true;
|
|
16270
16610
|
const interruptionCode = typeof metadata?.interruptionCode === "string" ? metadata.interruptionCode : "AI_STREAM_FAILED";
|
|
16271
16611
|
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure || isInterrupted ? " is-error" : ""}`;
|
|
16612
|
+
const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
|
|
16272
16613
|
const messageBody = isFailure
|
|
16273
16614
|
? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
|
|
16274
|
-
: renderMarkdown(text);
|
|
16615
|
+
: renderMarkdown(parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16275
16616
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
16276
16617
|
message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
|
|
16277
16618
|
event.preventDefault();
|
|
@@ -16293,6 +16634,18 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16293
16634
|
failureBadge.setAttribute("aria-label", `消息状态:${isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "失败"}`);
|
|
16294
16635
|
heading.firstElementChild?.append(failureBadge);
|
|
16295
16636
|
}
|
|
16637
|
+
if (parsedUserTurn?.hasMarkup && parsedUserTurn.sceneDirection) {
|
|
16638
|
+
const scene = document.createElement("div");
|
|
16639
|
+
scene.className = "user-message-scene";
|
|
16640
|
+
const sceneLabel = document.createElement("span");
|
|
16641
|
+
sceneLabel.className = "user-message-scene-label";
|
|
16642
|
+
sceneLabel.textContent = "旁白 / 场景";
|
|
16643
|
+
const sceneBody = document.createElement("div");
|
|
16644
|
+
sceneBody.className = "user-message-scene-body";
|
|
16645
|
+
sceneBody.textContent = parsedUserTurn.sceneDirection;
|
|
16646
|
+
scene.append(sceneLabel, sceneBody);
|
|
16647
|
+
message.querySelector(".message-body")?.before(scene);
|
|
16648
|
+
}
|
|
16296
16649
|
const mentionGroups = role === "user"
|
|
16297
16650
|
? [
|
|
16298
16651
|
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
@@ -16348,7 +16701,8 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
16348
16701
|
);
|
|
16349
16702
|
message.dataset.aiImageAttachmentIds = JSON.stringify(imageAttachments.map((attachment) => attachment.id));
|
|
16350
16703
|
appendAiMessageImageAttachments(message, imageAttachments);
|
|
16351
|
-
attachUserCopyAction(message, text);
|
|
16704
|
+
attachUserCopyAction(message, parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
|
|
16705
|
+
if (parsedUserTurn?.hasMarkup) message.dataset.sceneDirection = parsedUserTurn.sceneDirection;
|
|
16352
16706
|
}
|
|
16353
16707
|
if (role === "assistant" && !text.startsWith("调用失败:")) {
|
|
16354
16708
|
const selectedModelId = tab?.modelId ?? tab?.selectedModelId ?? $("#ai-model").value;
|
|
@@ -17012,16 +17366,12 @@ $("#account-settings-button").addEventListener("click", () => {
|
|
|
17012
17366
|
$("#profile-display-name").value = state.user?.displayName ?? "";
|
|
17013
17367
|
renderProfileAvatar();
|
|
17014
17368
|
$("#password-form").reset();
|
|
17015
|
-
$("#api-key-result").classList.add("hidden");
|
|
17016
|
-
$("#api-key-value").value = "";
|
|
17017
17369
|
$("#account-dialog").showModal();
|
|
17018
17370
|
api("/api/auth/api-key").then((status) => {
|
|
17019
|
-
|
|
17020
|
-
? `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`
|
|
17021
|
-
: "尚未生成 API Key。";
|
|
17022
|
-
$("#api-key-reset-button").textContent = status.configured ? "重置 API Key" : "生成 API Key";
|
|
17371
|
+
paintApiKeyStatus(status);
|
|
17023
17372
|
}).catch((error) => {
|
|
17024
17373
|
$("#api-key-status").textContent = error.message;
|
|
17374
|
+
$("#api-key-copy-button").hidden = true;
|
|
17025
17375
|
});
|
|
17026
17376
|
});
|
|
17027
17377
|
$("#account-dialog-close").addEventListener("click", () => $("#account-dialog").close());
|
|
@@ -17458,6 +17808,17 @@ function validatePasswordChangeConfirmation() {
|
|
|
17458
17808
|
}
|
|
17459
17809
|
$("#password-form input[name='newPassword']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17460
17810
|
$("#password-form input[name='passwordConfirmation']").addEventListener("input", validatePasswordChangeConfirmation);
|
|
17811
|
+
function paintApiKeyStatus(status) {
|
|
17812
|
+
const copyButton = $("#api-key-copy-button");
|
|
17813
|
+
copyButton.hidden = !status.configured;
|
|
17814
|
+
if (status.configured) {
|
|
17815
|
+
$("#api-key-status").textContent = `已配置 ${status.prefix}…${status.lastUsedAt ? `,最近使用:${formatDateTime(status.lastUsedAt)}` : ",尚未使用"}`;
|
|
17816
|
+
$("#api-key-reset-button").textContent = "重置 API Key";
|
|
17817
|
+
return;
|
|
17818
|
+
}
|
|
17819
|
+
$("#api-key-status").textContent = "尚未生成 API Key。";
|
|
17820
|
+
$("#api-key-reset-button").textContent = "生成 API Key";
|
|
17821
|
+
}
|
|
17461
17822
|
$("#api-key-reset-button").addEventListener("click", async () => {
|
|
17462
17823
|
if ($("#api-key-reset-button").textContent.includes("重置") && !(await confirmToast(
|
|
17463
17824
|
"重置后,所有使用旧 API Key 的 CLI 会立刻退出登录。确定继续吗?",
|
|
@@ -17465,25 +17826,31 @@ $("#api-key-reset-button").addEventListener("click", async () => {
|
|
|
17465
17826
|
))) return;
|
|
17466
17827
|
try {
|
|
17467
17828
|
const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
toast("新的 API Key
|
|
17829
|
+
paintApiKeyStatus({
|
|
17830
|
+
configured: true,
|
|
17831
|
+
prefix: result.prefix,
|
|
17832
|
+
lastUsedAt: null,
|
|
17833
|
+
copyable: result.copyable !== false
|
|
17834
|
+
});
|
|
17835
|
+
toast("新的 API Key 已生成,可点击复制");
|
|
17475
17836
|
} catch (error) { toast(error.message, "error"); }
|
|
17476
17837
|
});
|
|
17477
17838
|
$("#api-key-copy-button").addEventListener("click", async () => {
|
|
17478
|
-
const
|
|
17479
|
-
if (
|
|
17839
|
+
const button = $("#api-key-copy-button");
|
|
17840
|
+
if (button.hidden || button.disabled) return;
|
|
17841
|
+
button.disabled = true;
|
|
17480
17842
|
try {
|
|
17481
|
-
await
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17843
|
+
const result = await api("/api/auth/api-key/reveal", { method: "POST", body: {} });
|
|
17844
|
+
try {
|
|
17845
|
+
await navigator.clipboard.writeText(result.apiKey);
|
|
17846
|
+
toast("API Key 已复制");
|
|
17847
|
+
} catch {
|
|
17848
|
+
toast("无法自动复制,请检查浏览器剪贴板权限", "error");
|
|
17849
|
+
}
|
|
17850
|
+
} catch (error) {
|
|
17851
|
+
toast(error.message, "error");
|
|
17852
|
+
} finally {
|
|
17853
|
+
button.disabled = false;
|
|
17487
17854
|
}
|
|
17488
17855
|
});
|
|
17489
17856
|
$("#logout-button").addEventListener("click", async () => {
|
|
@@ -18087,6 +18454,15 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
|
|
|
18087
18454
|
}
|
|
18088
18455
|
$("#ai-attachment-input").click();
|
|
18089
18456
|
});
|
|
18457
|
+
$("#ai-scene-button").addEventListener("click", () => {
|
|
18458
|
+
toggleAiScenePanel();
|
|
18459
|
+
});
|
|
18460
|
+
for (const field of ["#ai-scene-direction", "#ai-scene-location", "#ai-scene-present", "#ai-scene-time"]) {
|
|
18461
|
+
$(field)?.addEventListener("input", () => {
|
|
18462
|
+
syncAiSceneComposer();
|
|
18463
|
+
persistActiveAiChatTab();
|
|
18464
|
+
});
|
|
18465
|
+
}
|
|
18090
18466
|
$("#ai-attachment-input").addEventListener("change", (event) => {
|
|
18091
18467
|
const input = event.currentTarget;
|
|
18092
18468
|
void addAiImageFiles(input.files).finally(() => { input.value = ""; });
|