@musnows/scriverse 0.8.1 → 0.8.3
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/dist/ai-conversation-export.js +3 -1
- package/dist/ai-conversation-export.js.map +1 -1
- package/dist/ai.js +68 -12
- package/dist/ai.js.map +1 -1
- package/dist/app.js +9 -4
- package/dist/app.js.map +1 -1
- package/dist/database.js +26 -2
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +2 -1
- package/dist/public/app.js +206 -34
- package/dist/public/index.html +10 -5
- package/dist/public/phone-client.js +6 -0
- package/dist/public/relationship-graph.js +11 -3
- package/dist/public/styles.css +19 -5
- package/dist/store.js +40 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3";
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
|
|
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";
|
|
@@ -24,7 +24,8 @@ import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/
|
|
|
24
24
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
25
25
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
26
26
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
27
|
-
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=
|
|
27
|
+
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260819-context-percent-v1";
|
|
28
|
+
import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
|
|
28
29
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
29
30
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
30
31
|
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
@@ -178,6 +179,7 @@ const state = {
|
|
|
178
179
|
aiConversationModelId: null,
|
|
179
180
|
aiConversations: [],
|
|
180
181
|
aiRoleplayCharacter: null,
|
|
182
|
+
aiRoleplayUserCharacter: null,
|
|
181
183
|
aiLastMessageAt: null,
|
|
182
184
|
settings: [],
|
|
183
185
|
dirty: false,
|
|
@@ -1330,12 +1332,15 @@ function constrainPanelLayout() {
|
|
|
1330
1332
|
}
|
|
1331
1333
|
|
|
1332
1334
|
function applyPanelLayout(persist = false) {
|
|
1335
|
+
const phoneClient = isPhoneClient();
|
|
1336
|
+
if (phoneClient && !aiConversationWorkspaceOpen) panelLayout.aiCollapsed = true;
|
|
1333
1337
|
constrainPanelLayout();
|
|
1334
1338
|
const app = $("#app");
|
|
1335
1339
|
app.style.setProperty("--left-panel-width", `${panelLayout.leftWidth}px`);
|
|
1336
1340
|
app.style.setProperty("--ai-panel-width", `${panelLayout.aiWidth}px`);
|
|
1337
1341
|
app.classList.toggle("left-panel-collapsed", panelLayout.leftCollapsed);
|
|
1338
1342
|
app.classList.toggle("ai-panel-collapsed", panelLayout.aiCollapsed);
|
|
1343
|
+
app.classList.toggle("phone-client", phoneClient);
|
|
1339
1344
|
const mobileViewport = isMobileViewport();
|
|
1340
1345
|
$("#left-panel-toggle").textContent = mobileViewport ? "›" : (panelLayout.leftCollapsed ? "›" : "‹");
|
|
1341
1346
|
$("#left-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
|
|
@@ -1355,7 +1360,7 @@ function applyPanelLayout(persist = false) {
|
|
|
1355
1360
|
|
|
1356
1361
|
function ensureAiPanelExpanded() {
|
|
1357
1362
|
if (!state.work || !canReadPermissionModule(state.work, "ai-chat")) return;
|
|
1358
|
-
if (isMobileViewport()) {
|
|
1363
|
+
if (isPhoneClient() || isMobileViewport()) {
|
|
1359
1364
|
setAiConversationWorkspaceVisible(true);
|
|
1360
1365
|
return;
|
|
1361
1366
|
}
|
|
@@ -2040,6 +2045,7 @@ function createAiChatTabState(input = {}) {
|
|
|
2040
2045
|
taskType: input.taskType ?? "chat",
|
|
2041
2046
|
contextScope: input.contextScope ?? { type: "none" },
|
|
2042
2047
|
roleplayCharacter: input.roleplayCharacter ?? null,
|
|
2048
|
+
roleplayUserCharacter: input.roleplayUserCharacter ?? null,
|
|
2043
2049
|
citations: input.citations ?? [],
|
|
2044
2050
|
references: input.references ?? [],
|
|
2045
2051
|
composer: input.composer ?? { text: "", citations: [], references: [] },
|
|
@@ -2067,6 +2073,7 @@ function persistActiveAiChatTab() {
|
|
|
2067
2073
|
tab.taskType = state.aiTaskType;
|
|
2068
2074
|
tab.contextScope = { ...state.aiContextScope };
|
|
2069
2075
|
tab.roleplayCharacter = state.aiRoleplayCharacter ? { ...state.aiRoleplayCharacter } : null;
|
|
2076
|
+
tab.roleplayUserCharacter = state.aiRoleplayUserCharacter ? { ...state.aiRoleplayUserCharacter } : null;
|
|
2070
2077
|
setAiChatTabComposerSnapshot(tab, captureAiPromptComposer());
|
|
2071
2078
|
tab.contextUsage = latestAiContextUsage;
|
|
2072
2079
|
tab.contextWarning = !$("#ai-context-warning").classList.contains("hidden");
|
|
@@ -2099,9 +2106,10 @@ function applyAiChatTabState(tab) {
|
|
|
2099
2106
|
$("#ai-conversation-title").textContent = tab.title || "新对话";
|
|
2100
2107
|
applyAiConversationTaskType(tab.taskType);
|
|
2101
2108
|
applyAiConversationContextScope(tab.contextScope);
|
|
2102
|
-
applyAiRoleplayCharacter(tab.roleplayCharacter);
|
|
2109
|
+
applyAiRoleplayCharacter(tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
2103
2110
|
const selectedModelId = tab.modelId ?? tab.selectedModelId;
|
|
2104
2111
|
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2112
|
+
syncAiModelPicker();
|
|
2105
2113
|
setAiPromptText(tab.composer.text);
|
|
2106
2114
|
renderAiCitations();
|
|
2107
2115
|
renderAiReferences();
|
|
@@ -2215,7 +2223,7 @@ function setAiConversationSwitcherVisible(visible) {
|
|
|
2215
2223
|
}
|
|
2216
2224
|
|
|
2217
2225
|
function setAiConversationWorkspaceVisible(visible) {
|
|
2218
|
-
const mobileWorkspace = isMobileViewport();
|
|
2226
|
+
const mobileWorkspace = isPhoneClient() || isMobileViewport();
|
|
2219
2227
|
aiConversationWorkspaceOpen = Boolean(visible);
|
|
2220
2228
|
panelLayout.aiCollapsed = aiConversationWorkspaceOpen ? false : mobileWorkspace;
|
|
2221
2229
|
$("#app").classList.toggle("ai-workspace-mode", aiConversationWorkspaceOpen);
|
|
@@ -2340,7 +2348,7 @@ function resetAiChatTabs() {
|
|
|
2340
2348
|
panels.replaceChildren();
|
|
2341
2349
|
aiChatTabManager.reset();
|
|
2342
2350
|
const tab = createAiChatTabState();
|
|
2343
|
-
resetAiFeed(tab.feed, tab.roleplayCharacter);
|
|
2351
|
+
resetAiFeed(tab.feed, tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
2344
2352
|
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
2345
2353
|
}
|
|
2346
2354
|
|
|
@@ -2385,13 +2393,18 @@ function updateMessageCreatedAt(message, createdAt) {
|
|
|
2385
2393
|
}
|
|
2386
2394
|
}
|
|
2387
2395
|
|
|
2388
|
-
function resetAiFeed(
|
|
2396
|
+
function resetAiFeed(
|
|
2397
|
+
feed = $("#ai-feed"),
|
|
2398
|
+
roleplayCharacter = state.aiRoleplayCharacter,
|
|
2399
|
+
roleplayUserCharacter = state.aiRoleplayUserCharacter
|
|
2400
|
+
) {
|
|
2389
2401
|
const tab = aiChatTabManager.get(feed?.dataset.aiTabId);
|
|
2390
2402
|
if (tab) tab.lastMessageAt = null;
|
|
2391
2403
|
if (isActiveAiChatTab(tab)) state.aiLastMessageAt = null;
|
|
2392
2404
|
const roleplayName = roleplayCharacter?.name;
|
|
2405
|
+
const roleplayUserName = roleplayUserCharacter?.name;
|
|
2393
2406
|
feed.innerHTML = roleplayName
|
|
2394
|
-
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}
|
|
2407
|
+
? `<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>`
|
|
2395
2408
|
: '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
2396
2409
|
}
|
|
2397
2410
|
|
|
@@ -2400,6 +2413,10 @@ function aiAssistantLabel(suffix = "", roleplayCharacter = state.aiRoleplayChara
|
|
|
2400
2413
|
return suffix ? `${name} · ${suffix}` : name;
|
|
2401
2414
|
}
|
|
2402
2415
|
|
|
2416
|
+
function aiUserLabel(roleplayUserCharacter = state.aiRoleplayUserCharacter) {
|
|
2417
|
+
return roleplayUserCharacter?.name || "作者";
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2403
2420
|
function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "当前上下文已压缩", title = "" } = {}) {
|
|
2404
2421
|
const divider = document.createElement("div");
|
|
2405
2422
|
divider.className = "ai-context-compaction-divider";
|
|
@@ -2901,6 +2918,7 @@ function aiConversationHistoryMeta(conversation) {
|
|
|
2901
2918
|
aiConversationContextScopeLabel(conversation.contextScope)
|
|
2902
2919
|
];
|
|
2903
2920
|
if (conversation.roleplayCharacter?.name) parts.push(`扮演 ${conversation.roleplayCharacter.name}`);
|
|
2921
|
+
if (conversation.roleplayUserCharacter?.name) parts.push(`用户扮演 ${conversation.roleplayUserCharacter.name}`);
|
|
2904
2922
|
parts.push(formatDateTime(conversation.updatedAt));
|
|
2905
2923
|
return parts.join(" · ");
|
|
2906
2924
|
}
|
|
@@ -3117,12 +3135,13 @@ function applyConversationToAiChatTab(tab, conversation) {
|
|
|
3117
3135
|
tab.taskType = conversation.taskType ?? "chat";
|
|
3118
3136
|
tab.contextScope = conversation.contextScope ?? { type: "none" };
|
|
3119
3137
|
tab.roleplayCharacter = conversation.roleplayCharacter ?? null;
|
|
3138
|
+
tab.roleplayUserCharacter = conversation.roleplayUserCharacter ?? null;
|
|
3120
3139
|
tab.citations = [];
|
|
3121
3140
|
tab.references = [];
|
|
3122
3141
|
tab.composer = { text: "", citations: [], references: [] };
|
|
3123
3142
|
tab.contextUsage = null;
|
|
3124
3143
|
tab.contextWarning = conversation.contextWarningPending === true;
|
|
3125
|
-
resetAiFeed(tab.feed, tab.roleplayCharacter);
|
|
3144
|
+
resetAiFeed(tab.feed, tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
3126
3145
|
for (const message of conversation.messages ?? []) {
|
|
3127
3146
|
appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id, { tab });
|
|
3128
3147
|
}
|
|
@@ -3197,10 +3216,65 @@ function renderAiRoleplayCharacterSelect() {
|
|
|
3197
3216
|
? aiConversationOptionLockedMessage
|
|
3198
3217
|
: "为当前对话选择角色卡;角色扮演时 Agent 只能查询与该角色自身有关的记忆"
|
|
3199
3218
|
: "当前账户没有角色模块读取权限";
|
|
3219
|
+
renderAiRoleplayUserCharacterSelect();
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
function renderAiRoleplayUserCharacterSelect() {
|
|
3223
|
+
const select = $("#ai-roleplay-user-character");
|
|
3224
|
+
const selectedId = String(state.aiRoleplayUserCharacter?.id ?? "");
|
|
3225
|
+
const aiCharacterId = String(state.aiRoleplayCharacter?.id ?? "");
|
|
3226
|
+
const availableCharacters = state.characters.filter((character) => (
|
|
3227
|
+
!character.mergedIntoCharacterId && String(character.id) !== aiCharacterId
|
|
3228
|
+
));
|
|
3229
|
+
const options = [{ id: "", name: "选择我的角色(可选)" }, ...availableCharacters.map((character) => ({
|
|
3230
|
+
id: String(character.id),
|
|
3231
|
+
name: `${String(character.name)}${character.isDead ? "(已死亡)" : ""}`
|
|
3232
|
+
}))];
|
|
3233
|
+
if (selectedId && !options.some((option) => option.id === selectedId)) {
|
|
3234
|
+
options.push({ id: selectedId, name: String(state.aiRoleplayUserCharacter.name) });
|
|
3235
|
+
}
|
|
3236
|
+
select.replaceChildren(...options.map((option) => {
|
|
3237
|
+
const element = document.createElement("option");
|
|
3238
|
+
element.value = option.id;
|
|
3239
|
+
element.textContent = option.name;
|
|
3240
|
+
return element;
|
|
3241
|
+
}));
|
|
3242
|
+
select.value = selectedId;
|
|
3243
|
+
const canSelectCharacter = Boolean(state.work)
|
|
3244
|
+
&& canReadModule("characters")
|
|
3245
|
+
&& canWritePermissionModule(state.work, "ai-chat");
|
|
3246
|
+
select.disabled = aiInteractionBusy() || !canSelectCharacter || !state.aiRoleplayCharacter;
|
|
3247
|
+
select.title = !canSelectCharacter
|
|
3248
|
+
? "当前账户没有角色模块读取权限"
|
|
3249
|
+
: !state.aiRoleplayCharacter
|
|
3250
|
+
? "请先选择 AI 扮演的角色"
|
|
3251
|
+
: state.aiPromptSent
|
|
3252
|
+
? aiConversationOptionLockedMessage
|
|
3253
|
+
: "选择后,AI 会将每条用户消息视为该角色的发言或行动";
|
|
3200
3254
|
}
|
|
3201
3255
|
|
|
3202
3256
|
const aiConversationOptionLockedMessage = "会话选项在会话开始后不支持修改,若需要修改,请新建会话";
|
|
3203
3257
|
|
|
3258
|
+
function selectedAiModelLabel() {
|
|
3259
|
+
return $("#ai-model").selectedOptions[0]?.textContent?.trim() || "尚未选择模型";
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function syncAiModelPicker() {
|
|
3263
|
+
const select = $("#ai-model");
|
|
3264
|
+
const button = $("#ai-model-picker");
|
|
3265
|
+
const interactionBusy = aiInteractionBusy();
|
|
3266
|
+
const selectedLabel = selectedAiModelLabel();
|
|
3267
|
+
const label = state.aiPromptSent
|
|
3268
|
+
? `当前模型:${selectedLabel}。${aiConversationOptionLockedMessage}`
|
|
3269
|
+
: `选择实际使用模型:${selectedLabel}`;
|
|
3270
|
+
select.disabled = interactionBusy;
|
|
3271
|
+
select.title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
|
|
3272
|
+
button.disabled = interactionBusy;
|
|
3273
|
+
button.title = label;
|
|
3274
|
+
button.setAttribute("aria-label", label);
|
|
3275
|
+
if (interactionBusy) setAiModelPickerVisible(false);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3204
3278
|
function notifyAiConversationOptionLocked(select) {
|
|
3205
3279
|
if (!state.aiPromptSent) return false;
|
|
3206
3280
|
const now = Date.now();
|
|
@@ -3224,19 +3298,21 @@ function blockLockedAiConversationOptionKeydown(event) {
|
|
|
3224
3298
|
|
|
3225
3299
|
function syncAiTaskOptions() {
|
|
3226
3300
|
const roleplaySelected = $("#ai-task").value === "roleplay";
|
|
3301
|
+
const relationshipRoleplaySelectable = roleplaySelected && Boolean(state.aiRoleplayCharacter);
|
|
3227
3302
|
const interactionBusy = aiInteractionBusy();
|
|
3228
3303
|
$("#ai-scope").classList.toggle("hidden", roleplaySelected);
|
|
3229
3304
|
$("#ai-scope").setAttribute("aria-hidden", String(roleplaySelected));
|
|
3230
3305
|
$("#ai-roleplay-character").classList.toggle("hidden", !roleplaySelected);
|
|
3231
3306
|
$("#ai-roleplay-character").setAttribute("aria-hidden", String(!roleplaySelected));
|
|
3307
|
+
$("#ai-roleplay-user-character").classList.toggle("hidden", !relationshipRoleplaySelectable);
|
|
3308
|
+
$("#ai-roleplay-user-character").setAttribute("aria-hidden", String(!relationshipRoleplaySelectable));
|
|
3232
3309
|
$("#ai-task").disabled = interactionBusy;
|
|
3233
3310
|
$("#ai-task").title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
|
|
3234
3311
|
$("#ai-scope").disabled = interactionBusy || roleplaySelected;
|
|
3235
3312
|
$("#ai-scope").title = state.aiPromptSent
|
|
3236
3313
|
? aiConversationOptionLockedMessage
|
|
3237
3314
|
: roleplaySelected ? "角色扮演模式只使用角色自身的记忆" : "";
|
|
3238
|
-
|
|
3239
|
-
$("#ai-model").title = state.aiPromptSent ? aiConversationOptionLockedMessage : "";
|
|
3315
|
+
syncAiModelPicker();
|
|
3240
3316
|
}
|
|
3241
3317
|
|
|
3242
3318
|
function applyAiConversationTaskType(taskType) {
|
|
@@ -3262,15 +3338,19 @@ function applyAiConversationContextScope(scope) {
|
|
|
3262
3338
|
syncAiTaskOptions();
|
|
3263
3339
|
}
|
|
3264
3340
|
|
|
3265
|
-
function applyAiRoleplayCharacter(character) {
|
|
3341
|
+
function applyAiRoleplayCharacter(character, userCharacter = null) {
|
|
3266
3342
|
state.aiRoleplayCharacter = character?.id ? character : null;
|
|
3343
|
+
state.aiRoleplayUserCharacter = state.aiRoleplayCharacter && userCharacter?.id ? userCharacter : null;
|
|
3267
3344
|
const tab = activeAiChatTab();
|
|
3268
3345
|
if (tab) tab.roleplayCharacter = state.aiRoleplayCharacter ? { ...state.aiRoleplayCharacter } : null;
|
|
3346
|
+
if (tab) tab.roleplayUserCharacter = state.aiRoleplayUserCharacter ? { ...state.aiRoleplayUserCharacter } : null;
|
|
3269
3347
|
const active = Boolean(state.aiRoleplayCharacter);
|
|
3270
3348
|
if (active) $("#ai-scope").value = "none";
|
|
3271
3349
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
3272
3350
|
$("#ai-prompt").dataset.placeholder = active
|
|
3273
|
-
?
|
|
3351
|
+
? state.aiRoleplayUserCharacter
|
|
3352
|
+
? `以 ${String(state.aiRoleplayUserCharacter.name)} 的身份与 ${String(state.aiRoleplayCharacter.name)} 对话……`
|
|
3353
|
+
: `与 ${String(state.aiRoleplayCharacter.name)} 角色开始对话……`
|
|
3274
3354
|
: "告诉 AI 你想讨论或修改什么……";
|
|
3275
3355
|
renderAiRoleplayCharacterSelect();
|
|
3276
3356
|
syncAiTaskOptions();
|
|
@@ -3281,22 +3361,27 @@ function refreshAiMessageRoleLabels() {
|
|
|
3281
3361
|
$("#ai-feed").querySelectorAll(".assistant-message .message-heading > span").forEach((label) => {
|
|
3282
3362
|
label.textContent = aiAssistantLabel();
|
|
3283
3363
|
});
|
|
3364
|
+
$("#ai-feed").querySelectorAll(".user-message .message-heading > span").forEach((label) => {
|
|
3365
|
+
label.textContent = aiUserLabel();
|
|
3366
|
+
});
|
|
3284
3367
|
}
|
|
3285
3368
|
|
|
3286
|
-
async function updateAiRoleplayCharacter(characterId) {
|
|
3369
|
+
async function updateAiRoleplayCharacter(characterId, userCharacterId = null) {
|
|
3287
3370
|
const conversationId = await ensureAiConversation();
|
|
3288
3371
|
const conversation = await api(`/api/ai-conversations/${conversationId}/roleplay`, {
|
|
3289
3372
|
method: "PATCH",
|
|
3290
|
-
body: { characterId: characterId || null }
|
|
3373
|
+
body: { characterId: characterId || null, userCharacterId: userCharacterId || null }
|
|
3291
3374
|
});
|
|
3292
3375
|
upsertAiConversationSummary(conversation);
|
|
3293
3376
|
applyAiConversationTaskType(conversation.taskType);
|
|
3294
|
-
applyAiRoleplayCharacter(conversation.roleplayCharacter);
|
|
3377
|
+
applyAiRoleplayCharacter(conversation.roleplayCharacter, conversation.roleplayUserCharacter);
|
|
3295
3378
|
resetAiContextMeter();
|
|
3296
3379
|
if ($("#ai-feed").querySelector("[data-message-id]")) refreshAiMessageRoleLabels();
|
|
3297
3380
|
else resetAiFeed();
|
|
3298
3381
|
toast(conversation.roleplayCharacter
|
|
3299
|
-
?
|
|
3382
|
+
? conversation.roleplayUserCharacter
|
|
3383
|
+
? `已进入 ${conversation.roleplayCharacter.name} 与 ${conversation.roleplayUserCharacter.name} 的关系扮演模式`
|
|
3384
|
+
: `已进入 ${conversation.roleplayCharacter.name} 的角色扮演模式`
|
|
3300
3385
|
: "已退出角色扮演模式");
|
|
3301
3386
|
}
|
|
3302
3387
|
|
|
@@ -3314,7 +3399,7 @@ async function persistAiConversationTaskType(taskType) {
|
|
|
3314
3399
|
});
|
|
3315
3400
|
upsertAiConversationSummary(conversation);
|
|
3316
3401
|
applyAiConversationTaskType(conversation.taskType);
|
|
3317
|
-
applyAiRoleplayCharacter(conversation.roleplayCharacter);
|
|
3402
|
+
applyAiRoleplayCharacter(conversation.roleplayCharacter, conversation.roleplayUserCharacter);
|
|
3318
3403
|
return conversation;
|
|
3319
3404
|
}
|
|
3320
3405
|
|
|
@@ -3354,9 +3439,10 @@ async function prepareAiRequestConversation(requestHolder, taskType, scope) {
|
|
|
3354
3439
|
upsertAiConversationSummary(taskConversation);
|
|
3355
3440
|
tab.taskType = taskConversation.taskType;
|
|
3356
3441
|
tab.roleplayCharacter = taskConversation.roleplayCharacter ?? null;
|
|
3442
|
+
tab.roleplayUserCharacter = taskConversation.roleplayUserCharacter ?? null;
|
|
3357
3443
|
if (isActiveAiChatTab(tab)) {
|
|
3358
3444
|
applyAiConversationTaskType(tab.taskType);
|
|
3359
|
-
applyAiRoleplayCharacter(tab.roleplayCharacter);
|
|
3445
|
+
applyAiRoleplayCharacter(tab.roleplayCharacter, tab.roleplayUserCharacter);
|
|
3360
3446
|
}
|
|
3361
3447
|
|
|
3362
3448
|
const scopedConversation = await api(`/api/ai-conversations/${encodeURIComponent(request.conversationId)}/context-scope`, {
|
|
@@ -3726,8 +3812,7 @@ function renderChapterAnnotations() {
|
|
|
3726
3812
|
},
|
|
3727
3813
|
locate: (annotation) => {
|
|
3728
3814
|
$("#chapter-annotations-dialog").close();
|
|
3729
|
-
|
|
3730
|
-
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
3815
|
+
revealChapterLines(annotation.startLine, annotation.endLine);
|
|
3731
3816
|
},
|
|
3732
3817
|
overlayDialog: $("#chapter-annotations-dialog")
|
|
3733
3818
|
});
|
|
@@ -6303,7 +6388,7 @@ async function runWorkSearch() {
|
|
|
6303
6388
|
renderSearchResults(results, query);
|
|
6304
6389
|
}
|
|
6305
6390
|
|
|
6306
|
-
function
|
|
6391
|
+
function revealChapterLines(startLine, endLine) {
|
|
6307
6392
|
const start = Math.max(0, Number(startLine) - 1);
|
|
6308
6393
|
const end = Math.max(start, Number(endLine ?? startLine) - 1);
|
|
6309
6394
|
const input = $("#chapter-content");
|
|
@@ -6358,7 +6443,7 @@ async function openSearchResult(result) {
|
|
|
6358
6443
|
if (target.kind === "chapter") {
|
|
6359
6444
|
await selectChapter(target.id);
|
|
6360
6445
|
if (state.chapter?.id === target.id && target.startLine) {
|
|
6361
|
-
|
|
6446
|
+
revealChapterLines(target.startLine, target.endLine);
|
|
6362
6447
|
toast(target.startLine === target.endLine ? `已定位到第 ${target.startLine} 行` : `已定位到第 ${target.startLine}-${target.endLine} 行`);
|
|
6363
6448
|
}
|
|
6364
6449
|
return;
|
|
@@ -6554,6 +6639,8 @@ function resetWorkScopedUiCaches() {
|
|
|
6554
6639
|
state.aiConversations = [];
|
|
6555
6640
|
resetAiChatTabs();
|
|
6556
6641
|
$("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
|
|
6642
|
+
syncAiModelPicker();
|
|
6643
|
+
setAiModelPickerVisible(false);
|
|
6557
6644
|
renderAiConversationHistory();
|
|
6558
6645
|
}
|
|
6559
6646
|
|
|
@@ -9357,10 +9444,10 @@ async function renderWorkChapterComments(page = moduleListPages.comments) {
|
|
|
9357
9444
|
bindChapterAnnotationCards($("#module-content"), result.items, {
|
|
9358
9445
|
refresh: () => renderWorkChapterComments(pageResult.page),
|
|
9359
9446
|
locate: async (annotation) => {
|
|
9360
|
-
await selectChapter(annotation.chapterId);
|
|
9447
|
+
const selected = await selectChapter(annotation.chapterId);
|
|
9448
|
+
if (!selected || String(state.chapter?.id ?? "") !== String(annotation.chapterId)) return;
|
|
9361
9449
|
await new Promise((resolve) => window.requestAnimationFrame(resolve));
|
|
9362
|
-
|
|
9363
|
-
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
9450
|
+
revealChapterLines(annotation.startLine, annotation.endLine);
|
|
9364
9451
|
}
|
|
9365
9452
|
});
|
|
9366
9453
|
}
|
|
@@ -11395,12 +11482,16 @@ async function ensureAiModelsLoaded() {
|
|
|
11395
11482
|
if (aiModelsLoadPromise && aiModelsLoadWorkId === workId) return aiModelsLoadPromise;
|
|
11396
11483
|
const select = $("#ai-model");
|
|
11397
11484
|
select.innerHTML = '<option value="">正在加载模型……</option>';
|
|
11485
|
+
syncAiModelPicker();
|
|
11398
11486
|
aiModelsLoadWorkId = workId;
|
|
11399
11487
|
aiModelsLoadPromise = loadModels();
|
|
11400
11488
|
try {
|
|
11401
11489
|
await aiModelsLoadPromise;
|
|
11402
11490
|
} catch (error) {
|
|
11403
|
-
if (state.work?.id === workId)
|
|
11491
|
+
if (state.work?.id === workId) {
|
|
11492
|
+
select.innerHTML = '<option value="">模型加载失败,点击重试</option>';
|
|
11493
|
+
syncAiModelPicker();
|
|
11494
|
+
}
|
|
11404
11495
|
throw error;
|
|
11405
11496
|
} finally {
|
|
11406
11497
|
if (aiModelsLoadWorkId === workId) {
|
|
@@ -11535,6 +11626,14 @@ function setAiContextDistributionVisible(visible) {
|
|
|
11535
11626
|
if (!visible && document.activeElement === $("#ai-context-popover-close")) meter.focus();
|
|
11536
11627
|
}
|
|
11537
11628
|
|
|
11629
|
+
function setAiModelPickerVisible(visible) {
|
|
11630
|
+
const button = $("#ai-model-picker");
|
|
11631
|
+
const popover = $("#ai-model-popover");
|
|
11632
|
+
popover.classList.toggle("hidden", !visible);
|
|
11633
|
+
button.setAttribute("aria-expanded", String(visible));
|
|
11634
|
+
if (!visible && document.activeElement === $("#ai-model")) button.focus();
|
|
11635
|
+
}
|
|
11636
|
+
|
|
11538
11637
|
function showAiContextWarning(usage = null) {
|
|
11539
11638
|
$("#ai-context-warning-title").textContent = "对话上下文过长";
|
|
11540
11639
|
$("#ai-context-warning-message").textContent = "当前对话上下文过长,是否进行压缩?不压缩可能会导致后续请求失败";
|
|
@@ -12813,8 +12912,17 @@ function updateVditorWordCount(editor, markdown) {
|
|
|
12813
12912
|
counter.title = "当前 Markdown 正文的字数,不计空白字符";
|
|
12814
12913
|
toolbar.append(counter);
|
|
12815
12914
|
}
|
|
12915
|
+
let value = counter.querySelector("[data-markdown-word-count-value]");
|
|
12916
|
+
if (!value) {
|
|
12917
|
+
value = document.createElement("span");
|
|
12918
|
+
value.className = "markdown-word-count-value";
|
|
12919
|
+
value.dataset.markdownWordCountValue = "true";
|
|
12920
|
+
const unit = document.createElement("span");
|
|
12921
|
+
unit.textContent = " 字";
|
|
12922
|
+
counter.replaceChildren(value, unit);
|
|
12923
|
+
}
|
|
12816
12924
|
const count = Array.from(String(markdown ?? "").replace(/\s/gu, "")).length;
|
|
12817
|
-
|
|
12925
|
+
value.textContent = count.toLocaleString("zh-CN");
|
|
12818
12926
|
}
|
|
12819
12927
|
|
|
12820
12928
|
function transformVditorPreview(html) {
|
|
@@ -14917,6 +15025,22 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
14917
15025
|
}
|
|
14918
15026
|
}
|
|
14919
15027
|
|
|
15028
|
+
function createAiStreamCharacterCount(value) {
|
|
15029
|
+
const count = document.createElement("span");
|
|
15030
|
+
count.className = "ai-stream-character-count";
|
|
15031
|
+
count.textContent = Math.max(0, Number(value) || 0).toLocaleString("zh-CN");
|
|
15032
|
+
return count;
|
|
15033
|
+
}
|
|
15034
|
+
|
|
15035
|
+
function renderAiStreamingCharacterProgress(meta, visibleCharacters, receivedCharacters) {
|
|
15036
|
+
const visible = Math.max(0, Number(visibleCharacters) || 0);
|
|
15037
|
+
const received = Math.max(visible, Number(receivedCharacters) || 0);
|
|
15038
|
+
const children = ["正在生成 · ", createAiStreamCharacterCount(visible)];
|
|
15039
|
+
if (received > visible) children.push(" / ", createAiStreamCharacterCount(received));
|
|
15040
|
+
children.push(" 字");
|
|
15041
|
+
meta.replaceChildren(...children);
|
|
15042
|
+
}
|
|
15043
|
+
|
|
14920
15044
|
async function streamChat(requestHolder, body, idempotencyKey) {
|
|
14921
15045
|
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
14922
15046
|
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
@@ -14944,9 +15068,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14944
15068
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
|
|
14945
15069
|
content.innerHTML = renderMarkdown(text);
|
|
14946
15070
|
const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
|
|
14947
|
-
meta
|
|
14948
|
-
? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
|
|
14949
|
-
: `正在生成 · ${progress.visibleCharacters} 字`;
|
|
15071
|
+
renderAiStreamingCharacterProgress(meta, progress.visibleCharacters, receivedCharacters);
|
|
14950
15072
|
scrollAiFeedToBottom(feed);
|
|
14951
15073
|
}
|
|
14952
15074
|
});
|
|
@@ -15201,7 +15323,9 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
15201
15323
|
});
|
|
15202
15324
|
const heading = attachMessageHeading(
|
|
15203
15325
|
message,
|
|
15204
|
-
role === "user"
|
|
15326
|
+
role === "user"
|
|
15327
|
+
? aiUserLabel(tab?.roleplayUserCharacter)
|
|
15328
|
+
: aiAssistantLabel(isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "", tab?.roleplayCharacter),
|
|
15205
15329
|
createdAt ?? undefined,
|
|
15206
15330
|
tab
|
|
15207
15331
|
);
|
|
@@ -16821,7 +16945,7 @@ $("#ai-assistant-entry").addEventListener("click", () => {
|
|
|
16821
16945
|
return toast("当前账户没有创作助手读取权限", "error");
|
|
16822
16946
|
}
|
|
16823
16947
|
setModuleNavExpanded(false);
|
|
16824
|
-
if (isMobileViewport()) {
|
|
16948
|
+
if (isPhoneClient() || isMobileViewport()) {
|
|
16825
16949
|
panelLayout.leftCollapsed = true;
|
|
16826
16950
|
applyPanelLayout(true);
|
|
16827
16951
|
setAiConversationWorkspaceVisible(true);
|
|
@@ -16863,7 +16987,7 @@ $("#ai-prompt").addEventListener("input", async () => {
|
|
|
16863
16987
|
$("#ai-prompt").addEventListener("focus", () => {
|
|
16864
16988
|
ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
|
|
16865
16989
|
});
|
|
16866
|
-
for (const select of [$("#ai-task"), $("#ai-scope"), $("#ai-roleplay-character"), $("#ai-model")]) {
|
|
16990
|
+
for (const select of [$("#ai-task"), $("#ai-scope"), $("#ai-roleplay-character"), $("#ai-roleplay-user-character"), $("#ai-model")]) {
|
|
16867
16991
|
select.addEventListener("pointerdown", blockLockedAiConversationOptionInteraction);
|
|
16868
16992
|
select.addEventListener("click", blockLockedAiConversationOptionInteraction);
|
|
16869
16993
|
select.addEventListener("keydown", blockLockedAiConversationOptionKeydown);
|
|
@@ -16874,9 +16998,26 @@ $("#ai-model").addEventListener("focus", () => {
|
|
|
16874
16998
|
$("#ai-model").addEventListener("change", (event) => {
|
|
16875
16999
|
if (state.aiPromptSent) {
|
|
16876
17000
|
event.currentTarget.value = state.aiConversationModelId ?? event.currentTarget.value;
|
|
17001
|
+
syncAiModelPicker();
|
|
16877
17002
|
return toast(aiConversationOptionLockedMessage);
|
|
16878
17003
|
}
|
|
16879
17004
|
setAiContextMeter(null);
|
|
17005
|
+
syncAiModelPicker();
|
|
17006
|
+
setAiModelPickerVisible(false);
|
|
17007
|
+
});
|
|
17008
|
+
$("#ai-model-picker").addEventListener("click", async (event) => {
|
|
17009
|
+
const button = event.currentTarget;
|
|
17010
|
+
if (notifyAiConversationOptionLocked(button)) return;
|
|
17011
|
+
const willOpen = $("#ai-model-popover").classList.contains("hidden");
|
|
17012
|
+
setAiContextDistributionVisible(false);
|
|
17013
|
+
setAiModelPickerVisible(willOpen);
|
|
17014
|
+
if (!willOpen) return;
|
|
17015
|
+
try {
|
|
17016
|
+
await ensureAiModelsLoaded();
|
|
17017
|
+
} catch (error) {
|
|
17018
|
+
toast(`模型加载失败:${error.message}`, "error");
|
|
17019
|
+
}
|
|
17020
|
+
if (!$("#ai-model-popover").classList.contains("hidden") && !$("#ai-model").disabled) $("#ai-model").focus();
|
|
16880
17021
|
});
|
|
16881
17022
|
$("#ai-roleplay-character").addEventListener("focus", async () => {
|
|
16882
17023
|
try {
|
|
@@ -16903,6 +17044,31 @@ $("#ai-roleplay-character").addEventListener("change", async (event) => {
|
|
|
16903
17044
|
renderAiRoleplayCharacterSelect();
|
|
16904
17045
|
}
|
|
16905
17046
|
});
|
|
17047
|
+
$("#ai-roleplay-user-character").addEventListener("focus", async () => {
|
|
17048
|
+
try {
|
|
17049
|
+
await ensureAiReferencesLoaded();
|
|
17050
|
+
renderAiRoleplayUserCharacterSelect();
|
|
17051
|
+
} catch (error) {
|
|
17052
|
+
toast(`角色卡加载失败:${error.message}`, "error");
|
|
17053
|
+
}
|
|
17054
|
+
});
|
|
17055
|
+
$("#ai-roleplay-user-character").addEventListener("change", async (event) => {
|
|
17056
|
+
const select = event.currentTarget;
|
|
17057
|
+
if (state.aiPromptSent) {
|
|
17058
|
+
renderAiRoleplayUserCharacterSelect();
|
|
17059
|
+
return toast(aiConversationOptionLockedMessage);
|
|
17060
|
+
}
|
|
17061
|
+
const userCharacterId = select.value;
|
|
17062
|
+
select.disabled = true;
|
|
17063
|
+
try {
|
|
17064
|
+
await updateAiRoleplayCharacter($("#ai-roleplay-character").value, userCharacterId);
|
|
17065
|
+
} catch (error) {
|
|
17066
|
+
renderAiRoleplayUserCharacterSelect();
|
|
17067
|
+
toast(`关系扮演模式切换失败:${error.message}`, "error");
|
|
17068
|
+
} finally {
|
|
17069
|
+
renderAiRoleplayUserCharacterSelect();
|
|
17070
|
+
}
|
|
17071
|
+
});
|
|
16906
17072
|
$("#ai-task").addEventListener("change", async (event) => {
|
|
16907
17073
|
const select = event.currentTarget;
|
|
16908
17074
|
const previousTaskType = select.dataset.previousValue || state.aiTaskType || "chat";
|
|
@@ -17097,6 +17263,7 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
17097
17263
|
if (!event.target.closest("#ai-conversation-switcher-menu") && !event.target.closest("#ai-conversation-switcher")) setAiConversationSwitcherVisible(false);
|
|
17098
17264
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
17099
17265
|
if (!event.target.closest("#ai-context-meter") && !event.target.closest("#ai-context-popover")) setAiContextDistributionVisible(false);
|
|
17266
|
+
if (!event.target.closest("#ai-model-picker") && !event.target.closest("#ai-model-popover")) setAiModelPickerVisible(false);
|
|
17100
17267
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
17101
17268
|
$("#account-menu").classList.add("hidden");
|
|
17102
17269
|
$("#account-button").setAttribute("aria-expanded", "false");
|
|
@@ -17114,6 +17281,10 @@ document.addEventListener("keydown", (event) => {
|
|
|
17114
17281
|
return;
|
|
17115
17282
|
}
|
|
17116
17283
|
if (event.key === "Escape") {
|
|
17284
|
+
if (!$("#ai-model-popover").classList.contains("hidden")) {
|
|
17285
|
+
setAiModelPickerVisible(false);
|
|
17286
|
+
return;
|
|
17287
|
+
}
|
|
17117
17288
|
if (!$("#chapter-search-panel").classList.contains("hidden")) {
|
|
17118
17289
|
closeChapterSearchPanel();
|
|
17119
17290
|
return;
|
|
@@ -17149,6 +17320,7 @@ document.addEventListener("keydown", (event) => {
|
|
|
17149
17320
|
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
17150
17321
|
}, { capture: true });
|
|
17151
17322
|
$("#ai-context-meter").addEventListener("click", () => {
|
|
17323
|
+
setAiModelPickerVisible(false);
|
|
17152
17324
|
setAiContextDistributionVisible($("#ai-context-popover").classList.contains("hidden"));
|
|
17153
17325
|
});
|
|
17154
17326
|
$("#ai-context-popover-close").addEventListener("click", () => setAiContextDistributionVisible(false));
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1">
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -379,9 +379,9 @@
|
|
|
379
379
|
<select id="ai-roleplay-character" class="ai-roleplay-character hidden" aria-label="角色扮演角色卡">
|
|
380
380
|
<option value="">选择角色卡</option>
|
|
381
381
|
</select>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
<select id="ai-roleplay-user-character" class="ai-roleplay-character ai-roleplay-user-character hidden" aria-label="我扮演的角色">
|
|
383
|
+
<option value="">选择我的角色(可选)</option>
|
|
384
|
+
</select>
|
|
385
385
|
</div>
|
|
386
386
|
<div class="prompt-composer">
|
|
387
387
|
<div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-controls="ai-mention-menu" aria-haspopup="listbox" aria-expanded="false" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
|
|
@@ -392,6 +392,11 @@
|
|
|
392
392
|
<header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
|
|
393
393
|
<div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
|
|
394
394
|
</section>
|
|
395
|
+
<button id="ai-model-picker" class="ai-model-picker" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-model-popover" aria-label="选择实际使用模型" title="选择实际使用模型"><svg class="ai-model-picker-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5.25a3.25 3.25 0 0 0-5.95 1.82A3.74 3.74 0 0 0 4.7 13.5a3.25 3.25 0 0 0 4.55 4.53A3.6 3.6 0 0 0 12 19.5"></path><path d="M12 5.25a3.25 3.25 0 0 1 5.95 1.82 3.74 3.74 0 0 1 1.35 6.43 3.25 3.25 0 0 1-4.55 4.53A3.6 3.6 0 0 1 12 19.5"></path><path d="M12 5.25v14.25"></path><path d="M8 9.1c1.1.1 2 .9 2.25 1.95"></path><path d="M16 9.1c-1.1.1-2 .9-2.25 1.95"></path><path d="M8.3 15.3c1.15-.05 2.1-.85 2.3-1.95"></path><path d="M15.7 15.3c-1.15-.05-2.1-.85-2.3-1.95"></path></svg></button>
|
|
396
|
+
<section id="ai-model-popover" class="ai-model-popover hidden" role="dialog" aria-labelledby="ai-model-popover-title">
|
|
397
|
+
<label id="ai-model-popover-title" for="ai-model">实际使用模型</label>
|
|
398
|
+
<select id="ai-model" aria-label="实际使用模型"><option value="">使用创作助手时加载模型</option></select>
|
|
399
|
+
</section>
|
|
395
400
|
<button id="ai-send" class="ai-send-button" type="button" data-state="send" aria-label="发送消息" title="发送消息"><svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 3 9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7Z"></path></svg></button>
|
|
396
401
|
</div>
|
|
397
402
|
</div>
|
|
@@ -1205,6 +1210,6 @@
|
|
|
1205
1210
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1206
1211
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1207
1212
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1208
|
-
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=calculate-time-tool-v1&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1"></script>
|
|
1213
|
+
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v1&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v1&feature=phone-client-entry-v1"></script>
|
|
1209
1214
|
</body>
|
|
1210
1215
|
</html>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function isPhoneClient(navigatorLike = globalThis.navigator) {
|
|
2
|
+
const mobile = navigatorLike?.userAgentData?.mobile;
|
|
3
|
+
if (typeof mobile === "boolean") return mobile;
|
|
4
|
+
const userAgent = String(navigatorLike?.userAgent ?? "");
|
|
5
|
+
return /(?:Android.*Mobile|iPhone|iPod|IEMobile|Windows Phone|Opera Mini)/iu.test(userAgent);
|
|
6
|
+
}
|
|
@@ -327,10 +327,18 @@ export function normalizeGalaxyMotionMode(value) {
|
|
|
327
327
|
return GALAXY_MOTION_MODES.includes(value) ? value : "auto";
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
export function isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount) {
|
|
331
|
+
return Math.max(0, Number(nodeCount) || 0) > GALAXY_REDUCED_MOTION_NODE_THRESHOLD
|
|
332
|
+
|| Math.max(0, Number(edgeCount) || 0) > GALAXY_REDUCED_MOTION_EDGE_THRESHOLD;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function shouldRenderGalaxyEdgeLabel(nodeCount, edgeCount, edgeSelected, highlighted) {
|
|
336
|
+
return Boolean(edgeSelected) || (Boolean(highlighted) && !isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount));
|
|
337
|
+
}
|
|
338
|
+
|
|
330
339
|
export function getGalaxyMotionProfile(mode, nodeCount, edgeCount, prefersReducedMotion = false) {
|
|
331
340
|
const requestedMode = normalizeGalaxyMotionMode(mode);
|
|
332
|
-
const thresholdExceeded =
|
|
333
|
-
|| Math.max(0, Number(edgeCount) || 0) > GALAXY_REDUCED_MOTION_EDGE_THRESHOLD;
|
|
341
|
+
const thresholdExceeded = isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount);
|
|
334
342
|
const reducedBySystem = requestedMode === "auto" && Boolean(prefersReducedMotion);
|
|
335
343
|
const reducedByThreshold = requestedMode === "auto" && thresholdExceeded;
|
|
336
344
|
const effectiveMode = requestedMode === "off"
|
|
@@ -2528,7 +2536,7 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2528
2536
|
context.lineTo(to.x - Math.cos(angle + 0.45) * arrowSize, to.y - Math.sin(angle + 0.45) * arrowSize);
|
|
2529
2537
|
context.fill();
|
|
2530
2538
|
}
|
|
2531
|
-
if (highlighted) {
|
|
2539
|
+
if (shouldRenderGalaxyEdgeLabel(graph.stats.nodeCount, graph.stats.edgeCount, edgeSelected, highlighted)) {
|
|
2532
2540
|
const fullLabel = formatRelationshipLabel(edge);
|
|
2533
2541
|
if (!highlightedKeywordSet.has(fullLabel)) {
|
|
2534
2542
|
highlightedKeywordSet.add(fullLabel);
|