@musnows/scriverse 0.7.13 → 0.8.1
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 +4 -0
- package/README.md +5 -0
- package/dist/ai-chat-tab-limit.js +14 -0
- package/dist/ai-chat-tab-limit.js.map +1 -0
- package/dist/ai-protocol.js +10 -2
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-retry.js +52 -0
- package/dist/ai-retry.js.map +1 -0
- package/dist/ai.js +547 -119
- package/dist/ai.js.map +1 -1
- package/dist/app.js +80 -22
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +2 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/database.js +127 -2
- package/dist/database.js.map +1 -1
- package/dist/domain.js +1 -0
- package/dist/domain.js.map +1 -1
- package/dist/public/ai-chat-tabs.js +75 -0
- package/dist/public/ai-request-manager.js +32 -15
- package/dist/public/app.js +1305 -298
- package/dist/public/background-task-center.d.ts +9 -0
- package/dist/public/background-task-center.js +18 -0
- package/dist/public/chapter-search.d.ts +6 -0
- package/dist/public/chapter-search.js +23 -0
- package/dist/public/character-filters.d.ts +3 -1
- package/dist/public/character-filters.js +17 -2
- package/dist/public/character-version.js +1 -0
- package/dist/public/display-labels.d.ts +1 -0
- package/dist/public/display-labels.js +5 -1
- package/dist/public/index.html +67 -26
- package/dist/public/model-config.d.ts +3 -1
- package/dist/public/model-config.js +13 -0
- package/dist/public/relationship-filters.d.ts +5 -0
- package/dist/public/relationship-filters.js +31 -0
- package/dist/public/relationship-graph.js +289 -58
- package/dist/public/stream-typewriter.d.ts +1 -0
- package/dist/public/stream-typewriter.js +12 -0
- package/dist/public/styles.css +185 -41
- package/dist/public/work-permissions.d.ts +1 -1
- package/dist/public/work-permissions.js +13 -1
- package/dist/server-runtime.js +6 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +127 -44
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +34 -7
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +6 -1
- package/dist/version.js.map +1 -1
- package/dist/work-permissions.js +13 -2
- package/dist/work-permissions.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, renderRelationshipMindMap } from "/relationship-graph.js?v=
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3";
|
|
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
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
|
-
import {
|
|
6
|
+
import { createAiChatTabManager, normalizeAiChatTabLimit } from "/ai-chat-tabs.js?v=20260816-ai-chat-switcher-v2";
|
|
7
|
+
import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManager, isAiRequestCancellation } from "/ai-request-manager.js?v=20260816-ai-chat-tabs-v1";
|
|
7
8
|
import { calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
8
9
|
import { buildChapterLineMirror, findChapterLineWindow } from "/chapter-editor-virtualization.js?v=20260810-visible-lines-v1";
|
|
9
10
|
import {
|
|
@@ -15,11 +16,11 @@ import {
|
|
|
15
16
|
visibleForeshadowReminders
|
|
16
17
|
} from "/foreshadow-reminder.js?v=20260812-editor-reminder-v1";
|
|
17
18
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
18
|
-
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, supportsMultimodalModelProtocol } from "/model-config.js?v=
|
|
19
|
+
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, supportsMultimodalModelProtocol } from "/model-config.js?v=20260816-extended-thinking-effort-v1";
|
|
19
20
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260812-connectivity-cooldown-v1";
|
|
20
21
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
21
22
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
22
|
-
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=
|
|
23
|
+
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260818-ai-agent-turn-process-v1";
|
|
23
24
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
24
25
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
25
26
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
@@ -27,9 +28,10 @@ import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContex
|
|
|
27
28
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
28
29
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
29
30
|
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
31
|
+
import { findTextMatches, replaceTextMatches } from "/chapter-search.js?v=20260818-chapter-search-replace-v1";
|
|
30
32
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
31
33
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
32
|
-
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=
|
|
34
|
+
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260816-character-gender-v1";
|
|
33
35
|
import { chapterDiffSummary, diffChapterLines } from "/chapter-version-diff.js?v=20260812-chapter-version-diff-v1";
|
|
34
36
|
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260809-global-replace-v1";
|
|
35
37
|
import {
|
|
@@ -49,8 +51,9 @@ import {
|
|
|
49
51
|
settingStatusLabel,
|
|
50
52
|
taskScopeLabel,
|
|
51
53
|
timelineStatusLabel,
|
|
54
|
+
characterGenderLabel,
|
|
52
55
|
characterStateFieldLabel
|
|
53
|
-
} from "/display-labels.js?v=
|
|
56
|
+
} from "/display-labels.js?v=20260816-character-gender-v1&feature=global-replace-volume-v1";
|
|
54
57
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260812-reader-preview-v1";
|
|
55
58
|
import {
|
|
56
59
|
READING_PREFERENCES_STORAGE_KEY,
|
|
@@ -70,12 +73,12 @@ import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelatio
|
|
|
70
73
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
71
74
|
import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
|
|
72
75
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
73
|
-
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=
|
|
76
|
+
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260818-annotation-permissions-v1";
|
|
74
77
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
75
78
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
76
79
|
import { prioritizeGlobalSearchResults, resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260804-agent-history-score-sort-v1";
|
|
77
|
-
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=
|
|
78
|
-
import { filterRelationships } from "/relationship-filters.js?v=
|
|
80
|
+
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260817-character-filter-state-v1";
|
|
81
|
+
import { filterRelationships, sortCharacterRelationships } from "/relationship-filters.js?v=20260818-character-relationship-group-v1";
|
|
79
82
|
import { filterSettings } from "/setting-filters.js?v=20260810-setting-inline-filters-v1";
|
|
80
83
|
import {
|
|
81
84
|
prepareTimelineEvents,
|
|
@@ -88,7 +91,7 @@ import {
|
|
|
88
91
|
outlineBoardRequestPath,
|
|
89
92
|
outlineBoardUnresolvedCount
|
|
90
93
|
} from "/outline-board.js?v=20260813-outline-board-page-v1";
|
|
91
|
-
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=
|
|
94
|
+
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions, filterBackgroundTaskTransitionsForAnnouncement } from "/background-task-center.js?v=20260817-analysis-task-expired-toast-v1";
|
|
92
95
|
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
93
96
|
import { systemStatusPresentation } from "/system-status.js?v=20260801-system-health-v1";
|
|
94
97
|
import { collectS3BackupRunTransitions, s3BackupEncryptionKeyFile, s3BackupEncryptionPresentation, s3BackupFailureToast, s3BackupRootPrefix, s3BackupStatusLabel } from "/s3-backup-ui.js?v=20260810-backup-encryption-v1";
|
|
@@ -120,6 +123,21 @@ const defaultPageSizes = Object.freeze({
|
|
|
120
123
|
analysisTasks: 30,
|
|
121
124
|
fileVersions: 30
|
|
122
125
|
});
|
|
126
|
+
const GALAXY_MOTION_MODE_STORAGE_KEY = "scriverse.galaxy-motion-mode.v1";
|
|
127
|
+
|
|
128
|
+
function storedGalaxyMotionMode() {
|
|
129
|
+
try {
|
|
130
|
+
return normalizeGalaxyMotionMode(localStorage.getItem(GALAXY_MOTION_MODE_STORAGE_KEY));
|
|
131
|
+
} catch {
|
|
132
|
+
return "auto";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function persistGalaxyMotionMode(mode) {
|
|
137
|
+
const normalized = normalizeGalaxyMotionMode(mode);
|
|
138
|
+
try { localStorage.setItem(GALAXY_MOTION_MODE_STORAGE_KEY, normalized); } catch { /* 浏览器禁用存储时仅保留本次选择 */ }
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
123
141
|
|
|
124
142
|
function normalizePageSize(value, fallback = 30) {
|
|
125
143
|
const candidate = Number(value);
|
|
@@ -177,6 +195,7 @@ const state = {
|
|
|
177
195
|
};
|
|
178
196
|
|
|
179
197
|
const aiRequestManager = createAiRequestManager();
|
|
198
|
+
const aiChatTabManager = createAiChatTabManager(() => createAiIdempotencyKey());
|
|
180
199
|
const moduleRequestCache = createModuleRequestCache();
|
|
181
200
|
const cachedWorkModules = new Set([
|
|
182
201
|
"drafts",
|
|
@@ -211,6 +230,8 @@ let systemBootCheckTimer = null;
|
|
|
211
230
|
let systemBootCheckPromise = null;
|
|
212
231
|
let systemRestartDetected = false;
|
|
213
232
|
let chapterAnnotations = [];
|
|
233
|
+
let chapterAnnotationCounts = new Map();
|
|
234
|
+
let chapterAnnotationsLineIndex = null;
|
|
214
235
|
let workAuditRecords = [];
|
|
215
236
|
let workAuditNextPage = null;
|
|
216
237
|
const chapterBatchSelectedIds = new Set();
|
|
@@ -240,6 +261,7 @@ let backgroundTaskCenterRequest = 0;
|
|
|
240
261
|
let backgroundTaskCenterWorkId = null;
|
|
241
262
|
let backgroundTaskCenterTasksInitialized = false;
|
|
242
263
|
let backgroundTaskCenterTaskSnapshots = new Map();
|
|
264
|
+
let backgroundTaskCenterExpiredNoticeTimes = new Map();
|
|
243
265
|
let backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
244
266
|
let s3BackupTargets = [];
|
|
245
267
|
let s3BackupRuns = [];
|
|
@@ -356,6 +378,13 @@ function canEditProse(work = state.work) {
|
|
|
356
378
|
return canWriteUiModule(work, "editor");
|
|
357
379
|
}
|
|
358
380
|
|
|
381
|
+
function canManageChapterAnnotation(annotation, work = state.work) {
|
|
382
|
+
if (!work) return false;
|
|
383
|
+
if (annotation.kind !== "todo") return canEditProse(work);
|
|
384
|
+
if (["admin", "owner"].includes(String(work.accessRole))) return true;
|
|
385
|
+
return annotation.createdByUserId === state.user?.userId && canWritePermissionModule(work, "todos");
|
|
386
|
+
}
|
|
387
|
+
|
|
359
388
|
function canReplaceProse(work = state.work) {
|
|
360
389
|
return WORK_PERMISSION_MODULES
|
|
361
390
|
.filter((item) => item.id !== "ai-settings")
|
|
@@ -409,6 +438,7 @@ function applyWorkAccessMode() {
|
|
|
409
438
|
$("#module-nav [data-module=\"comments\"]").classList.toggle("permission-hidden", Boolean(state.work) && !canReadModule("comments"));
|
|
410
439
|
$("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
|
|
411
440
|
$("#reader-open-button").classList.toggle("permission-hidden", proseHidden);
|
|
441
|
+
$("#ai-assistant-entry").classList.toggle("permission-hidden", !state.work || aiHidden);
|
|
412
442
|
$("#new-volume-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
413
443
|
$("#chapter-batch-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
414
444
|
$("#welcome-new-work").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
@@ -519,10 +549,30 @@ function setAiAssistantStatus(status) {
|
|
|
519
549
|
dot.title = label;
|
|
520
550
|
}
|
|
521
551
|
|
|
552
|
+
function activeAiChatTab() {
|
|
553
|
+
return aiChatTabManager.active();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function aiChatTabForRequest(request) {
|
|
557
|
+
return request?.tabId ? aiChatTabManager.get(request.tabId) : null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function isActiveAiChatTab(tab) {
|
|
561
|
+
return Boolean(tab && activeAiChatTab()?.id === tab.id);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setAiChatTabStatus(tab, status) {
|
|
565
|
+
if (!tab) return;
|
|
566
|
+
tab.status = status;
|
|
567
|
+
if (isActiveAiChatTab(tab)) setAiAssistantStatus(status);
|
|
568
|
+
renderAiChatTabs();
|
|
569
|
+
}
|
|
570
|
+
|
|
522
571
|
function aiRequestTargetsCurrentState(request) {
|
|
523
|
-
|
|
572
|
+
const tab = aiChatTabForRequest(request);
|
|
573
|
+
return aiRequestManager.isCurrent(request) && Boolean(tab) && aiRequestTargetsState(request, {
|
|
524
574
|
workId: state.work?.id,
|
|
525
|
-
conversationId:
|
|
575
|
+
conversationId: tab.conversationId
|
|
526
576
|
});
|
|
527
577
|
}
|
|
528
578
|
|
|
@@ -532,41 +582,56 @@ function assertAiRequestCurrent(request) {
|
|
|
532
582
|
}
|
|
533
583
|
|
|
534
584
|
function aiInteractionBusy() {
|
|
535
|
-
return aiRequestManager.hasActive() || aiConversationNavigationPending !== null;
|
|
585
|
+
return aiRequestManager.hasActive(activeAiChatTab()?.id) || aiConversationNavigationPending !== null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function aiSendButtonIconMarkup(stateName) {
|
|
589
|
+
return stateName === "stop"
|
|
590
|
+
? '<svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="6" y="6" width="12" height="12" rx="1.5"></rect></svg>'
|
|
591
|
+
: '<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>';
|
|
536
592
|
}
|
|
537
593
|
|
|
538
594
|
function syncAiRequestControls() {
|
|
539
|
-
const sending = aiRequestManager.hasActive();
|
|
595
|
+
const sending = aiRequestManager.hasActive(activeAiChatTab()?.id);
|
|
540
596
|
const switching = aiConversationNavigationPending !== null;
|
|
541
597
|
const button = $("#ai-send");
|
|
542
|
-
|
|
543
|
-
|
|
598
|
+
const stateName = sending ? "stop" : switching ? "switching" : "send";
|
|
599
|
+
const label = sending ? "终止当前回复" : switching ? "正在切换对话" : "发送消息";
|
|
600
|
+
button.disabled = switching;
|
|
601
|
+
button.dataset.state = stateName;
|
|
602
|
+
button.classList.toggle("is-stop", sending);
|
|
603
|
+
button.setAttribute("aria-label", label);
|
|
604
|
+
button.title = label;
|
|
605
|
+
button.innerHTML = aiSendButtonIconMarkup(stateName);
|
|
544
606
|
syncAiTaskOptions();
|
|
545
607
|
renderAiRoleplayCharacterSelect();
|
|
546
608
|
}
|
|
547
609
|
|
|
548
610
|
function cancelActiveAiRequest(reason) {
|
|
549
|
-
const cancelled = aiRequestManager.cancel(reason);
|
|
611
|
+
const cancelled = aiRequestManager.cancel(reason, activeAiChatTab()?.id);
|
|
550
612
|
syncAiRequestControls();
|
|
551
613
|
return cancelled;
|
|
552
614
|
}
|
|
553
615
|
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
616
|
+
function activateAiSendControl() {
|
|
617
|
+
const tab = activeAiChatTab();
|
|
618
|
+
if (aiRequestManager.hasActive(tab?.id)) {
|
|
619
|
+
if (cancelActiveAiRequest("用户已终止当前回复")) {
|
|
620
|
+
toast("已终止当前回复,可以重新发送");
|
|
621
|
+
$("#ai-prompt").focus();
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
562
624
|
}
|
|
625
|
+
void sendAi();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function beginAiConversationNavigation(reason, action = "切换会话") {
|
|
563
629
|
aiConversationNavigationGeneration += 1;
|
|
564
630
|
aiConversationNavigationPending = aiConversationNavigationGeneration;
|
|
565
631
|
const navigation = Object.freeze({
|
|
566
632
|
generation: aiConversationNavigationGeneration,
|
|
567
633
|
workId: String(state.work?.id ?? "")
|
|
568
634
|
});
|
|
569
|
-
cancelActiveAiRequest(reason);
|
|
570
635
|
return navigation;
|
|
571
636
|
}
|
|
572
637
|
|
|
@@ -584,11 +649,12 @@ function finishAiConversationNavigation(navigation) {
|
|
|
584
649
|
|
|
585
650
|
function invalidateAiConversationNavigation(reason, action = "切换作品") {
|
|
586
651
|
if (aiRequestManager.hasActive()) {
|
|
587
|
-
toast(
|
|
652
|
+
toast(`仍有 Agent turn 尚未结束,${action}会中断全部生成;已收到的内容会保留在历史记录中`, "warning");
|
|
588
653
|
}
|
|
589
654
|
aiConversationNavigationGeneration += 1;
|
|
590
655
|
aiConversationNavigationPending = null;
|
|
591
|
-
|
|
656
|
+
aiRequestManager.cancelAll(reason);
|
|
657
|
+
syncAiRequestControls();
|
|
592
658
|
}
|
|
593
659
|
|
|
594
660
|
function aiAssistantRequestId(request) {
|
|
@@ -665,6 +731,8 @@ let workSelectionRequestGeneration = 0;
|
|
|
665
731
|
let chapterSelectionRequestGeneration = 0;
|
|
666
732
|
let aiConversationNavigationGeneration = 0;
|
|
667
733
|
let aiConversationNavigationPending = null;
|
|
734
|
+
let aiChatTabLimit = 5;
|
|
735
|
+
let aiConversationWorkspaceOpen = false;
|
|
668
736
|
const loadedVolumeChapterIds = new Set();
|
|
669
737
|
const volumeChapterLoadingIds = new Set();
|
|
670
738
|
const volumeChapterRequests = new Map();
|
|
@@ -873,6 +941,7 @@ function replacePageRoute(route) {
|
|
|
873
941
|
const hash = serializePageRoute(route);
|
|
874
942
|
if (window.location.hash !== hash) window.history.replaceState(null, "", hash);
|
|
875
943
|
schedulePresenceHeartbeat();
|
|
944
|
+
syncTopSearchButton();
|
|
876
945
|
}
|
|
877
946
|
|
|
878
947
|
function presencePageForRoute(route = currentPageRoute()) {
|
|
@@ -1084,6 +1153,149 @@ function currentPageRoute() {
|
|
|
1084
1153
|
return { view: "welcome", workId };
|
|
1085
1154
|
}
|
|
1086
1155
|
|
|
1156
|
+
function isChapterSearchContext() {
|
|
1157
|
+
return Boolean(state.work && state.chapter && !$("#editor-view").classList.contains("hidden"));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function syncTopSearchButton() {
|
|
1161
|
+
const button = $("#top-search-button");
|
|
1162
|
+
const chapterContext = isChapterSearchContext();
|
|
1163
|
+
button.classList.toggle("hidden", chapterContext);
|
|
1164
|
+
button.disabled = !state.work || chapterContext || !canReadAggregateContent();
|
|
1165
|
+
if (!chapterContext) closeChapterSearchPanel({ restoreFocus: false });
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function chapterSearchMatches() {
|
|
1169
|
+
return findTextMatches($("#chapter-content").value, $("#chapter-search-query").value);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function renderChapterSearchStatus() {
|
|
1173
|
+
const query = $("#chapter-search-query").value;
|
|
1174
|
+
const matches = chapterSearchMatches();
|
|
1175
|
+
const status = $("#chapter-search-status");
|
|
1176
|
+
const readOnly = !canEditProse() || chapterEditorReadOnly;
|
|
1177
|
+
const replaceButtons = [$("#chapter-search-replace"), $("#chapter-search-replace-all")];
|
|
1178
|
+
replaceButtons.forEach((button) => { button.disabled = readOnly || matches.length === 0; });
|
|
1179
|
+
$("#chapter-search-previous").disabled = matches.length === 0;
|
|
1180
|
+
$("#chapter-search-next").disabled = matches.length === 0;
|
|
1181
|
+
if (!query) {
|
|
1182
|
+
status.textContent = "输入关键词后开始搜索";
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (!matches.length) {
|
|
1186
|
+
status.textContent = "本章未找到匹配内容";
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const current = chapterSearchMatchIndex >= 0 ? chapterSearchMatchIndex + 1 : 0;
|
|
1190
|
+
status.textContent = current ? `第 ${current} / ${matches.length} 处` : `找到 ${matches.length} 处`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function syncChapterSearchControls() {
|
|
1194
|
+
if (!$("#chapter-search-panel")) return;
|
|
1195
|
+
const matches = chapterSearchMatches();
|
|
1196
|
+
if (chapterSearchMatchIndex >= matches.length) chapterSearchMatchIndex = matches.length - 1;
|
|
1197
|
+
renderChapterSearchStatus();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function selectChapterSearchMatch(index) {
|
|
1201
|
+
const matches = chapterSearchMatches();
|
|
1202
|
+
if (!matches.length) {
|
|
1203
|
+
chapterSearchMatchIndex = -1;
|
|
1204
|
+
renderChapterSearchStatus();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
chapterSearchMatchIndex = (index + matches.length) % matches.length;
|
|
1208
|
+
const input = $("#chapter-content");
|
|
1209
|
+
const start = matches[chapterSearchMatchIndex];
|
|
1210
|
+
input.focus({ preventScroll: true });
|
|
1211
|
+
input.setSelectionRange(start, start + $("#chapter-search-query").value.length);
|
|
1212
|
+
renderChapterSearchStatus();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function moveChapterSearchMatch(direction) {
|
|
1216
|
+
const matches = chapterSearchMatches();
|
|
1217
|
+
if (!matches.length) {
|
|
1218
|
+
renderChapterSearchStatus();
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const nextIndex = chapterSearchMatchIndex < 0
|
|
1222
|
+
? direction > 0 ? 0 : matches.length - 1
|
|
1223
|
+
: chapterSearchMatchIndex + direction;
|
|
1224
|
+
selectChapterSearchMatch(nextIndex);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function openChapterSearchPanel() {
|
|
1228
|
+
if (!isChapterSearchContext()) return false;
|
|
1229
|
+
const panel = $("#chapter-search-panel");
|
|
1230
|
+
panel.classList.remove("hidden");
|
|
1231
|
+
panel.setAttribute("aria-hidden", "false");
|
|
1232
|
+
syncChapterSearchControls();
|
|
1233
|
+
queueMicrotask(() => {
|
|
1234
|
+
const input = $("#chapter-search-query");
|
|
1235
|
+
input.focus();
|
|
1236
|
+
if (input.value) input.select();
|
|
1237
|
+
});
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function closeChapterSearchPanel({ restoreFocus = true } = {}) {
|
|
1242
|
+
const panel = $("#chapter-search-panel");
|
|
1243
|
+
if (!panel || panel.classList.contains("hidden")) return;
|
|
1244
|
+
panel.classList.add("hidden");
|
|
1245
|
+
panel.setAttribute("aria-hidden", "true");
|
|
1246
|
+
chapterSearchMatchIndex = -1;
|
|
1247
|
+
if (restoreFocus && isChapterSearchContext()) $("#chapter-content").focus({ preventScroll: true });
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function resetChapterSearchPanel() {
|
|
1251
|
+
closeChapterSearchPanel({ restoreFocus: false });
|
|
1252
|
+
$("#chapter-search-query").value = "";
|
|
1253
|
+
$("#chapter-replace-query").value = "";
|
|
1254
|
+
chapterSearchMatchIndex = -1;
|
|
1255
|
+
renderChapterSearchStatus();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function replaceCurrentChapterSearchMatch() {
|
|
1259
|
+
if (!isChapterSearchContext() || !canEditProse() || chapterEditorReadOnly) {
|
|
1260
|
+
toast("请先进入编辑模式", "error");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const query = $("#chapter-search-query").value;
|
|
1264
|
+
const matches = chapterSearchMatches();
|
|
1265
|
+
if (!query || !matches.length) return renderChapterSearchStatus();
|
|
1266
|
+
const index = chapterSearchMatchIndex < 0 ? 0 : chapterSearchMatchIndex;
|
|
1267
|
+
const start = matches[index];
|
|
1268
|
+
const input = $("#chapter-content");
|
|
1269
|
+
const replacement = $("#chapter-replace-query").value;
|
|
1270
|
+
input.value = `${input.value.slice(0, start)}${replacement}${input.value.slice(start + query.length)}`;
|
|
1271
|
+
chapterSearchMatchIndex = Math.min(index, Math.max(0, findTextMatches(input.value, query).length - 1));
|
|
1272
|
+
updateChapterStats();
|
|
1273
|
+
clearChapterLineSelection();
|
|
1274
|
+
scheduleChapterLineNumbers();
|
|
1275
|
+
scheduleChapterAutoSave(120);
|
|
1276
|
+
syncChapterSearchControls();
|
|
1277
|
+
toast("已替换本章 1 处,等待自动保存");
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function replaceAllChapterSearchMatches() {
|
|
1281
|
+
if (!isChapterSearchContext() || !canEditProse() || chapterEditorReadOnly) {
|
|
1282
|
+
toast("请先进入编辑模式", "error");
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const query = $("#chapter-search-query").value;
|
|
1286
|
+
const input = $("#chapter-content");
|
|
1287
|
+
const result = replaceTextMatches(input.value, query, $("#chapter-replace-query").value);
|
|
1288
|
+
if (!result.matches) return renderChapterSearchStatus();
|
|
1289
|
+
input.value = result.content;
|
|
1290
|
+
chapterSearchMatchIndex = -1;
|
|
1291
|
+
updateChapterStats();
|
|
1292
|
+
clearChapterLineSelection();
|
|
1293
|
+
scheduleChapterLineNumbers();
|
|
1294
|
+
scheduleChapterAutoSave(120);
|
|
1295
|
+
syncChapterSearchControls();
|
|
1296
|
+
toast(`已替换本章 ${result.matches} 处,等待自动保存`);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1087
1299
|
function loadPanelLayout() {
|
|
1088
1300
|
try {
|
|
1089
1301
|
const stored = JSON.parse(localStorage.getItem(panelLayoutStorageKey) ?? "{}");
|
|
@@ -1131,9 +1343,9 @@ function applyPanelLayout(persist = false) {
|
|
|
1131
1343
|
? (panelLayout.leftCollapsed ? "打开作品模块" : "关闭作品模块")
|
|
1132
1344
|
: (panelLayout.leftCollapsed ? "展开作品侧栏" : "收起作品侧栏"));
|
|
1133
1345
|
$("#mobile-module-tab").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
|
|
1134
|
-
$("#ai-panel-toggle").textContent = panelLayout.aiCollapsed ? "‹" : "›";
|
|
1346
|
+
$("#ai-panel-toggle").textContent = aiConversationWorkspaceOpen ? "×" : (panelLayout.aiCollapsed ? "‹" : "›");
|
|
1135
1347
|
$("#ai-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.aiCollapsed));
|
|
1136
|
-
$("#ai-panel-toggle").setAttribute("aria-label", panelLayout.aiCollapsed ? "展开创作助手" : "收起创作助手");
|
|
1348
|
+
$("#ai-panel-toggle").setAttribute("aria-label", aiConversationWorkspaceOpen ? "关闭创作助手全屏面板" : (panelLayout.aiCollapsed ? "展开创作助手" : "收起创作助手"));
|
|
1137
1349
|
syncMobileAiPanelSafeTop();
|
|
1138
1350
|
scheduleChapterLineNumbers();
|
|
1139
1351
|
if (persist) {
|
|
@@ -1142,7 +1354,11 @@ function applyPanelLayout(persist = false) {
|
|
|
1142
1354
|
}
|
|
1143
1355
|
|
|
1144
1356
|
function ensureAiPanelExpanded() {
|
|
1145
|
-
if (!
|
|
1357
|
+
if (!state.work || !canReadPermissionModule(state.work, "ai-chat")) return;
|
|
1358
|
+
if (isMobileViewport()) {
|
|
1359
|
+
setAiConversationWorkspaceVisible(true);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1146
1362
|
panelLayout.aiCollapsed = false;
|
|
1147
1363
|
applyPanelLayout(true);
|
|
1148
1364
|
}
|
|
@@ -1193,6 +1409,7 @@ let chapterAutoSaveTimer = null;
|
|
|
1193
1409
|
let chapterSaveInFlight = null;
|
|
1194
1410
|
let chapterSaveGuardInFlight = null;
|
|
1195
1411
|
let lastSavedChapterSnapshot = null;
|
|
1412
|
+
let chapterSearchMatchIndex = -1;
|
|
1196
1413
|
let chapterSelectionRequestId = 0;
|
|
1197
1414
|
let chapterForeshadowReminderRequestId = 0;
|
|
1198
1415
|
let chapterForeshadowReminders = [];
|
|
@@ -1234,7 +1451,9 @@ const moduleListPages = {
|
|
|
1234
1451
|
comments: 1,
|
|
1235
1452
|
reviews: 1
|
|
1236
1453
|
};
|
|
1237
|
-
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
1454
|
+
const characterFilters = { raceIds: [], organizationIds: [], genderValues: [], deathState: "all" };
|
|
1455
|
+
const CHARACTER_GENDER_OPTIONS = [["unknown", "未知"], ["male", "男 / 雄"], ["female", "女 / 雌"], ["none", "无性别"]];
|
|
1456
|
+
const CHARACTER_DEATH_STATE_OPTIONS = [["all", "全部状态"], ["alive", "未死亡"], ["dead", "已死亡"]];
|
|
1238
1457
|
let characterFiltersPanelOpen = false;
|
|
1239
1458
|
const settingFilters = { keyword: "", category: "", lockState: "all" };
|
|
1240
1459
|
let settingFiltersPanelOpen = false;
|
|
@@ -1291,8 +1510,9 @@ function applyChapterEditorMode() {
|
|
|
1291
1510
|
$("#chapter-content").setAttribute("aria-readonly", String(viewOnly));
|
|
1292
1511
|
$("#chapter-edit-button").classList.toggle("hidden", permissionBlocked || !chapterEditorReadOnly || !state.chapter);
|
|
1293
1512
|
$("#chapter-delete-button").classList.toggle("hidden", permissionBlocked || chapterEditorReadOnly || !state.chapter);
|
|
1294
|
-
$("#chapter-annotations-button").classList.toggle("hidden", !state.chapter);
|
|
1513
|
+
$("#chapter-annotations-button").classList.toggle("hidden", !state.chapter || !canReadModule("comments"));
|
|
1295
1514
|
$("#chapter-reader-button").classList.toggle("hidden", !state.chapter || !canReadModule("editor"));
|
|
1515
|
+
syncChapterSearchControls();
|
|
1296
1516
|
if (viewOnly) cancelChapterAutoSave();
|
|
1297
1517
|
}
|
|
1298
1518
|
|
|
@@ -1582,22 +1802,36 @@ function renderChapterLineNumbers({ targetLineIndex = null } = {}) {
|
|
|
1582
1802
|
);
|
|
1583
1803
|
const numbers = document.createDocumentFragment();
|
|
1584
1804
|
for (let index = lineWindow.start; index < lineWindow.end; index += 1) {
|
|
1585
|
-
const number = document.createElement("
|
|
1586
|
-
number.type = "button";
|
|
1805
|
+
const number = document.createElement("div");
|
|
1587
1806
|
number.className = "chapter-line-number";
|
|
1588
|
-
number.textContent = String(index + 1);
|
|
1589
1807
|
number.dataset.lineIndex = String(index);
|
|
1590
|
-
|
|
1591
|
-
|
|
1808
|
+
const lineSelect = document.createElement("button");
|
|
1809
|
+
lineSelect.type = "button";
|
|
1810
|
+
lineSelect.className = "chapter-line-number-select";
|
|
1811
|
+
lineSelect.textContent = String(index + 1);
|
|
1812
|
+
lineSelect.setAttribute("aria-label", `选择第 ${index + 1} 行`);
|
|
1813
|
+
lineSelect.tabIndex = -1;
|
|
1814
|
+
number.append(lineSelect);
|
|
1815
|
+
const annotationCount = chapterAnnotationCounts.get(index + 1) ?? 0;
|
|
1816
|
+
if (annotationCount > 0) {
|
|
1817
|
+
const bubble = document.createElement("button");
|
|
1818
|
+
bubble.type = "button";
|
|
1819
|
+
bubble.className = "chapter-line-annotation-count";
|
|
1820
|
+
bubble.dataset.lineIndex = String(index);
|
|
1821
|
+
bubble.dataset.lineAnnotationCount = String(annotationCount);
|
|
1822
|
+
bubble.textContent = String(annotationCount);
|
|
1823
|
+
bubble.setAttribute("aria-label", `查看第 ${index + 1} 行的 ${annotationCount} 条评论和待办`);
|
|
1824
|
+
number.append(bubble);
|
|
1825
|
+
}
|
|
1592
1826
|
const selected = chapterLineSelection && index >= chapterLineSelection.start && index <= chapterLineSelection.end;
|
|
1593
1827
|
if (selected) {
|
|
1594
1828
|
number.classList.add("is-line-selected");
|
|
1595
|
-
|
|
1829
|
+
lineSelect.setAttribute("aria-pressed", "true");
|
|
1596
1830
|
}
|
|
1597
1831
|
const bounds = getLineBounds(index);
|
|
1598
1832
|
number.style.top = `${bounds.top}px`;
|
|
1599
1833
|
number.style.height = `${bounds.bottom - bounds.top}px`;
|
|
1600
|
-
|
|
1834
|
+
lineSelect.style.paddingTop = `${numberTextOffset}px`;
|
|
1601
1835
|
numbers.append(number);
|
|
1602
1836
|
}
|
|
1603
1837
|
inner.replaceChildren(numbers);
|
|
@@ -1673,7 +1907,7 @@ function paintChapterLineSelection(anchor, focus) {
|
|
|
1673
1907
|
$("#chapter-line-numbers-inner").querySelectorAll(".chapter-line-number").forEach((row) => {
|
|
1674
1908
|
const selected = Number(row.dataset.lineIndex) >= start && Number(row.dataset.lineIndex) <= end;
|
|
1675
1909
|
row.classList.toggle("is-line-selected", selected);
|
|
1676
|
-
row.setAttribute("aria-pressed", String(selected));
|
|
1910
|
+
row.querySelector(".chapter-line-number-select")?.setAttribute("aria-pressed", String(selected));
|
|
1677
1911
|
});
|
|
1678
1912
|
}
|
|
1679
1913
|
|
|
@@ -1781,9 +2015,346 @@ function renderAiQuickActions() {
|
|
|
1781
2015
|
quickActions.setAttribute("aria-hidden", String(!visible));
|
|
1782
2016
|
}
|
|
1783
2017
|
|
|
1784
|
-
function
|
|
2018
|
+
function createAiChatFeed() {
|
|
2019
|
+
const feed = document.createElement("div");
|
|
2020
|
+
feed.className = "ai-feed hidden";
|
|
2021
|
+
feed.setAttribute("role", "tabpanel");
|
|
2022
|
+
$("#ai-chat-panels").append(feed);
|
|
2023
|
+
return feed;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function createAiChatTabState(input = {}) {
|
|
2027
|
+
const existing = input.conversationId ? aiChatTabManager.findByConversation(input.conversationId) : null;
|
|
2028
|
+
if (existing) {
|
|
2029
|
+
aiChatTabManager.activate(existing.id);
|
|
2030
|
+
return existing;
|
|
2031
|
+
}
|
|
2032
|
+
const feed = input.feed ?? createAiChatFeed();
|
|
2033
|
+
const tab = aiChatTabManager.open({
|
|
2034
|
+
workId: String(state.work?.id ?? ""),
|
|
2035
|
+
conversationId: input.conversationId ?? null,
|
|
2036
|
+
title: input.title ?? "新对话",
|
|
2037
|
+
modelId: input.modelId ?? null,
|
|
2038
|
+
selectedModelId: input.selectedModelId ?? null,
|
|
2039
|
+
promptSent: input.promptSent === true,
|
|
2040
|
+
taskType: input.taskType ?? "chat",
|
|
2041
|
+
contextScope: input.contextScope ?? { type: "none" },
|
|
2042
|
+
roleplayCharacter: input.roleplayCharacter ?? null,
|
|
2043
|
+
citations: input.citations ?? [],
|
|
2044
|
+
references: input.references ?? [],
|
|
2045
|
+
composer: input.composer ?? { text: "", citations: [], references: [] },
|
|
2046
|
+
contextUsage: input.contextUsage ?? null,
|
|
2047
|
+
contextWarning: input.contextWarning === true,
|
|
2048
|
+
lastMessageAt: input.lastMessageAt ?? null,
|
|
2049
|
+
status: input.status ?? "ready",
|
|
2050
|
+
unread: input.unread === true,
|
|
2051
|
+
feed
|
|
2052
|
+
});
|
|
2053
|
+
feed.dataset.aiTabId = tab.id;
|
|
2054
|
+
feed.setAttribute("aria-labelledby", `ai-chat-tab-${tab.id}`);
|
|
2055
|
+
return tab;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function persistActiveAiChatTab() {
|
|
2059
|
+
const tab = activeAiChatTab();
|
|
2060
|
+
if (!tab) return null;
|
|
2061
|
+
tab.workId = String(state.work?.id ?? "");
|
|
2062
|
+
tab.conversationId = state.aiConversationId;
|
|
2063
|
+
tab.title = $("#ai-conversation-title").textContent || tab.title || "新对话";
|
|
2064
|
+
tab.modelId = state.aiConversationModelId;
|
|
2065
|
+
tab.selectedModelId = $("#ai-model").value || tab.selectedModelId || null;
|
|
2066
|
+
tab.promptSent = state.aiPromptSent;
|
|
2067
|
+
tab.taskType = state.aiTaskType;
|
|
2068
|
+
tab.contextScope = { ...state.aiContextScope };
|
|
2069
|
+
tab.roleplayCharacter = state.aiRoleplayCharacter ? { ...state.aiRoleplayCharacter } : null;
|
|
2070
|
+
setAiChatTabComposerSnapshot(tab, captureAiPromptComposer());
|
|
2071
|
+
tab.contextUsage = latestAiContextUsage;
|
|
2072
|
+
tab.contextWarning = !$("#ai-context-warning").classList.contains("hidden");
|
|
2073
|
+
tab.lastMessageAt = state.aiLastMessageAt;
|
|
2074
|
+
return tab;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function setAiChatTabComposerSnapshot(tab, snapshot) {
|
|
2078
|
+
if (!tab) return;
|
|
2079
|
+
tab.citations = snapshot.citations.map((citation) => ({ ...citation }));
|
|
2080
|
+
tab.references = snapshot.references.map((reference) => ({ ...reference }));
|
|
2081
|
+
tab.composer = {
|
|
2082
|
+
text: snapshot.text,
|
|
2083
|
+
citations: tab.citations.map((citation) => ({ ...citation })),
|
|
2084
|
+
references: tab.references.map((reference) => ({ ...reference }))
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function clearAiChatTabComposer(tab) {
|
|
2089
|
+
setAiChatTabComposerSnapshot(tab, { text: "", citations: [], references: [] });
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function applyAiChatTabState(tab) {
|
|
2093
|
+
state.aiConversationId = tab.conversationId;
|
|
2094
|
+
state.aiConversationModelId = tab.modelId;
|
|
2095
|
+
state.aiPromptSent = tab.promptSent;
|
|
2096
|
+
state.aiCitations = tab.citations.map((citation) => ({ ...citation }));
|
|
2097
|
+
state.aiReferences = tab.references.map((reference) => ({ ...reference }));
|
|
2098
|
+
state.aiLastMessageAt = tab.lastMessageAt;
|
|
2099
|
+
$("#ai-conversation-title").textContent = tab.title || "新对话";
|
|
2100
|
+
applyAiConversationTaskType(tab.taskType);
|
|
2101
|
+
applyAiConversationContextScope(tab.contextScope);
|
|
2102
|
+
applyAiRoleplayCharacter(tab.roleplayCharacter);
|
|
2103
|
+
const selectedModelId = tab.modelId ?? tab.selectedModelId;
|
|
2104
|
+
if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
|
|
2105
|
+
setAiPromptText(tab.composer.text);
|
|
2106
|
+
renderAiCitations();
|
|
2107
|
+
renderAiReferences();
|
|
2108
|
+
latestAiContextUsage = null;
|
|
2109
|
+
setAiContextMeter(tab.contextUsage);
|
|
2110
|
+
if (tab.contextWarning) showAiContextWarning();
|
|
2111
|
+
else hideAiContextWarning();
|
|
2112
|
+
setAiAssistantStatus(tab.status);
|
|
2113
|
+
renderAiQuickActions();
|
|
2114
|
+
renderAiConversationHistory();
|
|
2115
|
+
syncAiRequestControls();
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function aiChatTabIsReplaceable(tab = activeAiChatTab()) {
|
|
2119
|
+
return Boolean(tab
|
|
2120
|
+
&& !tab.conversationId
|
|
2121
|
+
&& !tab.promptSent
|
|
2122
|
+
&& !aiRequestManager.hasActive(tab.id)
|
|
2123
|
+
&& !(tab.composer?.text || "").trim()
|
|
2124
|
+
&& !(tab.composer?.citations?.length)
|
|
2125
|
+
&& !(tab.composer?.references?.length));
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function aiChatTabOpeningSlot() {
|
|
2129
|
+
const active = activeAiChatTab();
|
|
2130
|
+
if (aiChatTabLimit === 1) return { allowed: true, tab: active, replacing: true };
|
|
2131
|
+
if (aiChatTabIsReplaceable(active)) return { allowed: true, tab: active, replacing: true };
|
|
2132
|
+
if (aiChatTabManager.list().length < aiChatTabLimit) return { allowed: true, tab: null, replacing: false };
|
|
2133
|
+
return { allowed: false, tab: null, replacing: false };
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function interruptAiChatTabForReplacement(slot) {
|
|
2137
|
+
if (!slot?.replacing || !slot.tab) return;
|
|
2138
|
+
if (aiRequestManager.cancel("已切换到其他对话", slot.tab.id)) {
|
|
2139
|
+
toast("当前对话仍在生成,切换后已中断该 turn;已收到的内容仍保留在历史记录中", "warning");
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function reportAiChatTabLimit() {
|
|
2144
|
+
toast(`最多同时打开 ${aiChatTabLimit} 个对话,请先关闭一个再继续`, "warning");
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function aiChatTabStatusLabel(tab, selected) {
|
|
2148
|
+
if (tab.status === "streaming") return "正在生成";
|
|
2149
|
+
if (tab.status === "error") return "执行失败";
|
|
2150
|
+
if (tab.unread) return "已完成 · 未读";
|
|
2151
|
+
return selected ? "当前对话" : "空闲";
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function renderAiConversationSwitcher() {
|
|
2155
|
+
const button = $("#ai-conversation-switcher");
|
|
2156
|
+
const list = $("#ai-open-conversation-list");
|
|
2157
|
+
if (!button || !list) return;
|
|
2158
|
+
const tabs = aiChatTabManager.list();
|
|
2159
|
+
const active = activeAiChatTab();
|
|
2160
|
+
const multipleEnabled = aiChatTabLimit > 1;
|
|
2161
|
+
$("#ai-conversation-title").textContent = active?.title || "新对话";
|
|
2162
|
+
$("#ai-open-conversation-count").textContent = String(tabs.length);
|
|
2163
|
+
$("#ai-conversation-switcher-limit").textContent = `${tabs.length} / ${aiChatTabLimit}`;
|
|
2164
|
+
button.disabled = !multipleEnabled;
|
|
2165
|
+
button.setAttribute("aria-disabled", String(!multipleEnabled));
|
|
2166
|
+
button.title = multipleEnabled ? "切换打开的对话" : "多会话已通过环境变量关闭";
|
|
2167
|
+
button.classList.toggle("has-background-activity", tabs.some((tab) => tab.id !== active?.id && (tab.status === "streaming" || tab.unread)));
|
|
2168
|
+
$("#ai-workspace-open").classList.toggle("hidden", !multipleEnabled);
|
|
2169
|
+
list.replaceChildren();
|
|
2170
|
+
for (const tab of tabs) {
|
|
2171
|
+
const selected = tab.id === active?.id;
|
|
2172
|
+
const row = document.createElement("div");
|
|
2173
|
+
row.className = `ai-open-conversation-row${selected ? " is-active" : ""}`;
|
|
2174
|
+
row.setAttribute("role", "listitem");
|
|
2175
|
+
const select = document.createElement("button");
|
|
2176
|
+
select.type = "button";
|
|
2177
|
+
select.className = "ai-open-conversation-select";
|
|
2178
|
+
select.dataset.aiSwitcherTabId = tab.id;
|
|
2179
|
+
select.setAttribute("aria-current", selected ? "true" : "false");
|
|
2180
|
+
const status = document.createElement("span");
|
|
2181
|
+
status.className = `ai-chat-tab-status${tab.status === "streaming" ? " is-streaming" : tab.status === "error" ? " is-error" : tab.unread ? " is-unread" : ""}`;
|
|
2182
|
+
status.setAttribute("aria-hidden", "true");
|
|
2183
|
+
const copy = document.createElement("span");
|
|
2184
|
+
copy.className = "ai-open-conversation-copy";
|
|
2185
|
+
const title = document.createElement("strong");
|
|
2186
|
+
title.textContent = tab.title || "新对话";
|
|
2187
|
+
const meta = document.createElement("small");
|
|
2188
|
+
meta.textContent = aiChatTabStatusLabel(tab, selected);
|
|
2189
|
+
copy.append(title, meta);
|
|
2190
|
+
select.append(status, copy);
|
|
2191
|
+
select.addEventListener("click", () => {
|
|
2192
|
+
activateAiChatTab(tab.id);
|
|
2193
|
+
setAiConversationSwitcherVisible(false);
|
|
2194
|
+
});
|
|
2195
|
+
const close = document.createElement("button");
|
|
2196
|
+
close.type = "button";
|
|
2197
|
+
close.className = "ai-open-conversation-close";
|
|
2198
|
+
close.setAttribute("aria-label", `关闭对话“${tab.title || "新对话"}”`);
|
|
2199
|
+
close.textContent = "×";
|
|
2200
|
+
close.classList.toggle("hidden", tabs.length === 1);
|
|
2201
|
+
close.addEventListener("click", () => closeAiChatTab(tab.id));
|
|
2202
|
+
row.append(select, close);
|
|
2203
|
+
list.append(row);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function setAiConversationSwitcherVisible(visible) {
|
|
2208
|
+
const menu = $("#ai-conversation-switcher-menu");
|
|
2209
|
+
const button = $("#ai-conversation-switcher");
|
|
2210
|
+
if (!menu || !button) return;
|
|
2211
|
+
const nextVisible = Boolean(visible && aiChatTabLimit > 1);
|
|
2212
|
+
menu.classList.toggle("hidden", !nextVisible);
|
|
2213
|
+
button.setAttribute("aria-expanded", String(nextVisible));
|
|
2214
|
+
if (nextVisible) renderAiConversationSwitcher();
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function setAiConversationWorkspaceVisible(visible) {
|
|
2218
|
+
const mobileWorkspace = isMobileViewport();
|
|
2219
|
+
aiConversationWorkspaceOpen = Boolean(visible);
|
|
2220
|
+
panelLayout.aiCollapsed = aiConversationWorkspaceOpen ? false : mobileWorkspace;
|
|
2221
|
+
$("#app").classList.toggle("ai-workspace-mode", aiConversationWorkspaceOpen);
|
|
2222
|
+
$(".ai-panel").classList.toggle("is-conversation-workspace", aiConversationWorkspaceOpen);
|
|
2223
|
+
$("#ai-assistant-entry").classList.toggle("active", aiConversationWorkspaceOpen);
|
|
2224
|
+
$("#ai-assistant-entry").setAttribute("aria-expanded", String(aiConversationWorkspaceOpen));
|
|
2225
|
+
applyPanelLayout(true);
|
|
2226
|
+
$("#ai-chat-tabs").classList.add("hidden");
|
|
2227
|
+
$("#ai-workspace-close").classList.toggle("hidden", !aiConversationWorkspaceOpen);
|
|
2228
|
+
$("#ai-panel-resize").setAttribute("aria-hidden", String(aiConversationWorkspaceOpen));
|
|
2229
|
+
setAiConversationSwitcherVisible(false);
|
|
2230
|
+
renderAiChatTabs();
|
|
2231
|
+
$("#ai-panel-toggle").textContent = aiConversationWorkspaceOpen ? "×" : (panelLayout.aiCollapsed ? "‹" : "›");
|
|
2232
|
+
$("#ai-panel-toggle").setAttribute("aria-label", aiConversationWorkspaceOpen ? "关闭创作助手全屏面板" : (panelLayout.aiCollapsed ? "展开创作助手" : "收起创作助手"));
|
|
2233
|
+
if (aiConversationWorkspaceOpen) {
|
|
2234
|
+
window.requestAnimationFrame(() => $("#ai-prompt").focus({ preventScroll: true }));
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function applyAiChatTabLimit(value) {
|
|
2239
|
+
aiChatTabLimit = normalizeAiChatTabLimit(value);
|
|
2240
|
+
const singleSession = aiChatTabLimit === 1;
|
|
2241
|
+
$(".ai-panel").classList.toggle("is-single-conversation", singleSession);
|
|
2242
|
+
if (singleSession) {
|
|
2243
|
+
setAiConversationSwitcherVisible(false);
|
|
2244
|
+
}
|
|
2245
|
+
renderAiChatTabs();
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function renderAiChatTabs() {
|
|
2249
|
+
const host = $("#ai-chat-tabs");
|
|
2250
|
+
if (!host) return;
|
|
2251
|
+
const tabs = aiChatTabManager.list();
|
|
2252
|
+
const active = activeAiChatTab();
|
|
2253
|
+
host.replaceChildren();
|
|
2254
|
+
for (const tab of tabs) {
|
|
2255
|
+
const item = document.createElement("div");
|
|
2256
|
+
item.className = "ai-chat-tab";
|
|
2257
|
+
item.setAttribute("role", "presentation");
|
|
2258
|
+
const button = document.createElement("button");
|
|
2259
|
+
const selected = tab.id === active?.id;
|
|
2260
|
+
button.type = "button";
|
|
2261
|
+
button.id = `ai-chat-tab-${tab.id}`;
|
|
2262
|
+
button.className = "ai-chat-tab-select";
|
|
2263
|
+
button.dataset.aiTabId = tab.id;
|
|
2264
|
+
button.setAttribute("role", "tab");
|
|
2265
|
+
button.setAttribute("aria-selected", String(selected));
|
|
2266
|
+
button.setAttribute("aria-controls", tab.feed.id || `ai-chat-panel-${tab.id}`);
|
|
2267
|
+
button.tabIndex = selected ? 0 : -1;
|
|
2268
|
+
button.title = tab.title || "新对话";
|
|
2269
|
+
const status = document.createElement("span");
|
|
2270
|
+
status.className = `ai-chat-tab-status${tab.status === "streaming" ? " is-streaming" : tab.status === "error" ? " is-error" : tab.unread ? " is-unread" : ""}`;
|
|
2271
|
+
status.setAttribute("aria-hidden", "true");
|
|
2272
|
+
const title = document.createElement("span");
|
|
2273
|
+
title.textContent = tab.title || "新对话";
|
|
2274
|
+
button.append(status, title);
|
|
2275
|
+
button.addEventListener("click", () => activateAiChatTab(tab.id));
|
|
2276
|
+
button.addEventListener("keydown", (event) => {
|
|
2277
|
+
const index = tabs.findIndex((candidate) => candidate.id === tab.id);
|
|
2278
|
+
const offset = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
|
|
2279
|
+
if (!offset) return;
|
|
2280
|
+
event.preventDefault();
|
|
2281
|
+
const target = tabs[(index + offset + tabs.length) % tabs.length];
|
|
2282
|
+
activateAiChatTab(target.id);
|
|
2283
|
+
$("#ai-conversation-switcher").focus();
|
|
2284
|
+
});
|
|
2285
|
+
const close = document.createElement("button");
|
|
2286
|
+
close.type = "button";
|
|
2287
|
+
close.className = "ai-chat-tab-close";
|
|
2288
|
+
close.dataset.aiTabClose = tab.id;
|
|
2289
|
+
close.setAttribute("aria-label", `关闭对话页签“${tab.title || "新对话"}”`);
|
|
2290
|
+
close.title = "关闭页签";
|
|
2291
|
+
close.textContent = "×";
|
|
2292
|
+
close.classList.toggle("hidden", tabs.length === 1);
|
|
2293
|
+
close.addEventListener("click", () => closeAiChatTab(tab.id));
|
|
2294
|
+
item.append(button, close);
|
|
2295
|
+
host.append(item);
|
|
2296
|
+
}
|
|
2297
|
+
renderAiConversationSwitcher();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
function activateAiChatTab(tabId, { persistCurrent = true, force = false } = {}) {
|
|
2301
|
+
const current = activeAiChatTab();
|
|
2302
|
+
if (!force && current?.id === tabId) return current;
|
|
2303
|
+
if (persistCurrent) persistActiveAiChatTab();
|
|
2304
|
+
const tab = aiChatTabManager.activate(tabId);
|
|
2305
|
+
if (!tab) return null;
|
|
2306
|
+
tab.unread = false;
|
|
2307
|
+
for (const feed of $("#ai-chat-panels").querySelectorAll(".ai-feed")) {
|
|
2308
|
+
feed.classList.toggle("hidden", feed !== tab.feed);
|
|
2309
|
+
feed.id = feed === tab.feed ? "ai-feed" : `ai-chat-panel-${feed.dataset.aiTabId}`;
|
|
2310
|
+
}
|
|
2311
|
+
tab.feed.classList.remove("hidden");
|
|
2312
|
+
applyAiChatTabState(tab);
|
|
2313
|
+
renderAiChatTabs();
|
|
2314
|
+
return tab;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function closeAiChatTab(tabId) {
|
|
2318
|
+
const tab = aiChatTabManager.get(tabId);
|
|
2319
|
+
if (!tab) return;
|
|
2320
|
+
const wasActive = isActiveAiChatTab(tab);
|
|
2321
|
+
if (wasActive) persistActiveAiChatTab();
|
|
2322
|
+
if (aiRequestManager.cancel("对话页签已关闭", tab.id)) {
|
|
2323
|
+
toast(`“${tab.title || "新对话"}”仍在生成,关闭页签已中断该 turn;其他页签不受影响`, "warning");
|
|
2324
|
+
}
|
|
2325
|
+
const { active } = aiChatTabManager.close(tab.id);
|
|
2326
|
+
tab.feed.remove();
|
|
2327
|
+
if (!wasActive) {
|
|
2328
|
+
renderAiChatTabs();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
const next = active ?? createAiChatTabState();
|
|
2332
|
+
activateAiChatTab(next.id, { persistCurrent: false, force: true });
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function resetAiChatTabs() {
|
|
2336
|
+
aiRequestManager.cancelAll("已重置 Agent 对话页签");
|
|
2337
|
+
setAiConversationSwitcherVisible(false);
|
|
2338
|
+
setAiConversationWorkspaceVisible(false);
|
|
2339
|
+
const panels = $("#ai-chat-panels");
|
|
2340
|
+
panels.replaceChildren();
|
|
2341
|
+
aiChatTabManager.reset();
|
|
2342
|
+
const tab = createAiChatTabState();
|
|
2343
|
+
resetAiFeed(tab.feed, tab.roleplayCharacter);
|
|
2344
|
+
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function initializeAiChatTabs() {
|
|
2348
|
+
const feed = $("#ai-feed");
|
|
2349
|
+
aiChatTabManager.reset();
|
|
2350
|
+
const tab = createAiChatTabState({ feed });
|
|
2351
|
+
feed.dataset.aiTabId = tab.id;
|
|
2352
|
+
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function attachMessageHeading(message, label, createdAt = new Date().toISOString(), tab = activeAiChatTab()) {
|
|
1785
2356
|
const timestamp = createdAt || new Date().toISOString();
|
|
1786
|
-
const previousCreatedAt = state.aiLastMessageAt;
|
|
2357
|
+
const previousCreatedAt = tab?.lastMessageAt ?? state.aiLastMessageAt;
|
|
1787
2358
|
const heading = document.createElement("span");
|
|
1788
2359
|
heading.className = "message-heading";
|
|
1789
2360
|
const role = document.createElement("span");
|
|
@@ -1795,7 +2366,8 @@ function attachMessageHeading(message, label, createdAt = new Date().toISOString
|
|
|
1795
2366
|
message.prepend(heading);
|
|
1796
2367
|
message.dataset.createdAt = timestamp;
|
|
1797
2368
|
message.dataset.previousCreatedAt = previousCreatedAt ?? "";
|
|
1798
|
-
|
|
2369
|
+
if (tab) tab.lastMessageAt = timestamp;
|
|
2370
|
+
if (isActiveAiChatTab(tab)) state.aiLastMessageAt = timestamp;
|
|
1799
2371
|
return heading;
|
|
1800
2372
|
}
|
|
1801
2373
|
|
|
@@ -1806,19 +2378,25 @@ function updateMessageCreatedAt(message, createdAt) {
|
|
|
1806
2378
|
time.dateTime = createdAt;
|
|
1807
2379
|
time.textContent = formatAiMessageTime(createdAt);
|
|
1808
2380
|
message.dataset.createdAt = createdAt;
|
|
1809
|
-
|
|
2381
|
+
const tab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2382
|
+
if (message === tab?.feed.lastElementChild) {
|
|
2383
|
+
tab.lastMessageAt = createdAt;
|
|
2384
|
+
if (isActiveAiChatTab(tab)) state.aiLastMessageAt = createdAt;
|
|
2385
|
+
}
|
|
1810
2386
|
}
|
|
1811
2387
|
|
|
1812
|
-
function resetAiFeed() {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2388
|
+
function resetAiFeed(feed = $("#ai-feed"), roleplayCharacter = state.aiRoleplayCharacter) {
|
|
2389
|
+
const tab = aiChatTabManager.get(feed?.dataset.aiTabId);
|
|
2390
|
+
if (tab) tab.lastMessageAt = null;
|
|
2391
|
+
if (isActiveAiChatTab(tab)) state.aiLastMessageAt = null;
|
|
2392
|
+
const roleplayName = roleplayCharacter?.name;
|
|
2393
|
+
feed.innerHTML = roleplayName
|
|
1816
2394
|
? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。我只能通过角色卡和与自己有关的记忆回答。</p></div></div>`
|
|
1817
2395
|
: '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
1818
2396
|
}
|
|
1819
2397
|
|
|
1820
|
-
function aiAssistantLabel(suffix = "") {
|
|
1821
|
-
const name =
|
|
2398
|
+
function aiAssistantLabel(suffix = "", roleplayCharacter = state.aiRoleplayCharacter) {
|
|
2399
|
+
const name = roleplayCharacter?.name || "助手";
|
|
1822
2400
|
return suffix ? `${name} · ${suffix}` : name;
|
|
1823
2401
|
}
|
|
1824
2402
|
|
|
@@ -1834,26 +2412,24 @@ function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "
|
|
|
1834
2412
|
return divider;
|
|
1835
2413
|
}
|
|
1836
2414
|
|
|
1837
|
-
function appendAiContextCompactionDivider(kind, before = null) {
|
|
2415
|
+
function appendAiContextCompactionDivider(kind, before = null, feed = $("#ai-feed")) {
|
|
1838
2416
|
const divider = createAiContextCompactionDivider({ kind });
|
|
1839
|
-
const feed = $("#ai-feed");
|
|
1840
2417
|
if (before?.parentElement === feed) feed.insertBefore(divider, before);
|
|
1841
2418
|
else feed.append(divider);
|
|
1842
|
-
scrollAiFeedToBottom();
|
|
2419
|
+
scrollAiFeedToBottom(feed);
|
|
1843
2420
|
return divider;
|
|
1844
2421
|
}
|
|
1845
2422
|
|
|
1846
|
-
function renderConversationCompactionDivider(compactedMessageCount, totalMessageCount = null) {
|
|
1847
|
-
const feed = $("#ai-feed");
|
|
2423
|
+
function renderConversationCompactionDivider(compactedMessageCount, totalMessageCount = null, feed = $("#ai-feed"), conversation = null) {
|
|
1848
2424
|
feed.querySelector('[data-context-compaction="conversation"]')?.remove();
|
|
1849
2425
|
const compactedCount = Math.max(0, Number(compactedMessageCount) || 0);
|
|
1850
2426
|
if (compactedCount === 0) return null;
|
|
1851
2427
|
const messages = [...feed.querySelectorAll("[data-message-id]")];
|
|
1852
|
-
const current = state.aiConversations.find((
|
|
2428
|
+
const current = conversation ?? state.aiConversations.find((item) => item.id === state.aiConversationId);
|
|
1853
2429
|
const totalCount = Math.max(messages.length, Number(totalMessageCount ?? current?.messageCount) || messages.length);
|
|
1854
2430
|
const loadedOffset = Math.max(0, totalCount - messages.length);
|
|
1855
2431
|
const boundaryIndex = Math.max(0, Math.min(messages.length, compactedCount - loadedOffset));
|
|
1856
|
-
return appendAiContextCompactionDivider("conversation", messages[boundaryIndex] ?? null);
|
|
2432
|
+
return appendAiContextCompactionDivider("conversation", messages[boundaryIndex] ?? null, feed);
|
|
1857
2433
|
}
|
|
1858
2434
|
|
|
1859
2435
|
function renderMessageCardActions(message) {
|
|
@@ -1864,16 +2440,18 @@ function renderMessageCardActions(message) {
|
|
|
1864
2440
|
message.append(actions);
|
|
1865
2441
|
}
|
|
1866
2442
|
actions.replaceChildren();
|
|
1867
|
-
|
|
2443
|
+
const hasCopyValue = Object.hasOwn(message.dataset, "rawMarkdown") || Object.hasOwn(message.dataset, "copyText");
|
|
2444
|
+
if (hasCopyValue) {
|
|
1868
2445
|
const copy = document.createElement("button");
|
|
1869
2446
|
copy.type = "button";
|
|
1870
2447
|
copy.className = "message-copy-button";
|
|
1871
|
-
|
|
2448
|
+
const isUserMessage = message.classList.contains("user-message");
|
|
2449
|
+
copy.setAttribute("aria-label", isUserMessage ? "复制用户指令" : "复制 AI 回复");
|
|
1872
2450
|
copy.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg><span>复制</span>';
|
|
1873
2451
|
const copyLabel = copy.querySelector("span");
|
|
1874
2452
|
copy.addEventListener("click", async () => {
|
|
1875
2453
|
try {
|
|
1876
|
-
await copyAiRawMarkdown(message.dataset.rawMarkdown);
|
|
2454
|
+
await copyAiRawMarkdown(message.dataset.rawMarkdown ?? message.dataset.copyText ?? "");
|
|
1877
2455
|
copyLabel.textContent = "已复制";
|
|
1878
2456
|
window.setTimeout(() => { copyLabel.textContent = "复制"; }, 1200);
|
|
1879
2457
|
} catch (error) {
|
|
@@ -1890,9 +2468,16 @@ function renderMessageCardActions(message) {
|
|
|
1890
2468
|
fork.innerHTML = '<svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="5" r="2"/><circle cx="18" cy="5" r="2"/><circle cx="12" cy="19" r="2"/><path d="M6 7v2a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4V7M12 13v4"/></svg><span>从此续写</span>';
|
|
1891
2469
|
const forkRequestId = createAiIdempotencyKey();
|
|
1892
2470
|
fork.addEventListener("click", async () => {
|
|
2471
|
+
const openingSlot = aiChatTabOpeningSlot();
|
|
2472
|
+
if (!openingSlot.allowed) {
|
|
2473
|
+
reportAiChatTabLimit();
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
1893
2476
|
fork.disabled = true;
|
|
1894
2477
|
try {
|
|
1895
|
-
const
|
|
2478
|
+
const sourceTab = aiChatTabManager.get(message.closest(".ai-feed")?.dataset.aiTabId);
|
|
2479
|
+
if (!sourceTab?.conversationId) throw new Error("无法确定消息所属对话");
|
|
2480
|
+
const conversation = await api(`/api/ai-conversations/${sourceTab.conversationId}/fork`, {
|
|
1896
2481
|
method: "POST",
|
|
1897
2482
|
body: { messageId: message.dataset.messageId, requestId: forkRequestId }
|
|
1898
2483
|
});
|
|
@@ -1916,6 +2501,11 @@ function attachAssistantCopyAction(message, rawMarkdown) {
|
|
|
1916
2501
|
renderMessageCardActions(message);
|
|
1917
2502
|
}
|
|
1918
2503
|
|
|
2504
|
+
function attachUserCopyAction(message, text) {
|
|
2505
|
+
message.dataset.copyText = String(text ?? "");
|
|
2506
|
+
renderMessageCardActions(message);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
1919
2509
|
function attachMessageIdentity(message, messageId) {
|
|
1920
2510
|
if (!messageId) return;
|
|
1921
2511
|
message.dataset.messageId = messageId;
|
|
@@ -1931,7 +2521,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1931
2521
|
search_drafts: "搜索想法",
|
|
1932
2522
|
recall_self: "回忆自身",
|
|
1933
2523
|
image: "读取设定图片",
|
|
1934
|
-
recall_relationship: "回忆人物关系"
|
|
2524
|
+
recall_relationship: "回忆人物关系",
|
|
2525
|
+
calculate_time: "计算日期"
|
|
1935
2526
|
};
|
|
1936
2527
|
|
|
1937
2528
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1943,10 +2534,11 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
1943
2534
|
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。",
|
|
1944
2535
|
recall_self: "读取当前扮演角色自己的角色卡、档案,以及自己参与的关系、时间线和正文记忆。",
|
|
1945
2536
|
image: "读取设定正文引用的图片附件,并返回多模态模型的理解内容。",
|
|
1946
|
-
recall_relationship: "不传角色列表时读取有关系的角色列表;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。"
|
|
2537
|
+
recall_relationship: "不传角色列表时读取有关系的角色列表;传入一个或多个角色后读取当前角色与这些角色之间的关系详情。",
|
|
2538
|
+
calculate_time: "计算日期差值,或从起始日期推算目标日期。"
|
|
1947
2539
|
};
|
|
1948
2540
|
|
|
1949
|
-
|
|
2541
|
+
const aiFeedScrollFrames = new WeakMap();
|
|
1950
2542
|
let markdownTableMenuTarget = null;
|
|
1951
2543
|
let markdownTableMenuTrigger = null;
|
|
1952
2544
|
|
|
@@ -1987,14 +2579,15 @@ function openMarkdownTableMenu(header, clientX, clientY) {
|
|
|
1987
2579
|
toggle.focus();
|
|
1988
2580
|
}
|
|
1989
2581
|
|
|
1990
|
-
function scrollAiFeedToBottom() {
|
|
1991
|
-
const feed = $("#ai-feed");
|
|
2582
|
+
function scrollAiFeedToBottom(feed = $("#ai-feed")) {
|
|
1992
2583
|
feed.scrollTop = feed.scrollHeight;
|
|
1993
|
-
|
|
1994
|
-
|
|
2584
|
+
const currentFrame = aiFeedScrollFrames.get(feed);
|
|
2585
|
+
if (currentFrame !== undefined) window.cancelAnimationFrame(currentFrame);
|
|
2586
|
+
const nextFrame = window.requestAnimationFrame(() => {
|
|
1995
2587
|
feed.scrollTop = feed.scrollHeight;
|
|
1996
|
-
|
|
2588
|
+
aiFeedScrollFrames.delete(feed);
|
|
1997
2589
|
});
|
|
2590
|
+
aiFeedScrollFrames.set(feed, nextFrame);
|
|
1998
2591
|
}
|
|
1999
2592
|
|
|
2000
2593
|
function formatAiToolCallTime(value) {
|
|
@@ -2012,9 +2605,14 @@ function formatAiToolCallTime(value) {
|
|
|
2012
2605
|
}).format(date);
|
|
2013
2606
|
}
|
|
2014
2607
|
|
|
2608
|
+
const AI_TOOL_CALL_COPY_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="8" y="8" width="12" height="12" rx="2"></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path></svg>';
|
|
2609
|
+
const AI_TOOL_CALL_COPIED_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 4 4L19 6"></path></svg>';
|
|
2610
|
+
|
|
2015
2611
|
function setAiToolCallCopyButtonState(button, copied) {
|
|
2016
2612
|
const label = button.dataset.copyLabel ?? "代码块";
|
|
2017
|
-
|
|
2613
|
+
const nextState = copied ? "copied" : "idle";
|
|
2614
|
+
if (button.dataset.copyState !== nextState) button.innerHTML = copied ? AI_TOOL_CALL_COPIED_ICON : AI_TOOL_CALL_COPY_ICON;
|
|
2615
|
+
button.dataset.copyState = nextState;
|
|
2018
2616
|
button.classList.toggle("is-copied", copied);
|
|
2019
2617
|
button.setAttribute("aria-label", copied ? `${label}已复制` : `复制${label}`);
|
|
2020
2618
|
button.title = copied ? "已复制" : `复制${label}`;
|
|
@@ -2233,23 +2831,15 @@ async function downloadAiConversation(conversation) {
|
|
|
2233
2831
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
|
2234
2832
|
}
|
|
2235
2833
|
|
|
2834
|
+
async function copyAiConversationSessionId(conversation) {
|
|
2835
|
+
const sessionId = typeof conversation?.id === "string" ? conversation.id.trim() : "";
|
|
2836
|
+
if (!sessionId) throw new Error("无法确定对话 Session ID");
|
|
2837
|
+
await copyAiRawMarkdown(sessionId);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2236
2840
|
function resetAiConversationAfterDelete() {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
state.aiPromptSent = false;
|
|
2240
|
-
state.aiCitations = [];
|
|
2241
|
-
state.aiReferences = [];
|
|
2242
|
-
applyAiConversationTaskType("chat");
|
|
2243
|
-
applyAiConversationContextScope({ type: "none" });
|
|
2244
|
-
applyAiRoleplayCharacter(null);
|
|
2245
|
-
$("#ai-conversation-title").textContent = "新对话";
|
|
2246
|
-
resetAiFeed();
|
|
2247
|
-
hideAiContextWarning();
|
|
2248
|
-
resetAiContextMeter();
|
|
2249
|
-
setAiPromptText("");
|
|
2250
|
-
renderAiCitations();
|
|
2251
|
-
renderAiReferences();
|
|
2252
|
-
renderAiQuickActions();
|
|
2841
|
+
const tab = activeAiChatTab();
|
|
2842
|
+
if (tab) closeAiChatTab(tab.id);
|
|
2253
2843
|
}
|
|
2254
2844
|
|
|
2255
2845
|
async function favoriteAiConversation(conversation) {
|
|
@@ -2272,7 +2862,11 @@ async function deleteAiConversation(conversation) {
|
|
|
2272
2862
|
})) return;
|
|
2273
2863
|
await api(`/api/ai-conversations/${encodeURIComponent(conversation.id)}`, { method: "DELETE" });
|
|
2274
2864
|
state.aiConversations = state.aiConversations.filter((item) => item.id !== conversation.id);
|
|
2275
|
-
|
|
2865
|
+
const tab = aiChatTabManager.findByConversation(conversation.id);
|
|
2866
|
+
if (tab) {
|
|
2867
|
+
if (isActiveAiChatTab(tab)) resetAiConversationAfterDelete();
|
|
2868
|
+
else closeAiChatTab(tab.id);
|
|
2869
|
+
}
|
|
2276
2870
|
renderAiConversationHistory();
|
|
2277
2871
|
toast("对话已清理");
|
|
2278
2872
|
}
|
|
@@ -2408,8 +3002,11 @@ function applyAiConversationTitle(title, conversationId = state.aiConversationId
|
|
|
2408
3002
|
if (!title || !conversationId) return;
|
|
2409
3003
|
const current = state.aiConversations.find((conversation) => conversation.id === conversationId);
|
|
2410
3004
|
if (current) current.title = title;
|
|
3005
|
+
const tab = aiChatTabManager.findByConversation(conversationId);
|
|
3006
|
+
if (tab) tab.title = title;
|
|
2411
3007
|
if (state.aiConversationId === conversationId) $("#ai-conversation-title").textContent = title;
|
|
2412
3008
|
renderAiConversationHistory();
|
|
3009
|
+
renderAiChatTabs();
|
|
2413
3010
|
}
|
|
2414
3011
|
|
|
2415
3012
|
function applyAiConversations(pageResult) {
|
|
@@ -2453,6 +3050,21 @@ async function ensureAiConversationsLoaded() {
|
|
|
2453
3050
|
|
|
2454
3051
|
async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
|
|
2455
3052
|
if (!state.work) return null;
|
|
3053
|
+
const existingTab = aiChatTabManager.findByConversation(conversationId);
|
|
3054
|
+
const existingMessage = focusMessageId
|
|
3055
|
+
? existingTab?.feed.querySelector(`[data-message-id="${CSS.escape(String(focusMessageId))}"]`)
|
|
3056
|
+
: null;
|
|
3057
|
+
if (existingTab && (!focusMessageId || existingMessage || aiRequestManager.hasActive(existingTab.id))) {
|
|
3058
|
+
activateAiChatTab(existingTab.id);
|
|
3059
|
+
if (focusMessageId && existingMessage) focusAiConversationMessage(focusMessageId);
|
|
3060
|
+
if (hideHistory) setAiHistoryVisible(false);
|
|
3061
|
+
return state.aiConversations.find((conversation) => conversation.id === conversationId) ?? existingTab;
|
|
3062
|
+
}
|
|
3063
|
+
let openingSlot = existingTab ? { allowed: true, tab: existingTab, replacing: false } : aiChatTabOpeningSlot();
|
|
3064
|
+
if (!openingSlot.allowed) {
|
|
3065
|
+
reportAiChatTabLimit();
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
2456
3068
|
const navigation = beginAiConversationNavigation("已切换 AI 对话", "切换会话");
|
|
2457
3069
|
try {
|
|
2458
3070
|
const parameters = new URLSearchParams({ page: "1", limit: "100" });
|
|
@@ -2464,28 +3076,18 @@ async function openAiConversation(conversationId, hideHistory = true, focusMessa
|
|
|
2464
3076
|
if (!isAiConversationNavigationCurrent(navigation)) return null;
|
|
2465
3077
|
if (String(conversation.workId ?? "") !== navigation.workId) throw new Error("AI 对话不属于当前作品");
|
|
2466
3078
|
upsertAiConversationSummary(conversation);
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
applyAiRoleplayCharacter(conversation.roleplayCharacter);
|
|
2473
|
-
resetAiContextMeter();
|
|
2474
|
-
$("#ai-conversation-title").textContent = conversation.title;
|
|
2475
|
-
resetAiFeed();
|
|
2476
|
-
for (const message of conversation.messages) appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id);
|
|
2477
|
-
if (conversation.hasCompactedSummary) {
|
|
2478
|
-
renderConversationCompactionDivider(conversation.compactedMessageCount, conversation.messageCount);
|
|
3079
|
+
persistActiveAiChatTab();
|
|
3080
|
+
if (!existingTab) openingSlot = aiChatTabOpeningSlot();
|
|
3081
|
+
if (!openingSlot.allowed) {
|
|
3082
|
+
reportAiChatTabLimit();
|
|
3083
|
+
return null;
|
|
2479
3084
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
renderAiConversationHistory();
|
|
2487
|
-
if (conversation.contextWarningPending) showAiContextWarning();
|
|
2488
|
-
else hideAiContextWarning();
|
|
3085
|
+
interruptAiChatTabForReplacement(openingSlot);
|
|
3086
|
+
let tab = existingTab ?? openingSlot.tab;
|
|
3087
|
+
if (!tab) tab = createAiChatTabState();
|
|
3088
|
+
applyConversationToAiChatTab(tab, conversation);
|
|
3089
|
+
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
3090
|
+
if (focusMessageId) window.requestAnimationFrame(() => focusAiConversationMessage(focusMessageId));
|
|
2489
3091
|
if (hideHistory) setAiHistoryVisible(false);
|
|
2490
3092
|
return conversation;
|
|
2491
3093
|
} catch (error) {
|
|
@@ -2505,30 +3107,60 @@ function focusAiConversationMessage(messageId) {
|
|
|
2505
3107
|
window.setTimeout(() => message.classList.remove("is-search-target"), 1800);
|
|
2506
3108
|
}
|
|
2507
3109
|
|
|
2508
|
-
function
|
|
3110
|
+
function applyConversationToAiChatTab(tab, conversation) {
|
|
3111
|
+
tab.workId = String(conversation.workId ?? state.work?.id ?? "");
|
|
3112
|
+
tab.conversationId = conversation.id;
|
|
3113
|
+
tab.title = conversation.title || "新对话";
|
|
3114
|
+
tab.modelId = typeof conversation.modelId === "string" ? conversation.modelId : null;
|
|
3115
|
+
tab.selectedModelId = tab.modelId ?? tab.selectedModelId;
|
|
3116
|
+
tab.promptSent = Array.isArray(conversation.messages) && conversation.messages.some((message) => message.role === "user");
|
|
3117
|
+
tab.taskType = conversation.taskType ?? "chat";
|
|
3118
|
+
tab.contextScope = conversation.contextScope ?? { type: "none" };
|
|
3119
|
+
tab.roleplayCharacter = conversation.roleplayCharacter ?? null;
|
|
3120
|
+
tab.citations = [];
|
|
3121
|
+
tab.references = [];
|
|
3122
|
+
tab.composer = { text: "", citations: [], references: [] };
|
|
3123
|
+
tab.contextUsage = null;
|
|
3124
|
+
tab.contextWarning = conversation.contextWarningPending === true;
|
|
3125
|
+
resetAiFeed(tab.feed, tab.roleplayCharacter);
|
|
3126
|
+
for (const message of conversation.messages ?? []) {
|
|
3127
|
+
appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id, { tab });
|
|
3128
|
+
}
|
|
3129
|
+
if (conversation.hasCompactedSummary) {
|
|
3130
|
+
renderConversationCompactionDivider(conversation.compactedMessageCount, conversation.messageCount, tab.feed, conversation);
|
|
3131
|
+
}
|
|
3132
|
+
renderAiChatTabs();
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
function applyNewAiConversation(conversation, tab = activeAiChatTab()) {
|
|
2509
3136
|
upsertAiConversationSummary(conversation);
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
state.aiPromptSent = false;
|
|
2513
|
-
applyAiConversationTaskType(conversation.taskType);
|
|
2514
|
-
applyAiConversationContextScope(conversation.contextScope);
|
|
2515
|
-
applyAiRoleplayCharacter(conversation.roleplayCharacter);
|
|
2516
|
-
$("#ai-conversation-title").textContent = conversation.title;
|
|
2517
|
-
resetAiFeed();
|
|
2518
|
-
hideAiContextWarning();
|
|
2519
|
-
resetAiContextMeter();
|
|
2520
|
-
renderAiQuickActions();
|
|
3137
|
+
applyConversationToAiChatTab(tab, { ...conversation, messages: [] });
|
|
3138
|
+
if (isActiveAiChatTab(tab)) applyAiChatTabState(tab);
|
|
2521
3139
|
setAiHistoryVisible(false);
|
|
2522
3140
|
}
|
|
2523
3141
|
|
|
2524
3142
|
async function createNewAiConversation(taskType = "chat") {
|
|
2525
3143
|
if (!state.work) return null;
|
|
3144
|
+
let openingSlot = aiChatTabOpeningSlot();
|
|
3145
|
+
if (!openingSlot.allowed) {
|
|
3146
|
+
reportAiChatTabLimit();
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
2526
3149
|
const navigation = beginAiConversationNavigation("已新建 AI 对话", "新建会话");
|
|
2527
3150
|
try {
|
|
2528
3151
|
const conversation = await api(`/api/works/${navigation.workId}/ai-conversations`, { method: "POST", body: { taskType } });
|
|
2529
3152
|
if (!isAiConversationNavigationCurrent(navigation)) return null;
|
|
2530
3153
|
if (String(conversation.workId ?? "") !== navigation.workId) throw new Error("新对话不属于当前作品");
|
|
2531
|
-
|
|
3154
|
+
persistActiveAiChatTab();
|
|
3155
|
+
openingSlot = aiChatTabOpeningSlot();
|
|
3156
|
+
if (!openingSlot.allowed) {
|
|
3157
|
+
reportAiChatTabLimit();
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
interruptAiChatTabForReplacement(openingSlot);
|
|
3161
|
+
const tab = openingSlot.tab ?? createAiChatTabState();
|
|
3162
|
+
applyNewAiConversation(conversation, tab);
|
|
3163
|
+
activateAiChatTab(tab.id, { persistCurrent: false, force: true });
|
|
2532
3164
|
return conversation;
|
|
2533
3165
|
} catch (error) {
|
|
2534
3166
|
if (!isAiConversationNavigationCurrent(navigation)) return null;
|
|
@@ -2610,6 +3242,8 @@ function syncAiTaskOptions() {
|
|
|
2610
3242
|
function applyAiConversationTaskType(taskType) {
|
|
2611
3243
|
const normalizedTaskType = ["chat", "roleplay", "continue", "polish"].includes(taskType) ? taskType : "chat";
|
|
2612
3244
|
state.aiTaskType = normalizedTaskType;
|
|
3245
|
+
const tab = activeAiChatTab();
|
|
3246
|
+
if (tab) tab.taskType = normalizedTaskType;
|
|
2613
3247
|
$("#ai-task").value = normalizedTaskType;
|
|
2614
3248
|
$("#ai-task").dataset.previousValue = normalizedTaskType;
|
|
2615
3249
|
syncAiTaskOptions();
|
|
@@ -2618,6 +3252,8 @@ function applyAiConversationTaskType(taskType) {
|
|
|
2618
3252
|
function applyAiConversationContextScope(scope) {
|
|
2619
3253
|
const normalizedScope = scope && typeof scope === "object" ? JSON.parse(JSON.stringify(scope)) : { type: "none" };
|
|
2620
3254
|
state.aiContextScope = normalizedScope;
|
|
3255
|
+
const tab = activeAiChatTab();
|
|
3256
|
+
if (tab) tab.contextScope = { ...normalizedScope };
|
|
2621
3257
|
$("#ai-scope").value = normalizedScope.type === "book" ? "book"
|
|
2622
3258
|
: normalizedScope.type === "volume" ? "volume"
|
|
2623
3259
|
: normalizedScope.type === "chapter" && normalizedScope.includeBookSummary ? "chapter-summary"
|
|
@@ -2628,6 +3264,8 @@ function applyAiConversationContextScope(scope) {
|
|
|
2628
3264
|
|
|
2629
3265
|
function applyAiRoleplayCharacter(character) {
|
|
2630
3266
|
state.aiRoleplayCharacter = character?.id ? character : null;
|
|
3267
|
+
const tab = activeAiChatTab();
|
|
3268
|
+
if (tab) tab.roleplayCharacter = state.aiRoleplayCharacter ? { ...state.aiRoleplayCharacter } : null;
|
|
2631
3269
|
const active = Boolean(state.aiRoleplayCharacter);
|
|
2632
3270
|
if (active) $("#ai-scope").value = "none";
|
|
2633
3271
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
@@ -2693,6 +3331,8 @@ async function persistAiConversationContextScope(scope) {
|
|
|
2693
3331
|
|
|
2694
3332
|
async function prepareAiRequestConversation(requestHolder, taskType, scope) {
|
|
2695
3333
|
let request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
3334
|
+
const tab = aiChatTabForRequest(request);
|
|
3335
|
+
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
2696
3336
|
if (!request.conversationId) {
|
|
2697
3337
|
const conversation = await api(`/api/works/${encodeURIComponent(request.workId)}/ai-conversations`, {
|
|
2698
3338
|
method: "POST",
|
|
@@ -2701,7 +3341,7 @@ async function prepareAiRequestConversation(requestHolder, taskType, scope) {
|
|
|
2701
3341
|
});
|
|
2702
3342
|
assertAiRequestCurrent(request);
|
|
2703
3343
|
if (String(conversation.workId ?? "") !== request.workId) throw new Error("新对话不属于请求作品");
|
|
2704
|
-
applyNewAiConversation(conversation);
|
|
3344
|
+
applyNewAiConversation(conversation, tab);
|
|
2705
3345
|
requestHolder.snapshot = aiRequestManager.bind(request, { conversationId: conversation.id });
|
|
2706
3346
|
request = requestHolder.snapshot;
|
|
2707
3347
|
}
|
|
@@ -2712,8 +3352,12 @@ async function prepareAiRequestConversation(requestHolder, taskType, scope) {
|
|
|
2712
3352
|
});
|
|
2713
3353
|
assertAiRequestCurrent(request);
|
|
2714
3354
|
upsertAiConversationSummary(taskConversation);
|
|
2715
|
-
|
|
2716
|
-
|
|
3355
|
+
tab.taskType = taskConversation.taskType;
|
|
3356
|
+
tab.roleplayCharacter = taskConversation.roleplayCharacter ?? null;
|
|
3357
|
+
if (isActiveAiChatTab(tab)) {
|
|
3358
|
+
applyAiConversationTaskType(tab.taskType);
|
|
3359
|
+
applyAiRoleplayCharacter(tab.roleplayCharacter);
|
|
3360
|
+
}
|
|
2717
3361
|
|
|
2718
3362
|
const scopedConversation = await api(`/api/ai-conversations/${encodeURIComponent(request.conversationId)}/context-scope`, {
|
|
2719
3363
|
method: "PATCH",
|
|
@@ -2722,7 +3366,8 @@ async function prepareAiRequestConversation(requestHolder, taskType, scope) {
|
|
|
2722
3366
|
});
|
|
2723
3367
|
assertAiRequestCurrent(request);
|
|
2724
3368
|
upsertAiConversationSummary(scopedConversation);
|
|
2725
|
-
|
|
3369
|
+
tab.contextScope = scopedConversation.contextScope;
|
|
3370
|
+
if (isActiveAiChatTab(tab)) applyAiConversationContextScope(tab.contextScope);
|
|
2726
3371
|
return request;
|
|
2727
3372
|
}
|
|
2728
3373
|
|
|
@@ -2746,13 +3391,29 @@ async function persistAiRequestInterruption(request, interruption = null) {
|
|
|
2746
3391
|
const partialContent = typeof interruption?.content === "string" && interruption.content.trim()
|
|
2747
3392
|
? interruption.content
|
|
2748
3393
|
: null;
|
|
3394
|
+
const cancellationMessage = request.signal.reason instanceof Error
|
|
3395
|
+
? request.signal.reason.message
|
|
3396
|
+
: "AI 请求已取消";
|
|
3397
|
+
const cancelledByClient = request.signal.reason?.code === "AI_REQUEST_CANCELLED";
|
|
3398
|
+
const metadata = cancelledByClient
|
|
3399
|
+
? {
|
|
3400
|
+
...(partialContent ? interruption.metadata : {}),
|
|
3401
|
+
interrupted: true,
|
|
3402
|
+
interruptionCode: "AI_REQUEST_CANCELLED",
|
|
3403
|
+
interruptionMessage: cancellationMessage.slice(0, 500)
|
|
3404
|
+
}
|
|
3405
|
+
: interruption?.metadata ?? {
|
|
3406
|
+
interrupted: true,
|
|
3407
|
+
interruptionCode: "AI_REQUEST_CANCELLED",
|
|
3408
|
+
interruptionMessage: cancellationMessage.slice(0, 500)
|
|
3409
|
+
};
|
|
2749
3410
|
try {
|
|
2750
3411
|
const message = await persistAiConversationMessage(
|
|
2751
3412
|
request.conversationId,
|
|
2752
3413
|
"assistant",
|
|
2753
|
-
partialContent ?? "
|
|
3414
|
+
partialContent ?? "生成已终止,尚未收到可保留的回复内容。",
|
|
2754
3415
|
[],
|
|
2755
|
-
|
|
3416
|
+
metadata,
|
|
2756
3417
|
{ requestId: aiAssistantRequestId(request) }
|
|
2757
3418
|
);
|
|
2758
3419
|
updateAiConversationSummaryFromMessage(message);
|
|
@@ -2951,7 +3612,8 @@ function addSelectedLinesAsCitation() {
|
|
|
2951
3612
|
}
|
|
2952
3613
|
|
|
2953
3614
|
async function createSelectedLineAnnotation(kind) {
|
|
2954
|
-
|
|
3615
|
+
const permissionModule = kind === "todo" ? "todos" : "comments";
|
|
3616
|
+
if (!state.chapter || !chapterLineSelection || !canWritePermissionModule(state.work, permissionModule)) return;
|
|
2955
3617
|
const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
|
|
2956
3618
|
closeLineCitationMenu();
|
|
2957
3619
|
const note = await inputToast(kind === "todo" ? "描述需要后续处理的事项" : `评论第 ${selection.safeStart + 1} 行正文`, {
|
|
@@ -2967,7 +3629,8 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
2967
3629
|
body: { kind, startLine: selection.safeStart + 1, endLine: selection.safeEnd + 1, note }
|
|
2968
3630
|
});
|
|
2969
3631
|
toast(kind === "todo" ? "正文待办已添加" : "正文评论已添加");
|
|
2970
|
-
await
|
|
3632
|
+
await loadChapterAnnotationCounts();
|
|
3633
|
+
await openChapterAnnotationsDialog(selection.safeStart);
|
|
2971
3634
|
} catch (error) {
|
|
2972
3635
|
toast(error.message, "error");
|
|
2973
3636
|
}
|
|
@@ -2995,7 +3658,7 @@ function chapterAnnotationCard(annotation, { showSource = false } = {}) {
|
|
|
2995
3658
|
<blockquote>${esc(annotation.quote || "空白行")}</blockquote>
|
|
2996
3659
|
<p data-annotation-content>${esc(annotation.note)}</p>
|
|
2997
3660
|
<small>${esc(annotation.actor)} · ${esc(formatDateTime(annotation.updatedAt))} · v${Number(annotation.versionNo)}</small>
|
|
2998
|
-
<footer><button type="button" data-annotation-locate>定位原文</button>${
|
|
3661
|
+
<footer><button type="button" data-annotation-locate>定位原文</button>${canManageChapterAnnotation(annotation) ? `<button type="button" data-annotation-edit>${annotation.kind === "todo" ? "编辑待办" : "编辑评论"}</button><button type="button" data-annotation-status>${annotation.status === "resolved" ? "重新打开" : (annotation.kind === "todo" ? "完成待办" : "解决评论")}</button><button class="danger-button" type="button" data-annotation-delete>${annotation.kind === "todo" ? "删除待办" : "删除评论"}</button>` : ""}</footer>
|
|
2999
3662
|
</article>`;
|
|
3000
3663
|
}
|
|
3001
3664
|
|
|
@@ -3055,9 +3718,12 @@ function renderChapterAnnotations() {
|
|
|
3055
3718
|
const host = $("#chapter-annotations-list");
|
|
3056
3719
|
host.innerHTML = chapterAnnotations.length
|
|
3057
3720
|
? chapterAnnotations.map((annotation) => chapterAnnotationCard(annotation)).join("")
|
|
3058
|
-
:
|
|
3721
|
+
: `<p class="entity-history-empty">${chapterAnnotationsLineIndex === null ? "本章还没有评论。在正文行上点击右键即可添加。" : `第 ${chapterAnnotationsLineIndex + 1} 行还没有评论或待办。`}</p>`;
|
|
3059
3722
|
bindChapterAnnotationCards(host, chapterAnnotations, {
|
|
3060
|
-
refresh:
|
|
3723
|
+
refresh: async () => {
|
|
3724
|
+
await loadChapterAnnotationCounts();
|
|
3725
|
+
await loadChapterAnnotations(chapterAnnotationsLineIndex);
|
|
3726
|
+
},
|
|
3061
3727
|
locate: (annotation) => {
|
|
3062
3728
|
$("#chapter-annotations-dialog").close();
|
|
3063
3729
|
paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
|
|
@@ -3067,19 +3733,43 @@ function renderChapterAnnotations() {
|
|
|
3067
3733
|
});
|
|
3068
3734
|
}
|
|
3069
3735
|
|
|
3070
|
-
async function
|
|
3736
|
+
async function loadChapterAnnotationCounts(chapterId = state.chapter?.id) {
|
|
3737
|
+
if (!chapterId || !canReadModule("comments") && !canReadModule("todos")) {
|
|
3738
|
+
chapterAnnotationCounts = new Map();
|
|
3739
|
+
scheduleChapterLineNumbers();
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
const counts = await api(`/api/chapters/${encodeURIComponent(chapterId)}/annotation-counts`);
|
|
3743
|
+
if (String(state.chapter?.id ?? "") !== String(chapterId)) return;
|
|
3744
|
+
chapterAnnotationCounts = new Map(
|
|
3745
|
+
(Array.isArray(counts) ? counts : [])
|
|
3746
|
+
.map((item) => [Number(item.line), Number(item.count)])
|
|
3747
|
+
.filter(([line, count]) => Number.isInteger(line) && line > 0 && Number.isInteger(count) && count > 0)
|
|
3748
|
+
);
|
|
3749
|
+
scheduleChapterLineNumbers();
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
async function loadChapterAnnotations(lineIndex = chapterAnnotationsLineIndex) {
|
|
3071
3753
|
if (!state.chapter) return;
|
|
3072
|
-
|
|
3754
|
+
const chapterId = state.chapter.id;
|
|
3755
|
+
const line = Number.isInteger(lineIndex) && lineIndex >= 0 ? lineIndex + 1 : null;
|
|
3756
|
+
const query = line === null ? "" : `?line=${encodeURIComponent(line)}`;
|
|
3757
|
+
const annotations = await api(`/api/chapters/${encodeURIComponent(chapterId)}/annotations${query}`);
|
|
3758
|
+
if (String(state.chapter?.id ?? "") !== String(chapterId)) return;
|
|
3759
|
+
chapterAnnotationsLineIndex = line === null ? null : line - 1;
|
|
3760
|
+
chapterAnnotations = annotations;
|
|
3073
3761
|
renderChapterAnnotations();
|
|
3074
3762
|
}
|
|
3075
3763
|
|
|
3076
|
-
async function openChapterAnnotationsDialog() {
|
|
3764
|
+
async function openChapterAnnotationsDialog(lineIndex = null) {
|
|
3077
3765
|
if (!state.chapter) return;
|
|
3078
|
-
|
|
3766
|
+
chapterAnnotationsLineIndex = Number.isInteger(lineIndex) && lineIndex >= 0 ? lineIndex : null;
|
|
3767
|
+
const lineLabel = chapterAnnotationsLineIndex === null ? "" : `第 ${chapterAnnotationsLineIndex + 1} 行的`;
|
|
3768
|
+
$("#chapter-annotations-meta").textContent = `《${state.chapter.title}》${lineLabel}正文评论与待办`;
|
|
3079
3769
|
$("#chapter-annotations-list").innerHTML = '<p class="entity-history-empty">正在加载评论…</p>';
|
|
3080
3770
|
if (!$("#chapter-annotations-dialog").open) $("#chapter-annotations-dialog").showModal();
|
|
3081
3771
|
try {
|
|
3082
|
-
await loadChapterAnnotations();
|
|
3772
|
+
await loadChapterAnnotations(chapterAnnotationsLineIndex);
|
|
3083
3773
|
} catch (error) {
|
|
3084
3774
|
$("#chapter-annotations-dialog").close();
|
|
3085
3775
|
toast(error.message, "error");
|
|
@@ -3097,8 +3787,8 @@ function showLineCitationMenu(event, lineIndex) {
|
|
|
3097
3787
|
selectChapterLines(lineIndex, lineIndex);
|
|
3098
3788
|
}
|
|
3099
3789
|
const menu = $("#line-citation-menu");
|
|
3100
|
-
$("#add-line-annotation").classList.toggle("hidden", !
|
|
3101
|
-
$("#add-line-todo").classList.toggle("hidden", !
|
|
3790
|
+
$("#add-line-annotation").classList.toggle("hidden", !canWritePermissionModule(state.work, "comments"));
|
|
3791
|
+
$("#add-line-todo").classList.toggle("hidden", !canWritePermissionModule(state.work, "todos"));
|
|
3102
3792
|
const { start, end } = chapterLineSelection;
|
|
3103
3793
|
$("#line-citation-label").textContent = start === end ? `第 ${start + 1} 行` : `第 ${start + 1}-${end + 1} 行`;
|
|
3104
3794
|
menu.classList.remove("hidden");
|
|
@@ -3111,7 +3801,7 @@ function clearChapterLineSelection() {
|
|
|
3111
3801
|
chapterLineSelection = null;
|
|
3112
3802
|
$("#chapter-line-numbers-inner")?.querySelectorAll(".is-line-selected").forEach((row) => {
|
|
3113
3803
|
row.classList.remove("is-line-selected");
|
|
3114
|
-
row.setAttribute("aria-pressed", "false");
|
|
3804
|
+
row.querySelector(".chapter-line-number-select")?.setAttribute("aria-pressed", "false");
|
|
3115
3805
|
});
|
|
3116
3806
|
}
|
|
3117
3807
|
|
|
@@ -3361,6 +4051,7 @@ async function api(path, options = {}) {
|
|
|
3361
4051
|
const payload = await response.json();
|
|
3362
4052
|
if (path === "/api/health") {
|
|
3363
4053
|
applyImageUploadLimits(payload.data?.uploadLimits);
|
|
4054
|
+
applyAiChatTabLimit(payload.data?.aiChatTabLimit);
|
|
3364
4055
|
updateSystemHealth({
|
|
3365
4056
|
status: payload.data?.status === "ok" ? "ready" : "degraded",
|
|
3366
4057
|
version: payload.data?.version
|
|
@@ -3447,6 +4138,7 @@ function aiStreamInterruptionLabel(code) {
|
|
|
3447
4138
|
if (code === "AI_STREAM_NETWORK_ERROR") return "网络中断";
|
|
3448
4139
|
if (code === "AI_STREAM_UPSTREAM_CLOSED") return "流被关闭";
|
|
3449
4140
|
if (code === "AI_STREAM_REQUEST_CANCELLED") return "请求已取消";
|
|
4141
|
+
if (code === "AI_REQUEST_CANCELLED") return "已终止";
|
|
3450
4142
|
return "生成中断";
|
|
3451
4143
|
}
|
|
3452
4144
|
|
|
@@ -3578,8 +4270,9 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
3578
4270
|
|
|
3579
4271
|
function applyProductHealthMetadata(health) {
|
|
3580
4272
|
const version = String(health?.version ?? "").trim();
|
|
4273
|
+
const versionLabel = String(health?.versionLabel ?? "").trim();
|
|
3581
4274
|
document.querySelectorAll("[data-product-footer-version]").forEach((element) => {
|
|
3582
|
-
element.textContent = version ? `v${version}` : "v—";
|
|
4275
|
+
element.textContent = versionLabel || (version ? `v${version}` : "v—");
|
|
3583
4276
|
});
|
|
3584
4277
|
document.querySelectorAll("[data-product-footer-development]").forEach((element) => {
|
|
3585
4278
|
element.classList.toggle("hidden", health?.development !== true);
|
|
@@ -4286,7 +4979,6 @@ async function persistChapter({ automatic = false } = {}) {
|
|
|
4286
4979
|
const currentDraft = chapterDraftSnapshot();
|
|
4287
4980
|
if (sameChapterSnapshot(currentDraft, draft)) {
|
|
4288
4981
|
setSaveState(automatic ? "已自动保存" : collaborationAutoSaveDisabled ? "已保存 · 自动保存已关闭" : "已保存");
|
|
4289
|
-
if (!automatic) toast(`正文已保存为 v${state.chapter.versionNo}`);
|
|
4290
4982
|
} else {
|
|
4291
4983
|
scheduleChapterAutoSave(250);
|
|
4292
4984
|
}
|
|
@@ -5233,7 +5925,7 @@ function renderMemberPermissionGrid(value) {
|
|
|
5233
5925
|
<select data-member-permission="${esc(item.id)}" aria-label="${esc(item.label)}权限">
|
|
5234
5926
|
<option value="none" ${permissions[item.id] === "none" ? "selected" : ""}>无权限</option>
|
|
5235
5927
|
<option value="read" ${permissions[item.id] === "read" ? "selected" : ""}>只读</option>
|
|
5236
|
-
<option value="write" ${permissions[item.id] === "write" ? "selected" : ""}
|
|
5928
|
+
<option value="write" ${permissions[item.id] === "write" ? "selected" : ""}>${["comments", "todos"].includes(item.id) ? "可添加" : "可编辑"}</option>
|
|
5237
5929
|
</select>
|
|
5238
5930
|
</label>`).join("");
|
|
5239
5931
|
}
|
|
@@ -5359,12 +6051,29 @@ function syncGlobalReplaceScopeOptions() {
|
|
|
5359
6051
|
const selected = options.find((option) => option.checked && !option.disabled) ?? options.find((option) => !option.disabled);
|
|
5360
6052
|
options.forEach((option) => { option.checked = option === selected; });
|
|
5361
6053
|
const scope = selected?.value ?? "";
|
|
6054
|
+
const volumeSelect = $("#replace-volume");
|
|
6055
|
+
const volumeEnabled = scope === "prose" || scope === "prose-and-settings";
|
|
6056
|
+
volumeSelect.disabled = !volumeEnabled || !canEditProse();
|
|
6057
|
+
$("#replace-volume-description").textContent = scope === "settings"
|
|
6058
|
+
? "当前选择了设定库,分卷筛选不会生效。"
|
|
6059
|
+
: "选择分卷后,只替换该分卷的章节正文;设定库不按分卷筛选。";
|
|
5362
6060
|
$("#replace-submit").disabled = !scope || !canGlobalReplaceScope(scope);
|
|
5363
6061
|
$("#replace-permission-note").textContent = scope
|
|
5364
6062
|
? "替换完成后,命中的章节和设定会分别生成新的版本历史。"
|
|
5365
6063
|
: "当前账户没有可写入的正文或设定库权限。";
|
|
5366
6064
|
}
|
|
5367
6065
|
|
|
6066
|
+
function renderGlobalReplaceVolumeOptions() {
|
|
6067
|
+
const select = $("#replace-volume");
|
|
6068
|
+
const currentValue = select.value;
|
|
6069
|
+
const volumes = Array.isArray(state.work?.volumes) ? state.work.volumes : [];
|
|
6070
|
+
select.innerHTML = [
|
|
6071
|
+
'<option value="">全部分卷(整部作品正文)</option>',
|
|
6072
|
+
...volumes.map((volume) => `<option value="${esc(volume.id)}">${esc(volume.title)}</option>`)
|
|
6073
|
+
].join("");
|
|
6074
|
+
select.value = volumes.some((volume) => String(volume.id) === currentValue) ? currentValue : "";
|
|
6075
|
+
}
|
|
6076
|
+
|
|
5368
6077
|
function openGlobalReplaceDialog() {
|
|
5369
6078
|
if (!state.work) {
|
|
5370
6079
|
toast("请先打开一部作品", "error");
|
|
@@ -5376,6 +6085,7 @@ function openGlobalReplaceDialog() {
|
|
|
5376
6085
|
}
|
|
5377
6086
|
if ($("#search-dialog").open) $("#search-dialog").close();
|
|
5378
6087
|
$("#replace-form").reset();
|
|
6088
|
+
renderGlobalReplaceVolumeOptions();
|
|
5379
6089
|
syncGlobalReplaceScopeOptions();
|
|
5380
6090
|
$("#replace-dialog").showModal();
|
|
5381
6091
|
queueMicrotask(() => $("#replace-find").focus());
|
|
@@ -5463,6 +6173,7 @@ async function submitGlobalReplace(event) {
|
|
|
5463
6173
|
const find = $("#replace-find").value;
|
|
5464
6174
|
const replacement = $("#replace-with").value;
|
|
5465
6175
|
const scope = $("#replace-form").querySelector('input[name="replaceScope"]:checked')?.value ?? "prose";
|
|
6176
|
+
const volumeId = scope === "settings" ? null : ($("#replace-volume").value || null);
|
|
5466
6177
|
if (!find.trim()) {
|
|
5467
6178
|
toast("请输入要查找的内容", "error");
|
|
5468
6179
|
$("#replace-find").focus();
|
|
@@ -5485,8 +6196,14 @@ async function submitGlobalReplace(event) {
|
|
|
5485
6196
|
return;
|
|
5486
6197
|
}
|
|
5487
6198
|
const scopeLabel = globalReplaceScopeLabels[scope] ?? "正文";
|
|
6199
|
+
const selectedVolume = volumeId
|
|
6200
|
+
? state.work.volumes.find((volume) => String(volume.id) === volumeId)
|
|
6201
|
+
: null;
|
|
6202
|
+
const targetLabel = selectedVolume
|
|
6203
|
+
? `${scopeLabel}(正文仅限“${selectedVolume.title}”分卷${scope === "prose-and-settings" ? ",设定库仍为全部设定库" : ""})`
|
|
6204
|
+
: scopeLabel;
|
|
5488
6205
|
const replacementLabel = replacement ? `“${replacement}”` : "空内容";
|
|
5489
|
-
const confirmed = await confirmToast(`将把《${state.work.title}》的${
|
|
6206
|
+
const confirmed = await confirmToast(`将把《${state.work.title}》的${targetLabel}中所有“${find}”替换为${replacementLabel}。每个命中对象都会生成新版本,确认继续吗?`, {
|
|
5490
6207
|
title: "确认全局替换",
|
|
5491
6208
|
confirmLabel: "确认替换"
|
|
5492
6209
|
});
|
|
@@ -5511,7 +6228,7 @@ async function submitGlobalReplace(event) {
|
|
|
5511
6228
|
try {
|
|
5512
6229
|
const result = await api(`/api/works/${encodeURIComponent(workId)}/replace`, {
|
|
5513
6230
|
method: "POST",
|
|
5514
|
-
body: { find, replacement, scope },
|
|
6231
|
+
body: { find, replacement, scope, volumeId },
|
|
5515
6232
|
skipOptimisticVersion: true
|
|
5516
6233
|
});
|
|
5517
6234
|
$("#replace-dialog").close();
|
|
@@ -5806,6 +6523,7 @@ function resetWorkScopedUiCaches() {
|
|
|
5806
6523
|
state.characters = [];
|
|
5807
6524
|
state.settings = [];
|
|
5808
6525
|
state.races = [];
|
|
6526
|
+
state.organizations = [];
|
|
5809
6527
|
characterListPage = 1;
|
|
5810
6528
|
draftTypeFilter = "all";
|
|
5811
6529
|
draftBindingFilters = [];
|
|
@@ -5833,23 +6551,9 @@ function resetWorkScopedUiCaches() {
|
|
|
5833
6551
|
volumeChapterRequests.clear();
|
|
5834
6552
|
state.collapsedRaceIds.clear();
|
|
5835
6553
|
lastSavedChapterSnapshot = null;
|
|
5836
|
-
state.aiCitations = [];
|
|
5837
|
-
state.aiReferences = [];
|
|
5838
|
-
state.aiPromptSent = false;
|
|
5839
|
-
state.aiConversationId = null;
|
|
5840
|
-
state.aiConversationModelId = null;
|
|
5841
6554
|
state.aiConversations = [];
|
|
5842
|
-
|
|
5843
|
-
renderAiCitations();
|
|
5844
|
-
renderAiReferences();
|
|
5845
|
-
renderAiQuickActions();
|
|
5846
|
-
resetAiFeed();
|
|
5847
|
-
applyAiConversationTaskType("chat");
|
|
5848
|
-
applyAiConversationContextScope({ type: "none" });
|
|
5849
|
-
applyAiRoleplayCharacter(null);
|
|
5850
|
-
$("#ai-conversation-title").textContent = "新对话";
|
|
6555
|
+
resetAiChatTabs();
|
|
5851
6556
|
$("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
|
|
5852
|
-
resetAiContextMeter();
|
|
5853
6557
|
renderAiConversationHistory();
|
|
5854
6558
|
}
|
|
5855
6559
|
|
|
@@ -5994,7 +6698,7 @@ function renderTree() {
|
|
|
5994
6698
|
<div class="volume-node ${collapsed ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
|
|
5995
6699
|
<div class="volume-title">
|
|
5996
6700
|
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="左键展开或折叠;右键打开分卷详情;可将章节拖到这里追加"><span>${esc(volume.title)}</span><span>${Number(volume.chapterCount ?? chapters.length)} 章</span></button>
|
|
5997
|
-
${proseEditable ? `<button class="ghost-button volume-detail-button" type="button" data-volume-detail="${esc(volume.id)}" aria-label="打开“${esc(volume.title)}”分卷详情"
|
|
6701
|
+
${proseEditable ? `<button class="ghost-button volume-detail-button" type="button" data-volume-detail="${esc(volume.id)}" aria-label="打开“${esc(volume.title)}”分卷详情" title="打开“${esc(volume.title)}”分卷详情"><svg class="volume-detail-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5"></circle><path d="M12 10.5v5.5M12 7.5h.01"></path></svg></button>` : ""}
|
|
5998
6702
|
${proseEditable ? `<button class="add-button chapter-add-button" type="button" data-new-chapter-volume="${esc(volume.id)}" aria-label="在“${esc(volume.title)}”中新建章节" title="在“${esc(volume.title)}”中新建章节">+</button>` : ""}
|
|
5999
6703
|
</div>
|
|
6000
6704
|
<div class="volume-chapters">
|
|
@@ -6506,6 +7210,7 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
6506
7210
|
if (selectionRequestId === chapterSelectionRequestId) void loadChapterForeshadowReminders();
|
|
6507
7211
|
return false;
|
|
6508
7212
|
}
|
|
7213
|
+
resetChapterSearchPanel();
|
|
6509
7214
|
dismissDeleteToasts();
|
|
6510
7215
|
if (selectionGeneration !== chapterSelectionRequestGeneration || selectionRequestId !== chapterSelectionRequestId || state.work?.id !== workId) return false;
|
|
6511
7216
|
cancelChapterAutoSave();
|
|
@@ -6535,8 +7240,12 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
6535
7240
|
const normalizedContent = normalizeParagraphSpacing(state.chapter.content);
|
|
6536
7241
|
const spacingChanged = normalizedContent !== state.chapter.content;
|
|
6537
7242
|
$("#chapter-content").value = normalizedContent;
|
|
7243
|
+
chapterAnnotationCounts = new Map();
|
|
6538
7244
|
clearChapterLineSelection();
|
|
6539
7245
|
scheduleChapterLineNumbers();
|
|
7246
|
+
void loadChapterAnnotationCounts(state.chapter.id).catch((error) => {
|
|
7247
|
+
if (String(state.chapter?.id ?? "") === String(chapterId)) toast("正文评论数量读取失败,请稍后重试", "error");
|
|
7248
|
+
});
|
|
6540
7249
|
dismissChapterInsightToast();
|
|
6541
7250
|
updateChapterStats();
|
|
6542
7251
|
if (!canEditProse()) setSaveState("正文只读");
|
|
@@ -7021,22 +7730,15 @@ function handleReadingKeyboard(event) {
|
|
|
7021
7730
|
}
|
|
7022
7731
|
}
|
|
7023
7732
|
|
|
7024
|
-
function handleReadingWheel(event) {
|
|
7025
|
-
if (readingPreferences.mode !== "scroll" || readingLoading || Math.abs(event.deltaY) < 2) return;
|
|
7026
|
-
const viewport = $("#reader-viewport");
|
|
7027
|
-
const atEnd = viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - 2;
|
|
7028
|
-
const atStart = viewport.scrollTop <= 2;
|
|
7029
|
-
if (event.deltaY > 0 && atEnd) {
|
|
7030
|
-
event.preventDefault();
|
|
7031
|
-
void navigateReadingChapter(1, { continueFromBoundary: true });
|
|
7032
|
-
} else if (event.deltaY < 0 && atStart) {
|
|
7033
|
-
event.preventDefault();
|
|
7034
|
-
void navigateReadingChapter(-1, { continueFromBoundary: true });
|
|
7035
|
-
}
|
|
7036
|
-
}
|
|
7037
|
-
|
|
7038
7733
|
async function saveChapter() {
|
|
7039
|
-
|
|
7734
|
+
const dismissSavingToast = persistentToast("正在保存中");
|
|
7735
|
+
try {
|
|
7736
|
+
const saved = await persistChapter({ automatic: false });
|
|
7737
|
+
if (saved) toast(`保存成功(正文 v${saved.versionNo})`);
|
|
7738
|
+
return saved;
|
|
7739
|
+
} finally {
|
|
7740
|
+
dismissSavingToast();
|
|
7741
|
+
}
|
|
7040
7742
|
}
|
|
7041
7743
|
|
|
7042
7744
|
function tidyChapterBlankLines() {
|
|
@@ -7067,7 +7769,7 @@ function showWelcome(hasWork = false) {
|
|
|
7067
7769
|
}
|
|
7068
7770
|
|
|
7069
7771
|
function markActiveModule(module) {
|
|
7070
|
-
$("#module-nav").querySelectorAll("button").forEach((button) => button.classList.toggle("active", button.dataset.module === module));
|
|
7772
|
+
$("#module-nav").querySelectorAll("button").forEach((button) => button.classList.toggle("active", button.dataset.module === module || (button.id === "ai-assistant-entry" && aiConversationWorkspaceOpen)));
|
|
7071
7773
|
}
|
|
7072
7774
|
|
|
7073
7775
|
const moduleMeta = {
|
|
@@ -7079,7 +7781,7 @@ const moduleMeta = {
|
|
|
7079
7781
|
timeline: ["剧情脉络", "大事件时间轴", "候选事件经作者确认后,才进入正式时间线。", "新建事件"],
|
|
7080
7782
|
outlines: ["创作规划", "大纲/伏笔", "为每章维护目标、冲突与转折,并持续提醒尚未回收的伏笔。", "新建伏笔"],
|
|
7081
7783
|
relationships: ["跨章证据", "人物关系", "记录关系方向、阶段、置信度与原文依据。", "新建关系"],
|
|
7082
|
-
comments: ["正文协作", "
|
|
7784
|
+
comments: ["正文协作", "正文评论与待办", "集中查看并处理当前作品所有章节的评论与待办。", ""],
|
|
7083
7785
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
7084
7786
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
7085
7787
|
"ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词会追加在内置提示词和平台全局提示词之后;任务默认模型只作用于当前作品。", "保存设置"]
|
|
@@ -7865,7 +8567,10 @@ async function renderSettings(page = moduleListPages.settings) {
|
|
|
7865
8567
|
}
|
|
7866
8568
|
|
|
7867
8569
|
async function renderCharacters(page = characterListPage) {
|
|
7868
|
-
const hasCharacterFilters = characterFilters.raceIds.length > 0
|
|
8570
|
+
const hasCharacterFilters = characterFilters.raceIds.length > 0
|
|
8571
|
+
|| characterFilters.organizationIds.length > 0
|
|
8572
|
+
|| characterFilters.genderValues.length > 0
|
|
8573
|
+
|| characterFilters.deathState !== "all";
|
|
7869
8574
|
const pageSize = pageSizeFor("characters");
|
|
7870
8575
|
const [characterSource, races, organizations] = await Promise.all([
|
|
7871
8576
|
hasCharacterFilters
|
|
@@ -7893,6 +8598,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
7893
8598
|
<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}”`)}
|
|
7894
8599
|
<div class="character-card-heading"><h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
|
|
7895
8600
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
8601
|
+
<div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
|
|
7896
8602
|
${item.aliases.length ? `<div class="character-aliases"><b>别名</b>${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
|
|
7897
8603
|
${item.code ? `<div class="character-code"><b>编号</b><span class="pill">${esc(item.code)}</span></div>` : ""}
|
|
7898
8604
|
${item.species ? `<div class="character-species"><b>种族</b><span class="pill">${esc(racePathLabel(item.race) || item.species)}</span></div>` : ""}
|
|
@@ -7906,6 +8612,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
7906
8612
|
const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${characterStateFieldLabel(key)}:${value}`).join(" ") || "尚未记录当前状态");
|
|
7907
8613
|
const meta = [
|
|
7908
8614
|
item.code ? `编号 ${item.code}` : "",
|
|
8615
|
+
`性别 ${characterGenderLabel(item.gender)}`,
|
|
7909
8616
|
item.species ? (racePathLabel(item.race) || item.species) : "",
|
|
7910
8617
|
...(item.aliases ?? []).slice(0, 3),
|
|
7911
8618
|
(item.organizations ?? []).length ? (item.organizations ?? []).map((organization) => organization.name).join("、") : ""
|
|
@@ -7927,16 +8634,25 @@ async function renderCharacters(page = characterListPage) {
|
|
|
7927
8634
|
: "";
|
|
7928
8635
|
const selectedRaceIds = new Set(characterFilters.raceIds);
|
|
7929
8636
|
const selectedOrganizationIds = new Set(characterFilters.organizationIds);
|
|
8637
|
+
const selectedGenderValues = new Set(characterFilters.genderValues);
|
|
7930
8638
|
const selectedRaceNames = races.filter((race) => selectedRaceIds.has(String(race.id))).map((race) => racePathLabel(race) || race.name);
|
|
7931
8639
|
const selectedOrganizationNames = organizations.filter((organization) => selectedOrganizationIds.has(String(organization.id))).map((organization) => organization.name);
|
|
8640
|
+
const selectedGenderNames = CHARACTER_GENDER_OPTIONS
|
|
8641
|
+
.filter(([value]) => selectedGenderValues.has(value))
|
|
8642
|
+
.map(([, label]) => label);
|
|
8643
|
+
const selectedDeathStateLabel = CHARACTER_DEATH_STATE_OPTIONS.find(([value]) => value === characterFilters.deathState)?.[1] ?? "全部状态";
|
|
7932
8644
|
const filterOptionList = (items, selectedIds, valueKey = "id") => items.map((item) => {
|
|
7933
8645
|
const value = String(item[valueKey]);
|
|
7934
8646
|
const label = valueKey === "id" ? (racePathLabel(item) || item.name) : item.name;
|
|
7935
8647
|
return `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedIds.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`;
|
|
7936
8648
|
}).join("");
|
|
7937
|
-
const
|
|
8649
|
+
const genderFilterOptions = CHARACTER_GENDER_OPTIONS.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedGenderValues.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("");
|
|
8650
|
+
const deathStateFilterOptions = CHARACTER_DEATH_STATE_OPTIONS.map(([value, label]) => `<label class="character-filter-option"><input type="radio" name="character-death-state" value="${esc(value)}" ${characterFilters.deathState === value ? "checked" : ""}><span>${esc(label)}</span></label>`).join("");
|
|
8651
|
+
const filterToolbar = `<section id="character-filter-panel" class="character-filter-toolbar character-filter-toolbar-with-extra-filters${characterFiltersPanelOpen ? "" : " hidden"}" aria-label="角色筛选">
|
|
7938
8652
|
<details class="character-filter-dropdown"><summary><span>按种族筛选</span><strong>${selectedRaceNames.length ? `已选 ${selectedRaceNames.length} 项` : "全部种族"}</strong></summary><div id="character-race-filter" class="character-filter-options">${filterOptionList(orderRaceFilterOptions(races), selectedRaceIds)}</div></details>
|
|
7939
8653
|
<details class="character-filter-dropdown"><summary><span>按组织筛选</span><strong>${selectedOrganizationNames.length ? `已选 ${selectedOrganizationNames.length} 项` : "全部组织"}</strong></summary><div id="character-organization-filter" class="character-filter-options">${filterOptionList(organizations, selectedOrganizationIds, "id")}</div></details>
|
|
8654
|
+
<details class="character-filter-dropdown"><summary><span>按性别筛选</span><strong>${selectedGenderNames.length ? `已选 ${selectedGenderNames.length} 项` : "全部性别"}</strong></summary><div id="character-gender-filter" class="character-filter-options">${genderFilterOptions}</div></details>
|
|
8655
|
+
<details class="character-filter-dropdown"><summary><span>按死亡状态筛选</span><strong>${esc(selectedDeathStateLabel)}</strong></summary><div id="character-death-filter" class="character-filter-options">${deathStateFilterOptions}</div></details>
|
|
7940
8656
|
<div class="character-filter-toolbar-actions">${hasCharacterFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${characterPage.total} 个角色</span>` : ""}<button id="clear-character-filters" class="ghost-button" type="button" ${hasCharacterFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
7941
8657
|
</section>`;
|
|
7942
8658
|
mountCharacterFilterToggle();
|
|
@@ -7958,10 +8674,24 @@ async function renderCharacters(page = characterListPage) {
|
|
|
7958
8674
|
characterListPage = 1;
|
|
7959
8675
|
await renderCharacters(1);
|
|
7960
8676
|
});
|
|
8677
|
+
$("#character-gender-filter").addEventListener("change", async () => {
|
|
8678
|
+
characterFiltersPanelOpen = true;
|
|
8679
|
+
characterFilters.genderValues = readSelectedValues("#character-gender-filter");
|
|
8680
|
+
characterListPage = 1;
|
|
8681
|
+
await renderCharacters(1);
|
|
8682
|
+
});
|
|
8683
|
+
$("#character-death-filter").addEventListener("change", async (event) => {
|
|
8684
|
+
characterFiltersPanelOpen = true;
|
|
8685
|
+
characterFilters.deathState = CHARACTER_DEATH_STATE_OPTIONS.some(([value]) => value === event.target.value) ? event.target.value : "all";
|
|
8686
|
+
characterListPage = 1;
|
|
8687
|
+
await renderCharacters(1);
|
|
8688
|
+
});
|
|
7961
8689
|
$("#clear-character-filters")?.addEventListener("click", async () => {
|
|
7962
8690
|
characterFiltersPanelOpen = true;
|
|
7963
8691
|
characterFilters.raceIds = [];
|
|
7964
8692
|
characterFilters.organizationIds = [];
|
|
8693
|
+
characterFilters.genderValues = [];
|
|
8694
|
+
characterFilters.deathState = "all";
|
|
7965
8695
|
characterListPage = 1;
|
|
7966
8696
|
await renderCharacters(1);
|
|
7967
8697
|
});
|
|
@@ -8556,9 +9286,9 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
8556
9286
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
8557
9287
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
8558
9288
|
state.relationshipGraph = graph;
|
|
8559
|
-
const relationshipList = filteredRelationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${pageResult.items.map((item) => `
|
|
9289
|
+
const relationshipList = filteredRelationships.length ? `<div class="relationship-table-wrapper"><table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${pageResult.items.map((item) => `
|
|
8560
9290
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
8561
|
-
<td>${esc(relationshipCategoryLabel(item.category))} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(relationshipConfirmationLabel(item.confirmationStatus))}</td><td class="relationship-actions">${canEditRelationships ? `<button data-edit-relationship="${esc(item.id)}">编辑</button>` : ""}<button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : relationships.length ? '<div class="relationship-empty-note">没有符合当前筛选条件的关系。</div>' : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>';
|
|
9291
|
+
<td>${esc(relationshipCategoryLabel(item.category))} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(relationshipConfirmationLabel(item.confirmationStatus))}</td><td class="relationship-actions">${canEditRelationships ? `<button data-edit-relationship="${esc(item.id)}">编辑</button>` : ""}<button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table></div>` : relationships.length ? '<div class="relationship-empty-note">没有符合当前筛选条件的关系。</div>' : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>';
|
|
8562
9292
|
$("#module-content").innerHTML = `${filterToolbar}<div id="relationship-map-host"></div>${relationshipList}${renderModulePagination(pageResult, "relationships", "人物关系列表")}`;
|
|
8563
9293
|
bindModulePagination("relationships", renderRelationships);
|
|
8564
9294
|
const openGalaxy = () => {
|
|
@@ -8566,6 +9296,8 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
8566
9296
|
const galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, {
|
|
8567
9297
|
workId: state.work.id,
|
|
8568
9298
|
frameRate: state.uiSettings.galaxyFrameRate,
|
|
9299
|
+
motionMode: storedGalaxyMotionMode(),
|
|
9300
|
+
onMotionModeChange: persistGalaxyMotionMode,
|
|
8569
9301
|
onClose: () => {
|
|
8570
9302
|
if (state.galaxy === galaxy) state.galaxy = null;
|
|
8571
9303
|
}
|
|
@@ -8619,8 +9351,8 @@ async function renderWorkChapterComments(page = moduleListPages.comments) {
|
|
|
8619
9351
|
moduleListPages.comments = pageResult.page;
|
|
8620
9352
|
mountModuleCount(total);
|
|
8621
9353
|
$("#module-content").innerHTML = result.items.length
|
|
8622
|
-
? `<div class="chapter-comment-module-list">${result.items.map((annotation) => chapterAnnotationCard(annotation, { showSource: true })).join("")}</div>${renderModulePagination(pageResult, "comments", "
|
|
8623
|
-
: emptyModule("
|
|
9354
|
+
? `<div class="chapter-comment-module-list">${result.items.map((annotation) => chapterAnnotationCard(annotation, { showSource: true })).join("")}</div>${renderModulePagination(pageResult, "comments", "正文评论与待办列表")}`
|
|
9355
|
+
: emptyModule("还没有正文评论或待办", "在任一正文行上点击右键,即可添加评论或待办。");
|
|
8624
9356
|
bindModulePagination("comments", renderWorkChapterComments);
|
|
8625
9357
|
bindChapterAnnotationCards($("#module-content"), result.items, {
|
|
8626
9358
|
refresh: () => renderWorkChapterComments(pageResult.page),
|
|
@@ -8742,7 +9474,7 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
8742
9474
|
moduleApiPage("tasks", `/api/works/${state.work.id}/tasks`, page, pageSize, { refresh }),
|
|
8743
9475
|
canReadModule("ai-settings")
|
|
8744
9476
|
? moduleApi("tasks", `/api/works/${state.work.id}/ai-settings`, { refresh })
|
|
8745
|
-
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunPaused: false })
|
|
9477
|
+
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunStabilityDelayMinutes: 2, autoRunPaused: false })
|
|
8746
9478
|
]);
|
|
8747
9479
|
if (!taskPage.items.length && page > 1) return renderTasks(page - 1, { refresh });
|
|
8748
9480
|
taskListPage = taskPage.page;
|
|
@@ -8779,6 +9511,7 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
8779
9511
|
<div class="task-auto-run-copy">
|
|
8780
9512
|
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
8781
9513
|
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
9514
|
+
<small>正文停止编辑后,等待稳定窗口结束才会创建章节理解任务。</small>
|
|
8782
9515
|
<small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
|
|
8783
9516
|
</div>
|
|
8784
9517
|
<div class="task-auto-run-actions">
|
|
@@ -8790,11 +9523,13 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
8790
9523
|
<label>同时运行上限<input id="task-auto-run-concurrency" type="number" min="1" max="8" value="${esc(String(settings.autoRunConcurrency ?? 2))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
|
|
8791
9524
|
<label>每日任务上限<input id="task-auto-run-daily-limit" type="number" min="0" max="10000" value="${esc(String(settings.autoRunDailyTaskLimit ?? 0))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}" aria-describedby="task-auto-run-daily-help"></label>
|
|
8792
9525
|
<label>连续失败暂停阈值<input id="task-auto-run-failure-threshold" type="number" min="1" max="10" value="${esc(String(settings.autoRunFailureThreshold ?? 3))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}"></label>
|
|
9526
|
+
<label>停止编辑后创建任务<input id="task-auto-run-stability-delay" type="number" min="1" max="120" value="${esc(String(settings.autoRunStabilityDelayMinutes ?? 2))}" ${autoRunEditing ? "" : "disabled"} aria-readonly="${String(!autoRunEditing)}" aria-describedby="task-auto-run-stability-help"></label>
|
|
8793
9527
|
${autoRunEditing
|
|
8794
9528
|
? `<button id="task-auto-run-save" class="primary-button" type="button">保存并生效</button><button id="task-auto-run-pause" class="ghost-button" type="button">${autoRunPauseLabel}</button>`
|
|
8795
9529
|
: ""}
|
|
8796
9530
|
</div>
|
|
8797
9531
|
<p id="task-auto-run-daily-help" class="task-auto-run-help">每日任务上限填 0 表示不限制;达到上限后会在下一个 UTC 自然日自动恢复。</p>
|
|
9532
|
+
<p id="task-auto-run-stability-help" class="task-auto-run-help">正文连续 ${esc(String(settings.autoRunStabilityDelayMinutes ?? 2))} 分钟没有编辑,才会创建章节理解任务;最低 1 分钟。</p>
|
|
8798
9533
|
<p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个 · ${autoRunPaused ? "已暂停" : autoRunActive ? "持续执行中" : "未开启"}</p>
|
|
8799
9534
|
${autoRunPaused ? `<p class="task-auto-run-alert" role="status"><strong>自动执行已暂停</strong><span>${esc(settings.autoRunPauseReason || "需要人工确认后恢复")}</span>${settings.autoRunResumeAt ? `<small>预计 ${esc(formatDateTime(settings.autoRunResumeAt))} 自动恢复</small>` : ""}</p>` : ""}
|
|
8800
9535
|
<div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
|
|
@@ -8848,7 +9583,8 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
8848
9583
|
autoRunEnabled: $("#task-auto-run-enabled").checked,
|
|
8849
9584
|
autoRunConcurrency: Number($("#task-auto-run-concurrency").value),
|
|
8850
9585
|
autoRunDailyTaskLimit: Number($("#task-auto-run-daily-limit").value),
|
|
8851
|
-
autoRunFailureThreshold: Number($("#task-auto-run-failure-threshold").value)
|
|
9586
|
+
autoRunFailureThreshold: Number($("#task-auto-run-failure-threshold").value),
|
|
9587
|
+
autoRunStabilityDelayMinutes: Number($("#task-auto-run-stability-delay").value)
|
|
8852
9588
|
}
|
|
8853
9589
|
});
|
|
8854
9590
|
toast(updated.autoRunEnabled
|
|
@@ -9771,7 +10507,7 @@ function renderProviderCards(providers, models) {
|
|
|
9771
10507
|
: "";
|
|
9772
10508
|
return `
|
|
9773
10509
|
<article class="record-card provider-card ${provider.status === "disabled" ? "is-disabled" : ""}"><div class="provider-card-meta"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><span class="provider-status-badge ${providerStatusClass}">${esc(providerStatusLabel(provider.status))}</span></div><h3>${esc(provider.name)}</h3>
|
|
9774
|
-
${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
10510
|
+
${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
9775
10511
|
<div class="provider-models">${providerModels.map((model) => {
|
|
9776
10512
|
const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
|
|
9777
10513
|
const modelStatus = !model.enabled
|
|
@@ -9781,7 +10517,8 @@ function renderProviderCards(providers, models) {
|
|
|
9781
10517
|
: "";
|
|
9782
10518
|
const capability = model.multimodalEnabled ? " · 多模态" : "";
|
|
9783
10519
|
const defaultBadge = model.imageToolDefault ? " · 默认读图模型" : "";
|
|
9784
|
-
|
|
10520
|
+
const thinkingEffortLabel = MODEL_THINKING_EFFORT_OPTIONS.find(([value]) => value === model.thinkingEffort)?.[1] ?? "模型默认";
|
|
10521
|
+
return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"}${capability}${defaultBadge} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 思考强度 ${esc(thinkingEffortLabel)} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}</div>`;
|
|
9785
10522
|
}).join("")}</div>
|
|
9786
10523
|
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button>${provider.status === "enabled" ? `<button data-test-provider="${esc(provider.id)}" ${providerModels.length ? "" : "disabled aria-disabled=\"true\" title=\"请先添加模型\""}>测试连接</button>` : ""}<button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`;
|
|
9787
10524
|
}).join("")}</div>`
|
|
@@ -10106,7 +10843,12 @@ async function refreshBackgroundTaskCenter({ announce = true } = {}) {
|
|
|
10106
10843
|
backgroundTaskCenterTaskSnapshots = transitionResult.snapshots;
|
|
10107
10844
|
backgroundTaskCenterSnapshot.taskPage = taskResult.value;
|
|
10108
10845
|
if (backgroundTaskCenterTasksInitialized && announce && state.module !== "tasks") {
|
|
10109
|
-
|
|
10846
|
+
const announcementResult = filterBackgroundTaskTransitionsForAnnouncement(
|
|
10847
|
+
transitionResult.transitions,
|
|
10848
|
+
backgroundTaskCenterExpiredNoticeTimes
|
|
10849
|
+
);
|
|
10850
|
+
backgroundTaskCenterExpiredNoticeTimes = announcementResult.noticeTimes;
|
|
10851
|
+
for (const transition of announcementResult.transitions) {
|
|
10110
10852
|
const notification = backgroundTaskTransitionMessage(transition);
|
|
10111
10853
|
toast(notification.message, notification.type);
|
|
10112
10854
|
}
|
|
@@ -10134,6 +10876,7 @@ function stopBackgroundTaskCenter() {
|
|
|
10134
10876
|
backgroundTaskCenterWorkId = null;
|
|
10135
10877
|
backgroundTaskCenterTasksInitialized = false;
|
|
10136
10878
|
backgroundTaskCenterTaskSnapshots = new Map();
|
|
10879
|
+
backgroundTaskCenterExpiredNoticeTimes = new Map();
|
|
10137
10880
|
backgroundTaskCenterSnapshot = { taskPage: null, relationshipIndex: null, errors: {} };
|
|
10138
10881
|
const dialog = $("#background-task-dialog");
|
|
10139
10882
|
if (dialog?.open) dialog.close();
|
|
@@ -10347,7 +11090,7 @@ async function renderBookAiSettings() {
|
|
|
10347
11090
|
const host = $("#module-content");
|
|
10348
11091
|
const workId = String(state.work.id);
|
|
10349
11092
|
const maximumAgentToolCallLimit = Math.max(5, Number(settings.agentToolCallLimitMaximum) || 80);
|
|
10350
|
-
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image"]);
|
|
11093
|
+
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts", "image", "calculate_time"]);
|
|
10351
11094
|
const dailyTokenQuota = settings.dailyTokenQuota === null ? null : Number(settings.dailyTokenQuota);
|
|
10352
11095
|
const quotaUsedTokens = Number(usage?.quota?.usedTokens) || 0;
|
|
10353
11096
|
const quotaRemainingTokens = usage?.quota?.remainingTokens === null
|
|
@@ -10380,7 +11123,7 @@ async function renderBookAiSettings() {
|
|
|
10380
11123
|
);
|
|
10381
11124
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
10382
11125
|
"beforeend",
|
|
10383
|
-
`<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>`
|
|
11126
|
+
`<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>计算日期差值,或从起始日期推算目标日期,不读取作品内容。</small></span></label>`
|
|
10384
11127
|
);
|
|
10385
11128
|
if (!canEditModule("ai-settings")) {
|
|
10386
11129
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -10772,6 +11515,18 @@ function resetAiContextMeter() {
|
|
|
10772
11515
|
setAiContextMeter(null);
|
|
10773
11516
|
}
|
|
10774
11517
|
|
|
11518
|
+
function setAiChatTabContextUsage(tab, usage, allowShrink = true) {
|
|
11519
|
+
if (!tab) return;
|
|
11520
|
+
if (isActiveAiChatTab(tab)) {
|
|
11521
|
+
setAiContextMeter(usage, allowShrink);
|
|
11522
|
+
tab.contextUsage = latestAiContextUsage;
|
|
11523
|
+
return;
|
|
11524
|
+
}
|
|
11525
|
+
tab.contextUsage = allowShrink
|
|
11526
|
+
? resolveAiContextUsage(tab.contextUsage, usage)
|
|
11527
|
+
: mergeAiContextUsage(tab.contextUsage, usage, false);
|
|
11528
|
+
}
|
|
11529
|
+
|
|
10775
11530
|
function setAiContextDistributionVisible(visible) {
|
|
10776
11531
|
const meter = $("#ai-context-meter");
|
|
10777
11532
|
const popover = $("#ai-context-popover");
|
|
@@ -10790,6 +11545,14 @@ function hideAiContextWarning() {
|
|
|
10790
11545
|
$("#ai-context-warning").classList.add("hidden");
|
|
10791
11546
|
}
|
|
10792
11547
|
|
|
11548
|
+
function setAiChatTabContextWarning(tab, visible) {
|
|
11549
|
+
if (!tab) return;
|
|
11550
|
+
tab.contextWarning = visible;
|
|
11551
|
+
if (!isActiveAiChatTab(tab)) return;
|
|
11552
|
+
if (visible) showAiContextWarning();
|
|
11553
|
+
else hideAiContextWarning();
|
|
11554
|
+
}
|
|
11555
|
+
|
|
10793
11556
|
function setAiContextWarningActionsDisabled(disabled) {
|
|
10794
11557
|
for (const button of $("#ai-context-warning").querySelectorAll("button")) button.disabled = disabled;
|
|
10795
11558
|
}
|
|
@@ -10798,13 +11561,17 @@ async function loadAiReferences() {
|
|
|
10798
11561
|
const workId = state.work?.id;
|
|
10799
11562
|
if (!workId) return;
|
|
10800
11563
|
const generation = workScopedUiGeneration;
|
|
10801
|
-
const [characters, settings] = await Promise.all([
|
|
11564
|
+
const [characters, settings, races, organizations] = await Promise.all([
|
|
10802
11565
|
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
10803
|
-
canReadModule("settings") ? api(`/api/works/${workId}/settings/context`) : Promise.resolve([])
|
|
11566
|
+
canReadModule("settings") ? api(`/api/works/${workId}/settings/context`) : Promise.resolve([]),
|
|
11567
|
+
canReadModule("races") ? api(`/api/works/${workId}/races`) : Promise.resolve([]),
|
|
11568
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${workId}/organizations`) : Promise.resolve([])
|
|
10804
11569
|
]);
|
|
10805
11570
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
10806
11571
|
state.characters = characters;
|
|
10807
11572
|
state.settings = settings;
|
|
11573
|
+
state.races = races;
|
|
11574
|
+
state.organizations = organizations;
|
|
10808
11575
|
renderAiRoleplayCharacterSelect();
|
|
10809
11576
|
loadedAiReferencesWorkId = workId;
|
|
10810
11577
|
}
|
|
@@ -11640,8 +12407,11 @@ function renderCharacterEditorRelationships() {
|
|
|
11640
12407
|
const category = relationshipCategoryLabel(relationship.category);
|
|
11641
12408
|
const relationLabel = [category, relationship.subtype].filter(Boolean).join(" · ") || "未细分";
|
|
11642
12409
|
const keywords = Array.isArray(relationship.keywords) ? relationship.keywords : [];
|
|
12410
|
+
const actions = !entityEditorReadOnly && canEditModule("relationships")
|
|
12411
|
+
? `<div class="character-relationship-actions"><button type="button" data-character-relationship-edit="${esc(relationship.id)}">编辑关系</button><button class="danger-button" type="button" data-character-relationship-delete="${esc(relationship.id)}">删除关系</button></div>`
|
|
12412
|
+
: "";
|
|
11643
12413
|
return `<article class="character-relationship-row">
|
|
11644
|
-
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${
|
|
12414
|
+
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${actions}</div>
|
|
11645
12415
|
<div class="character-relationship-keywords"><small>关系关键词</small><div>${keywords.map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || '<span class="character-relationship-empty-keywords">未填写关键词</span>'}</div></div>
|
|
11646
12416
|
</article>`;
|
|
11647
12417
|
}).join("");
|
|
@@ -11650,6 +12420,14 @@ function renderCharacterEditorRelationships() {
|
|
|
11650
12420
|
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipEdit);
|
|
11651
12421
|
if (relationship) void openRelationshipDialog(relationship, { characterId });
|
|
11652
12422
|
}));
|
|
12423
|
+
host.querySelectorAll("[data-character-relationship-delete]").forEach((button) => button.addEventListener("click", () => {
|
|
12424
|
+
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipDelete);
|
|
12425
|
+
if (!relationship) return;
|
|
12426
|
+
button.disabled = true;
|
|
12427
|
+
void deleteRelationshipRecord(relationship, characterId).finally(() => {
|
|
12428
|
+
if (button.isConnected) button.disabled = false;
|
|
12429
|
+
});
|
|
12430
|
+
}));
|
|
11653
12431
|
host.querySelector("[data-character-relationship-create]")?.addEventListener("click", () => void openRelationshipDialog(null, { characterId }));
|
|
11654
12432
|
}
|
|
11655
12433
|
|
|
@@ -11668,7 +12446,7 @@ async function loadCharacterEditorRelationships(characterId, { refresh = false }
|
|
|
11668
12446
|
]);
|
|
11669
12447
|
if (state.work?.id !== workId || characterEditorItem?.id !== characterId) return;
|
|
11670
12448
|
state.characters = characters;
|
|
11671
|
-
characterEditorRelationships = relationships.filter((relationship) => relationship.fromCharacterId === characterId || relationship.toCharacterId === characterId);
|
|
12449
|
+
characterEditorRelationships = sortCharacterRelationships(relationships.filter((relationship) => relationship.fromCharacterId === characterId || relationship.toCharacterId === characterId), characterId);
|
|
11672
12450
|
characterEditorRelationshipsLoaded = true;
|
|
11673
12451
|
loaded = true;
|
|
11674
12452
|
} catch (error) {
|
|
@@ -11692,6 +12470,21 @@ async function refreshRelationshipSurfaces(characterId = null) {
|
|
|
11692
12470
|
await Promise.all(tasks);
|
|
11693
12471
|
}
|
|
11694
12472
|
|
|
12473
|
+
async function deleteRelationshipRecord(item, characterId = null) {
|
|
12474
|
+
const relationshipName = `${state.characters.find((character) => character.id === item.fromCharacterId)?.name ?? "未知角色"} / ${state.characters.find((character) => character.id === item.toCharacterId)?.name ?? "未知角色"}`;
|
|
12475
|
+
if (!await confirmToast(`确认删除人物关系“${relationshipName}”吗?`, { title: "删除人物关系", confirmLabel: "继续删除" })) return false;
|
|
12476
|
+
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return false;
|
|
12477
|
+
try {
|
|
12478
|
+
await api(`/api/relationships/${item.id}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
12479
|
+
await Promise.all([refreshRelationshipSurfaces(characterId), loadAiReferences()]);
|
|
12480
|
+
deleteToast(`已删除人物关系“${relationshipName}”`);
|
|
12481
|
+
return true;
|
|
12482
|
+
} catch (error) {
|
|
12483
|
+
toast(error.message, "error");
|
|
12484
|
+
return false;
|
|
12485
|
+
}
|
|
12486
|
+
}
|
|
12487
|
+
|
|
11695
12488
|
const characterSectionTypeLabels = {
|
|
11696
12489
|
overview: "基本档案",
|
|
11697
12490
|
appearance: "外貌与生理",
|
|
@@ -12426,6 +13219,7 @@ function renderCharacterEditorFields(item) {
|
|
|
12426
13219
|
$("#character-editor-fields").innerHTML = [
|
|
12427
13220
|
characterEditorSection("basic", "基础资料", "用于检索、去重和建立人物在作品中的基本归属。",
|
|
12428
13221
|
field("name", "标准名", "text", item?.name) +
|
|
13222
|
+
field("gender", "性别", "select", item?.gender ?? "unknown", CHARACTER_GENDER_OPTIONS) +
|
|
12429
13223
|
field("isDead", "标记为已死亡", "checkbox", item?.isDead ?? false) +
|
|
12430
13224
|
field("aliases", "别名", "item-list", item?.aliases ?? []) +
|
|
12431
13225
|
(!canReadModule("races")
|
|
@@ -12479,6 +13273,7 @@ function collectCharacterBody(form) {
|
|
|
12479
13273
|
delete profile.sections;
|
|
12480
13274
|
const body = {
|
|
12481
13275
|
name: String(form.get("name") ?? "").trim(),
|
|
13276
|
+
gender: String(form.get("gender") ?? "unknown"),
|
|
12482
13277
|
isDead: form.has("isDead"),
|
|
12483
13278
|
code: String(form.get("code") ?? "").trim(),
|
|
12484
13279
|
aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
|
|
@@ -13083,7 +13878,6 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
13083
13878
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
13084
13879
|
const defaultFrom = options.characterId && state.characters.some((character) => character.id === options.characterId) ? options.characterId : characterOptions[0][0];
|
|
13085
13880
|
const defaultTo = characterOptions.find(([id]) => id !== defaultFrom)?.[0] ?? characterOptions[1][0];
|
|
13086
|
-
const relationshipName = item ? `${state.characters.find((character) => character.id === item.fromCharacterId)?.name ?? "未知角色"} / ${state.characters.find((character) => character.id === item.toCharacterId)?.name ?? "未知角色"}` : "";
|
|
13087
13881
|
const management = item && canEditModule("relationships") ? `<section class="entity-dialog-management" aria-label="人物关系操作">
|
|
13088
13882
|
<div><strong>关系操作</strong><small>删除操作会记录到版本历史和审计日志。</small></div>
|
|
13089
13883
|
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-relationship-delete>删除关系</button></div>
|
|
@@ -13101,16 +13895,7 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
13101
13895
|
dialog.showModal();
|
|
13102
13896
|
};
|
|
13103
13897
|
dialog.close();
|
|
13104
|
-
if (!await
|
|
13105
|
-
if (!await confirmToast(`删除人物关系“${relationshipName}”后无法恢复。`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return reopenDialog();
|
|
13106
|
-
try {
|
|
13107
|
-
await api(`/api/relationships/${item.id}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
13108
|
-
await Promise.all([refreshRelationshipSurfaces(options.characterId ?? null), loadAiReferences()]);
|
|
13109
|
-
deleteToast(`已删除人物关系“${relationshipName}”`);
|
|
13110
|
-
} catch (error) {
|
|
13111
|
-
reopenDialog();
|
|
13112
|
-
toast(error.message, "error");
|
|
13113
|
-
}
|
|
13898
|
+
if (!await deleteRelationshipRecord(item, options.characterId ?? null)) return reopenDialog();
|
|
13114
13899
|
});
|
|
13115
13900
|
}
|
|
13116
13901
|
|
|
@@ -13161,17 +13946,23 @@ async function openTaskDialog() {
|
|
|
13161
13946
|
group.push(item);
|
|
13162
13947
|
groups.set(groupKey, { id: groupId, title: groupTitle, options: group });
|
|
13163
13948
|
return groups;
|
|
13164
|
-
}, new Map())].map(([, group]) =>
|
|
13949
|
+
}, new Map())].map(([, group]) => {
|
|
13950
|
+
const groupOptionsId = group.id ? `task-scope-volume-options-${group.id}` : "task-scope-volume-options-unassigned";
|
|
13951
|
+
return `<section class="task-scope-volume-group" data-task-scope-group data-task-scope-volume-id="${esc(group.id)}">
|
|
13165
13952
|
<header>
|
|
13166
13953
|
${group.id ? `<label class="task-scope-volume-option">
|
|
13167
13954
|
<input type="checkbox" data-task-scope-volume-input="${esc(group.id)}" data-task-scope-volume-title="${esc(group.title)}" aria-label="全选${esc(group.title)}章节">
|
|
13168
13955
|
<span class="task-scope-checkbox" aria-hidden="true"></span>
|
|
13169
13956
|
<span class="task-scope-volume-copy"><strong>${esc(group.title)}</strong><small>勾选以全选本卷章节</small></span>
|
|
13170
13957
|
</label>` : `<strong>${esc(group.title)}</strong>`}
|
|
13171
|
-
<span>${group.options.length} 章</span>
|
|
13958
|
+
<span class="task-scope-volume-count">${group.options.length} 章</span>
|
|
13959
|
+
${group.id ? `<button type="button" class="task-scope-volume-collapse" data-task-scope-volume-toggle="${esc(group.id)}" data-task-scope-volume-title="${esc(group.title)}" aria-expanded="true" aria-controls="${esc(groupOptionsId)}" aria-label="折叠${esc(group.title)}章节"><span aria-hidden="true">⌃</span></button>` : ""}
|
|
13172
13960
|
</header>
|
|
13173
|
-
${
|
|
13174
|
-
|
|
13961
|
+
<div id="${esc(groupOptionsId)}" class="task-scope-volume-options">
|
|
13962
|
+
${group.options.map(renderOption).join("")}
|
|
13963
|
+
</div>
|
|
13964
|
+
</section>`;
|
|
13965
|
+
}).join("");
|
|
13175
13966
|
return `<div class="form-field task-scope-field task-scope-${kind}-field is-disabled" data-task-scope-field="${kind}" aria-disabled="true">
|
|
13176
13967
|
<div class="task-scope-field-heading"><span id="task-${kind}-label">${esc(label)}(可多选)</span><small>从左侧选择,右侧确认已选内容</small></div>
|
|
13177
13968
|
<div class="task-scope-picker">
|
|
@@ -13262,7 +14053,8 @@ async function openTaskDialog() {
|
|
|
13262
14053
|
const buildRelationshipScope = (form) => {
|
|
13263
14054
|
const taskType = String(form.get("taskType"));
|
|
13264
14055
|
const scopeType = String(form.get("scopeType"));
|
|
13265
|
-
const includeAllSettings = taskType === "relationship-analysis"
|
|
14056
|
+
const includeAllSettings = taskType === "relationship-analysis"
|
|
14057
|
+
&& ["chapter-with-settings", "book-with-settings"].includes(scopeType);
|
|
13266
14058
|
const settingsOnly = taskType === "relationship-analysis" && scopeType === "settings";
|
|
13267
14059
|
const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
|
|
13268
14060
|
const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
|
|
@@ -13273,11 +14065,12 @@ async function openTaskDialog() {
|
|
|
13273
14065
|
const chapterScope = {
|
|
13274
14066
|
type: "chapter",
|
|
13275
14067
|
...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
|
|
13276
|
-
...(chapterIds.length ? { chapterIds } : {})
|
|
14068
|
+
...(chapterIds.length ? { chapterIds } : {}),
|
|
14069
|
+
...(scopeType === "chapter-with-settings" ? { includeAllSettings: true } : {})
|
|
13277
14070
|
};
|
|
13278
14071
|
const scope = settingsOnly
|
|
13279
14072
|
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13280
|
-
: scopeType === "book" ||
|
|
14073
|
+
: scopeType === "book" || scopeType === "book-with-settings"
|
|
13281
14074
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13282
14075
|
: { ...chapterScope, ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
13283
14076
|
return scope;
|
|
@@ -13318,7 +14111,7 @@ async function openTaskDialog() {
|
|
|
13318
14111
|
}
|
|
13319
14112
|
taskListPage = 1;
|
|
13320
14113
|
toast("分析任务已创建,已进入任务队列");
|
|
13321
|
-
|
|
14114
|
+
await refreshAnalysisTaskViewsAfterCreate(workId);
|
|
13322
14115
|
}, "AI 分析", {
|
|
13323
14116
|
large: true,
|
|
13324
14117
|
submitLabel: "创建任务",
|
|
@@ -13347,6 +14140,9 @@ async function openTaskDialog() {
|
|
|
13347
14140
|
const scopeVolumeInputs = [
|
|
13348
14141
|
...$("#dialog-fields").querySelectorAll("[data-task-scope-volume-input]")
|
|
13349
14142
|
];
|
|
14143
|
+
const scopeVolumeCollapseButtons = [
|
|
14144
|
+
...$("#dialog-fields").querySelectorAll("[data-task-scope-volume-toggle]")
|
|
14145
|
+
];
|
|
13350
14146
|
const scopeSummaries = {
|
|
13351
14147
|
chapter: $("#dialog-fields").querySelector("#task-chapter-summary")
|
|
13352
14148
|
};
|
|
@@ -13393,6 +14189,9 @@ async function openTaskDialog() {
|
|
|
13393
14189
|
const allSettingsOption = document.createElement("option");
|
|
13394
14190
|
allSettingsOption.value = "book-with-settings";
|
|
13395
14191
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
14192
|
+
const chapterSettingsOption = document.createElement("option");
|
|
14193
|
+
chapterSettingsOption.value = "chapter-with-settings";
|
|
14194
|
+
chapterSettingsOption.textContent = "指定章节 + 设定集";
|
|
13396
14195
|
const settingsOnlyOption = document.createElement("option");
|
|
13397
14196
|
settingsOnlyOption.value = "settings";
|
|
13398
14197
|
settingsOnlyOption.textContent = "仅设定集";
|
|
@@ -13405,6 +14204,13 @@ async function openTaskDialog() {
|
|
|
13405
14204
|
scopeSearches[kind].focus({ preventScroll: true });
|
|
13406
14205
|
}
|
|
13407
14206
|
};
|
|
14207
|
+
const setTaskScopeVolumeCollapsed = (button, collapsed) => {
|
|
14208
|
+
const group = button.closest("[data-task-scope-group]");
|
|
14209
|
+
if (!group) return;
|
|
14210
|
+
group.classList.toggle("is-collapsed", collapsed);
|
|
14211
|
+
button.setAttribute("aria-expanded", String(!collapsed));
|
|
14212
|
+
button.setAttribute("aria-label", `${collapsed ? "展开" : "折叠"}${button.dataset.taskScopeVolumeTitle ?? ""}章节`);
|
|
14213
|
+
};
|
|
13408
14214
|
const syncTaskScopeVolumeInputs = () => {
|
|
13409
14215
|
for (const volumeInput of scopeVolumeInputs) {
|
|
13410
14216
|
const group = volumeInput.closest("[data-task-scope-group]");
|
|
@@ -13442,7 +14248,12 @@ async function openTaskDialog() {
|
|
|
13442
14248
|
if (visible) visibleCount += 1;
|
|
13443
14249
|
}
|
|
13444
14250
|
scopeFields[kind].querySelectorAll("[data-task-scope-group]").forEach((group) => {
|
|
13445
|
-
|
|
14251
|
+
const hasVisibleOptions = Boolean(group.querySelector(".task-scope-option:not(.hidden)"));
|
|
14252
|
+
group.classList.toggle("hidden", !hasVisibleOptions);
|
|
14253
|
+
if (query && hasVisibleOptions) {
|
|
14254
|
+
const collapseButton = group.querySelector("[data-task-scope-volume-toggle]");
|
|
14255
|
+
if (collapseButton) setTaskScopeVolumeCollapsed(collapseButton, false);
|
|
14256
|
+
}
|
|
13446
14257
|
});
|
|
13447
14258
|
scopeAvailableCounts[kind].textContent = query
|
|
13448
14259
|
? `${visibleCount} / ${scopeInputs[kind].length} 章`
|
|
@@ -13451,7 +14262,7 @@ async function openTaskDialog() {
|
|
|
13451
14262
|
};
|
|
13452
14263
|
const syncChapterField = () => {
|
|
13453
14264
|
const kind = "chapter";
|
|
13454
|
-
const enabled =
|
|
14265
|
+
const enabled = ["chapter", "chapter-with-settings"].includes(scopeTypeSelect.value);
|
|
13455
14266
|
scopeFields[kind].classList.toggle("is-disabled", !enabled);
|
|
13456
14267
|
scopeFields[kind].classList.toggle("hidden", !enabled);
|
|
13457
14268
|
scopeFields[kind].setAttribute("aria-disabled", String(!enabled));
|
|
@@ -13525,8 +14336,14 @@ async function openTaskDialog() {
|
|
|
13525
14336
|
};
|
|
13526
14337
|
const syncRelationshipOptions = () => {
|
|
13527
14338
|
const enabled = taskTypeSelect.value === "relationship-analysis";
|
|
14339
|
+
const bookScopeOption = scopeTypeSelect.querySelector('option[value="book"]');
|
|
14340
|
+
if (enabled && !chapterSettingsOption.isConnected) bookScopeOption.before(chapterSettingsOption);
|
|
13528
14341
|
if (enabled && !allSettingsOption.isConnected) scopeTypeSelect.append(allSettingsOption);
|
|
13529
14342
|
if (enabled && !settingsOnlyOption.isConnected) scopeTypeSelect.append(settingsOnlyOption);
|
|
14343
|
+
if (!enabled && chapterSettingsOption.isConnected) {
|
|
14344
|
+
if (scopeTypeSelect.value === chapterSettingsOption.value) scopeTypeSelect.value = "chapter";
|
|
14345
|
+
chapterSettingsOption.remove();
|
|
14346
|
+
}
|
|
13530
14347
|
if (!enabled && allSettingsOption.isConnected) {
|
|
13531
14348
|
if (scopeTypeSelect.value === allSettingsOption.value) scopeTypeSelect.value = "book";
|
|
13532
14349
|
allSettingsOption.remove();
|
|
@@ -13644,6 +14461,11 @@ async function openTaskDialog() {
|
|
|
13644
14461
|
clearContextWarning();
|
|
13645
14462
|
invalidateRelationshipSourcePreview();
|
|
13646
14463
|
});
|
|
14464
|
+
for (const collapseButton of scopeVolumeCollapseButtons) collapseButton.addEventListener("click", (event) => {
|
|
14465
|
+
event.preventDefault();
|
|
14466
|
+
event.stopPropagation();
|
|
14467
|
+
setTaskScopeVolumeCollapsed(collapseButton, collapseButton.getAttribute("aria-expanded") === "true");
|
|
14468
|
+
});
|
|
13647
14469
|
$("#dynamic-form").onclick = (event) => {
|
|
13648
14470
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
13649
14471
|
for (const kind of ["chapter"]) {
|
|
@@ -13711,12 +14533,15 @@ function openProviderDialog(item) {
|
|
|
13711
14533
|
return field("apiKey", item ? "替换 API 密钥(留空则不变)" : "API 密钥", "password");
|
|
13712
14534
|
};
|
|
13713
14535
|
const defaultBaseUrl = item?.baseUrl ?? defaultBaseUrlForProtocol(protocol);
|
|
14536
|
+
const useMaxCompletionTokens = item?.maxTokensParameter === "max_completion_tokens" && protocol !== "anthropic-messages";
|
|
14537
|
+
const maxTokensParameterField = `<div class="form-field provider-max-tokens-parameter-field" data-provider-max-tokens-parameter-field><span>最大输出令牌参数</span><label class="checkbox-field"><input name="useMaxCompletionTokens" type="checkbox" ${useMaxCompletionTokens ? "checked" : ""} aria-describedby="provider-max-tokens-parameter-hint"><span>使用 max_completion_tokens</span></label><small id="provider-max-tokens-parameter-hint" data-provider-max-tokens-parameter-hint>默认使用 max_tokens;OpenAI 新版模型可按需切换。</small></div>`;
|
|
13714
14538
|
openDialog(
|
|
13715
14539
|
item ? "编辑 AI 供应商" : "新建 AI 供应商",
|
|
13716
14540
|
field("name", "显示名称", "text", item?.name)
|
|
13717
14541
|
+ field("protocol", "接口协议", "select", protocol, providerProtocolOptions)
|
|
13718
14542
|
+ field("baseUrl", "API 基础地址", "url", defaultBaseUrl)
|
|
13719
14543
|
+ `<div data-provider-credential-field>${credentialFieldForProtocol(protocol)}</div>`
|
|
14544
|
+
+ maxTokensParameterField
|
|
13720
14545
|
+ field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10)
|
|
13721
14546
|
+ field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10)
|
|
13722
14547
|
+ field("note", "用途备注", "textarea", item?.note)
|
|
@@ -13726,6 +14551,7 @@ function openProviderDialog(item) {
|
|
|
13726
14551
|
name: form.get("name"),
|
|
13727
14552
|
protocol: form.get("protocol"),
|
|
13728
14553
|
baseUrl: form.get("baseUrl"),
|
|
14554
|
+
maxTokensParameter: form.get("useMaxCompletionTokens") === "on" ? "max_completion_tokens" : "max_tokens",
|
|
13729
14555
|
concurrencyLimit: Number(form.get("concurrencyLimit")),
|
|
13730
14556
|
rpmLimit: Number(form.get("rpmLimit")),
|
|
13731
14557
|
note: form.get("note"),
|
|
@@ -13742,12 +14568,23 @@ function openProviderDialog(item) {
|
|
|
13742
14568
|
const protocolSelect = $("#dialog-fields select[name='protocol']");
|
|
13743
14569
|
const baseUrlInput = $("#dialog-fields input[name='baseUrl']");
|
|
13744
14570
|
const credentialHost = $("#dialog-fields [data-provider-credential-field]");
|
|
14571
|
+
const maxTokensParameterFieldElement = $("#dialog-fields [data-provider-max-tokens-parameter-field]");
|
|
14572
|
+
const maxTokensParameterInput = $("#dialog-fields input[name='useMaxCompletionTokens']");
|
|
14573
|
+
const syncMaxTokensParameter = () => {
|
|
14574
|
+
const anthropic = protocolSelect.value === "anthropic-messages";
|
|
14575
|
+
maxTokensParameterFieldElement.hidden = anthropic;
|
|
14576
|
+
maxTokensParameterFieldElement.classList.toggle("hidden", anthropic);
|
|
14577
|
+
maxTokensParameterInput.disabled = anthropic;
|
|
14578
|
+
if (anthropic) maxTokensParameterInput.checked = false;
|
|
14579
|
+
};
|
|
13745
14580
|
const syncProviderCredentialField = () => {
|
|
13746
14581
|
const nextProtocol = protocolSelect.value;
|
|
13747
14582
|
credentialHost.innerHTML = credentialFieldForProtocol(nextProtocol);
|
|
13748
14583
|
if (!item) baseUrlInput.value = defaultBaseUrlForProtocol(nextProtocol);
|
|
14584
|
+
syncMaxTokensParameter();
|
|
13749
14585
|
};
|
|
13750
14586
|
protocolSelect.addEventListener("change", syncProviderCredentialField);
|
|
14587
|
+
syncMaxTokensParameter();
|
|
13751
14588
|
}
|
|
13752
14589
|
|
|
13753
14590
|
function openModelDialog(providerId, item = null, provider = null) {
|
|
@@ -13757,16 +14594,16 @@ function openModelDialog(providerId, item = null, provider = null) {
|
|
|
13757
14594
|
const contextWindowField = `<div class="form-field model-context-window-field"><label for="model-context-window">模型上下文令牌总量<input id="model-context-window" name="contextWindow" type="number" value="${esc(values.contextWindow)}" min="${MIN_MODEL_CONTEXT_WINDOW}" max="2000000" step="1" required aria-describedby="model-context-window-hint"></label><small id="model-context-window-hint" class="model-context-window-hint" hidden>低于 128K 的模型在小说创作场景不太适用,建议使用支持更长上下文的模型。</small></div>`;
|
|
13758
14595
|
const temperatureField = `<div class="form-field model-temperature-field"><label for="model-temperature">默认温度<input id="model-temperature" name="temperature" type="number" value="${esc(values.temperature)}" step="any" aria-describedby="model-temperature-hint"></label><small id="model-temperature-hint" class="model-temperature-hint" hidden>Kimi 模型必须设置温度为 1。</small></div>`;
|
|
13759
14596
|
const connectionTestDescription = values.multimodalEnabled && imageDefaultSupported
|
|
13760
|
-
? "
|
|
13761
|
-
: "
|
|
14597
|
+
? "使用当前已保存的模型标识符、思考设置和供应商凭据,并发送一张测试图片验证图片请求。"
|
|
14598
|
+
: "使用当前已保存的模型标识符、思考设置和供应商凭据发起最小请求。";
|
|
13762
14599
|
const connectionTest = item && item.providerStatus === "enabled"
|
|
13763
14600
|
? `<section class="model-connection-test">
|
|
13764
14601
|
<div><strong>模型连接测试</strong><p>${connectionTestDescription}</p></div>
|
|
13765
14602
|
<button class="ghost-button" type="button" data-test-model="${esc(item.id)}">测试连接</button>
|
|
13766
14603
|
</section>`
|
|
13767
14604
|
: "";
|
|
13768
|
-
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + contextWindowField + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + multimodalFields + field("enabled", "启用模型", "checkbox", values.enabled) + connectionTest, async (form) => {
|
|
13769
|
-
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", multimodalEnabled: form.get("multimodalEnabled") === "on", imageToolDefault: form.get("imageToolDefault") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
14605
|
+
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + contextWindowField + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + field("thinkingEffort", "思考强度(模型默认时不发送强度参数)", "select", values.thinkingEffort, MODEL_THINKING_EFFORT_OPTIONS) + multimodalFields + field("enabled", "启用模型", "checkbox", values.enabled) + connectionTest, async (form) => {
|
|
14606
|
+
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", thinkingEffort: form.get("thinkingEffort") ?? thinkingEffortSelect.value, multimodalEnabled: form.get("multimodalEnabled") === "on", imageToolDefault: form.get("imageToolDefault") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
13770
14607
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
13771
14608
|
await renderPlatformAiConfig();
|
|
13772
14609
|
await loadModels();
|
|
@@ -13777,6 +14614,8 @@ function openModelDialog(providerId, item = null, provider = null) {
|
|
|
13777
14614
|
const contextWindowHint = $("#model-context-window-hint");
|
|
13778
14615
|
const temperatureInput = $("#dialog-fields input[name='temperature']");
|
|
13779
14616
|
const temperatureHint = $("#model-temperature-hint");
|
|
14617
|
+
const thinkingEnabledInput = $("#dialog-fields input[name='thinkingEnabled']");
|
|
14618
|
+
const thinkingEffortSelect = $("#dialog-fields select[name='thinkingEffort']");
|
|
13780
14619
|
const multimodalInput = $("#model-multimodal-enabled");
|
|
13781
14620
|
const imageDefaultField = $("#model-image-tool-default-field");
|
|
13782
14621
|
const imageDefaultInput = $("#model-image-tool-default");
|
|
@@ -13796,11 +14635,15 @@ function openModelDialog(providerId, item = null, provider = null) {
|
|
|
13796
14635
|
const isKimi = isKimiModelId(modelIdInput.value);
|
|
13797
14636
|
temperatureHint.hidden = !isKimi;
|
|
13798
14637
|
};
|
|
14638
|
+
const syncThinkingEffort = () => {
|
|
14639
|
+
thinkingEffortSelect.disabled = !thinkingEnabledInput.checked;
|
|
14640
|
+
};
|
|
13799
14641
|
modelIdInput.addEventListener("input", () => {
|
|
13800
14642
|
if (isKimiModelId(modelIdInput.value)) temperatureInput.value = "1";
|
|
13801
14643
|
syncKimiTemperature();
|
|
13802
14644
|
});
|
|
13803
14645
|
contextWindowInput.addEventListener("input", syncModelContextWindowGuidance);
|
|
14646
|
+
thinkingEnabledInput.addEventListener("change", syncThinkingEffort);
|
|
13804
14647
|
multimodalInput?.addEventListener("change", syncMultimodalFields);
|
|
13805
14648
|
$("#dialog-fields [data-test-model]")?.addEventListener("click", async (event) => {
|
|
13806
14649
|
const button = event.currentTarget;
|
|
@@ -13823,6 +14666,7 @@ function openModelDialog(providerId, item = null, provider = null) {
|
|
|
13823
14666
|
});
|
|
13824
14667
|
syncModelContextWindowGuidance();
|
|
13825
14668
|
syncKimiTemperature();
|
|
14669
|
+
syncThinkingEffort();
|
|
13826
14670
|
syncMultimodalFields();
|
|
13827
14671
|
}
|
|
13828
14672
|
|
|
@@ -13832,6 +14676,9 @@ async function sendAi() {
|
|
|
13832
14676
|
|
|
13833
14677
|
async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
13834
14678
|
if (!state.work) return toast("请先选择作品", "error");
|
|
14679
|
+
const tab = activeAiChatTab();
|
|
14680
|
+
if (!tab) return toast("Agent 对话页签尚未就绪", "error");
|
|
14681
|
+
if (aiRequestManager.hasActive(tab.id)) return;
|
|
13835
14682
|
const composerSnapshot = captureAiPromptComposer();
|
|
13836
14683
|
const instruction = composerSnapshot.text.trim();
|
|
13837
14684
|
if (!instruction) return toast("请输入指令", "error");
|
|
@@ -13841,31 +14688,38 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
13841
14688
|
const { taskType, scope, selection } = requestScope;
|
|
13842
14689
|
if (taskType === "polish" && !selection) return toast("请先在正文中选中一段文本", "error");
|
|
13843
14690
|
const citations = composerSnapshot.citations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
14691
|
+
const selectedTaskType = $("#ai-task").value;
|
|
14692
|
+
persistActiveAiChatTab();
|
|
14693
|
+
tab.selectedModelId = $("#ai-model").value || tab.selectedModelId || null;
|
|
13844
14694
|
const requestHolder = {
|
|
13845
14695
|
snapshot: aiRequestManager.begin({
|
|
14696
|
+
tabId: tab.id,
|
|
13846
14697
|
workId: state.work.id,
|
|
13847
14698
|
conversationId: state.aiConversationId
|
|
13848
14699
|
})
|
|
13849
14700
|
};
|
|
14701
|
+
setAiChatTabStatus(tab, "streaming");
|
|
13850
14702
|
syncAiRequestControls();
|
|
13851
14703
|
try {
|
|
13852
14704
|
try {
|
|
13853
14705
|
await ensureAiModelsLoaded();
|
|
14706
|
+
await ensureAiReferencesLoaded();
|
|
13854
14707
|
} catch (error) {
|
|
13855
14708
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
13856
|
-
|
|
14709
|
+
setAiChatTabStatus(tab, "error");
|
|
13857
14710
|
return toast(`创作助手加载失败:${error.message}`, "error");
|
|
13858
14711
|
}
|
|
13859
14712
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
13860
|
-
const modelId = $("#ai-model").value;
|
|
14713
|
+
const modelId = tab.selectedModelId || state.models[0]?.id || (isActiveAiChatTab(tab) ? $("#ai-model").value : "");
|
|
13861
14714
|
if (!modelId) return toast("请先在 AI 管理中配置并选择模型", "error");
|
|
14715
|
+
tab.selectedModelId = modelId;
|
|
13862
14716
|
try {
|
|
13863
|
-
await prepareAiRequestConversation(requestHolder,
|
|
14717
|
+
await prepareAiRequestConversation(requestHolder, selectedTaskType, requestScope.conversationScope);
|
|
13864
14718
|
} catch (error) {
|
|
13865
14719
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
13866
14720
|
return toast(`对话配置锁定失败:${error.message}`, "error");
|
|
13867
14721
|
}
|
|
13868
|
-
|
|
14722
|
+
setAiChatTabStatus(tab, "streaming");
|
|
13869
14723
|
if (taskType !== "chat") {
|
|
13870
14724
|
try {
|
|
13871
14725
|
const request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
@@ -13880,16 +14734,22 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
13880
14734
|
assertAiRequestCurrent(request);
|
|
13881
14735
|
updateAiConversationSummaryFromMessage(persistedUserMessage);
|
|
13882
14736
|
requestHolder.snapshot = aiRequestManager.bind(request, { userMessageId: persistedUserMessage.id });
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
14737
|
+
tab.modelId = modelId;
|
|
14738
|
+
tab.selectedModelId = modelId;
|
|
14739
|
+
tab.promptSent = true;
|
|
14740
|
+
clearAiChatTabComposer(tab);
|
|
14741
|
+
appendMessage("user", instruction, citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id, { tab });
|
|
14742
|
+
if (isActiveAiChatTab(tab)) {
|
|
14743
|
+
state.aiConversationModelId = modelId;
|
|
14744
|
+
state.aiPromptSent = true;
|
|
14745
|
+
syncAiTaskOptions();
|
|
14746
|
+
renderAiRoleplayCharacterSelect();
|
|
14747
|
+
renderAiQuickActions();
|
|
14748
|
+
clearAiPromptComposer();
|
|
14749
|
+
}
|
|
13890
14750
|
} catch (error) {
|
|
13891
14751
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
13892
|
-
|
|
14752
|
+
setAiChatTabStatus(tab, "error");
|
|
13893
14753
|
return toast(`对话记录创建失败:${error.message}`, "error");
|
|
13894
14754
|
}
|
|
13895
14755
|
}
|
|
@@ -13925,8 +14785,9 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
13925
14785
|
const suggestionFailed = suggestion.guard?.status === "failed"
|
|
13926
14786
|
|| suggestion.toolCalls?.some((toolCall) => toolCall.status === "failed")
|
|
13927
14787
|
|| suggestion.processSteps?.some((step) => step?.toolCall?.status === "failed");
|
|
13928
|
-
if (suggestionFailed)
|
|
13929
|
-
|
|
14788
|
+
if (suggestionFailed) setAiChatTabStatus(tab, "error");
|
|
14789
|
+
tab.contextUsage = mergeAiContextUsage(tab.contextUsage, suggestion.contextUsage, false);
|
|
14790
|
+
if (isActiveAiChatTab(tab)) setAiContextMeter(suggestion.contextUsage, false);
|
|
13930
14791
|
assistantContent = suggestion.content;
|
|
13931
14792
|
assistantMetadata = { modelId, modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent, processDurationMs: suggestion.processDurationMs };
|
|
13932
14793
|
}
|
|
@@ -13955,36 +14816,50 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
13955
14816
|
if (assistantMessage) {
|
|
13956
14817
|
updateMessageCreatedAt(assistantMessage, persistedAssistantMessage.createdAt);
|
|
13957
14818
|
attachMessageIdentity(assistantMessage, persistedAssistantMessage.id);
|
|
13958
|
-
} else if (suggestion) appendSuggestion(suggestion, persistedAssistantMessage.createdAt, persistedAssistantMessage.id);
|
|
14819
|
+
} else if (suggestion) appendSuggestion(suggestion, persistedAssistantMessage.createdAt, persistedAssistantMessage.id, { tab });
|
|
13959
14820
|
}
|
|
13960
14821
|
} catch (error) {
|
|
13961
14822
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
13962
|
-
|
|
13963
|
-
if (suggestion) appendSuggestion(suggestion);
|
|
14823
|
+
setAiChatTabStatus(tab, "error");
|
|
14824
|
+
if (suggestion) appendSuggestion(suggestion, null, null, { tab });
|
|
13964
14825
|
toast(`AI 回复已生成,但历史记录保存失败:${error.message}`, "error");
|
|
13965
14826
|
}
|
|
13966
14827
|
} catch (error) {
|
|
13967
14828
|
const request = requestHolder.snapshot;
|
|
13968
14829
|
if (isAiRequestCancellation(error, request) || !aiRequestTargetsCurrentState(request)) {
|
|
13969
|
-
await persistAiRequestInterruption(request, error?.streamInterruption);
|
|
14830
|
+
const persistedInterruption = await persistAiRequestInterruption(request, error?.streamInterruption);
|
|
14831
|
+
const userStoppedCurrentReply = request.signal.reason instanceof Error
|
|
14832
|
+
&& request.signal.reason.message === "用户已终止当前回复";
|
|
14833
|
+
if (userStoppedCurrentReply && persistedInterruption && isActiveAiChatTab(tab)) {
|
|
14834
|
+
appendMessage(
|
|
14835
|
+
"assistant",
|
|
14836
|
+
persistedInterruption.content,
|
|
14837
|
+
[],
|
|
14838
|
+
persistedInterruption.createdAt,
|
|
14839
|
+
persistedInterruption.metadata,
|
|
14840
|
+
persistedInterruption.id,
|
|
14841
|
+
{ tab }
|
|
14842
|
+
);
|
|
14843
|
+
}
|
|
13970
14844
|
return;
|
|
13971
14845
|
}
|
|
13972
14846
|
if (error?.code === "AI_CONVERSATION_RESPONSE_IN_PROGRESS") {
|
|
13973
|
-
|
|
14847
|
+
setAiChatTabComposerSnapshot(tab, composerSnapshot);
|
|
14848
|
+
if (isActiveAiChatTab(tab)) restoreAiPromptComposer(composerSnapshot);
|
|
13974
14849
|
toast("当前对话仍在生成回复,请等待完成或取消后再发送", "error");
|
|
13975
|
-
$("#ai-prompt").focus();
|
|
14850
|
+
if (isActiveAiChatTab(tab)) $("#ai-prompt").focus();
|
|
13976
14851
|
return;
|
|
13977
14852
|
}
|
|
13978
14853
|
if (error?.code === "AI_IDEMPOTENT_REQUEST_IN_PROGRESS") {
|
|
13979
14854
|
toast("当前对话仍在生成回复,请等待完成或取消后再发送", "error");
|
|
13980
|
-
$("#ai-prompt").focus();
|
|
14855
|
+
if (isActiveAiChatTab(tab)) $("#ai-prompt").focus();
|
|
13981
14856
|
return;
|
|
13982
14857
|
}
|
|
13983
14858
|
if (["IDEMPOTENCY_KEY_REUSED", "AI_IDEMPOTENT_REQUEST_TERMINAL"].includes(error?.code)) {
|
|
13984
14859
|
toast(error.message, "error");
|
|
13985
14860
|
return;
|
|
13986
14861
|
}
|
|
13987
|
-
|
|
14862
|
+
setAiChatTabStatus(tab, "error");
|
|
13988
14863
|
const interruption = error?.streamInterruption;
|
|
13989
14864
|
if (interruption?.content) {
|
|
13990
14865
|
try {
|
|
@@ -14031,15 +14906,21 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
|
14031
14906
|
updateAiConversationSummaryFromMessage(persistedFailureMessage);
|
|
14032
14907
|
} catch { /* 主请求错误已显示,历史记录保存失败不覆盖原始错误 */ }
|
|
14033
14908
|
if (aiRequestTargetsCurrentState(request)) {
|
|
14034
|
-
appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, {}, persistedFailureMessage?.id);
|
|
14909
|
+
appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, {}, persistedFailureMessage?.id, { tab });
|
|
14035
14910
|
}
|
|
14036
14911
|
} finally {
|
|
14037
14912
|
aiRequestManager.finish(requestHolder.snapshot);
|
|
14038
|
-
|
|
14913
|
+
if (!isActiveAiChatTab(tab) && aiChatTabManager.get(tab.id)) tab.unread = true;
|
|
14914
|
+
if (tab.status === "streaming") setAiChatTabStatus(tab, "ready");
|
|
14915
|
+
else renderAiChatTabs();
|
|
14916
|
+
if (isActiveAiChatTab(tab)) syncAiRequestControls();
|
|
14039
14917
|
}
|
|
14040
14918
|
}
|
|
14041
14919
|
|
|
14042
14920
|
async function streamChat(requestHolder, body, idempotencyKey) {
|
|
14921
|
+
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
14922
|
+
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
14923
|
+
const feed = tab.feed;
|
|
14043
14924
|
const message = document.createElement("div");
|
|
14044
14925
|
message.className = "assistant-message is-streaming";
|
|
14045
14926
|
message.dataset.testid = "ai-stream-message";
|
|
@@ -14051,10 +14932,10 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14051
14932
|
const mountAssistantMessage = () => {
|
|
14052
14933
|
if (messageMounted) return true;
|
|
14053
14934
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return false;
|
|
14054
|
-
attachMessageHeading(message, aiAssistantLabel("正在生成"));
|
|
14055
|
-
|
|
14935
|
+
attachMessageHeading(message, aiAssistantLabel("正在生成", tab.roleplayCharacter), undefined, tab);
|
|
14936
|
+
feed.append(message);
|
|
14056
14937
|
messageMounted = true;
|
|
14057
|
-
scrollAiFeedToBottom();
|
|
14938
|
+
scrollAiFeedToBottom(feed);
|
|
14058
14939
|
return true;
|
|
14059
14940
|
};
|
|
14060
14941
|
const typewriter = createStreamTypewriter({
|
|
@@ -14066,10 +14947,11 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14066
14947
|
meta.textContent = progress.pendingCharacters > 0
|
|
14067
14948
|
? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
|
|
14068
14949
|
: `正在生成 · ${progress.visibleCharacters} 字`;
|
|
14069
|
-
scrollAiFeedToBottom();
|
|
14950
|
+
scrollAiFeedToBottom(feed);
|
|
14070
14951
|
}
|
|
14071
14952
|
});
|
|
14072
14953
|
let streamedText = "";
|
|
14954
|
+
let streamedPendingText = "";
|
|
14073
14955
|
let generatedMetadata = {};
|
|
14074
14956
|
let toolCalls = [];
|
|
14075
14957
|
let processSteps = [];
|
|
@@ -14080,7 +14962,6 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14080
14962
|
let contextAction = "ready";
|
|
14081
14963
|
let warningOnly = false;
|
|
14082
14964
|
let streamContextCompacted = false;
|
|
14083
|
-
let finalAnswerStarted = false;
|
|
14084
14965
|
const processStartedAt = Date.now();
|
|
14085
14966
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
14086
14967
|
const processStepTypewriters = new Map();
|
|
@@ -14098,8 +14979,8 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14098
14979
|
onRender: (text) => {
|
|
14099
14980
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
14100
14981
|
processStepVisibleContents.set(step, text);
|
|
14101
|
-
renderStreamingProcessSteps(
|
|
14102
|
-
scrollAiFeedToBottom();
|
|
14982
|
+
renderStreamingProcessSteps(false);
|
|
14983
|
+
scrollAiFeedToBottom(feed);
|
|
14103
14984
|
}
|
|
14104
14985
|
});
|
|
14105
14986
|
processStepTypewriters.set(step, typewriter);
|
|
@@ -14127,7 +15008,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14127
15008
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
14128
15009
|
if (eventName === "context") {
|
|
14129
15010
|
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
14130
|
-
if (!
|
|
15011
|
+
if (!tab.promptSent) setAiChatTabContextUsage(tab, payload.usage);
|
|
14131
15012
|
if (payload.conversation?.id) {
|
|
14132
15013
|
if (String(payload.conversation.id) !== requestHolder.snapshot.conversationId) {
|
|
14133
15014
|
throw new Error("流式上下文返回了其他对话");
|
|
@@ -14135,11 +15016,11 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14135
15016
|
upsertAiConversationSummary(payload.conversation);
|
|
14136
15017
|
applyAiConversationTitle(payload.conversation.title, requestHolder.snapshot.conversationId);
|
|
14137
15018
|
}
|
|
14138
|
-
if (contextAction === "warn")
|
|
15019
|
+
if (contextAction === "warn") setAiChatTabContextWarning(tab, true);
|
|
14139
15020
|
else {
|
|
14140
|
-
|
|
15021
|
+
setAiChatTabContextWarning(tab, false);
|
|
14141
15022
|
if (contextAction === "compacted") {
|
|
14142
|
-
renderConversationCompactionDivider(payload.compaction?.compactedMessageCount, payload.conversation?.messageCount);
|
|
15023
|
+
renderConversationCompactionDivider(payload.compaction?.compactedMessageCount, payload.conversation?.messageCount, feed, payload.conversation);
|
|
14143
15024
|
toast("已自动整理较早对话为长期记忆并继续发送");
|
|
14144
15025
|
}
|
|
14145
15026
|
}
|
|
@@ -14154,29 +15035,38 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14154
15035
|
const lockedModelId = typeof persistedUserMessage.metadata?.modelId === "string"
|
|
14155
15036
|
? persistedUserMessage.metadata.modelId
|
|
14156
15037
|
: typeof body.modelId === "string" ? body.modelId : null;
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
15038
|
+
tab.modelId = lockedModelId;
|
|
15039
|
+
tab.selectedModelId = lockedModelId;
|
|
15040
|
+
tab.promptSent = true;
|
|
15041
|
+
clearAiChatTabComposer(tab);
|
|
15042
|
+
appendMessage("user", persistedUserMessage.content, persistedUserMessage.citations, persistedUserMessage.createdAt, persistedUserMessage.metadata, persistedUserMessage.id, { tab });
|
|
15043
|
+
if (isActiveAiChatTab(tab)) {
|
|
15044
|
+
state.aiConversationModelId = lockedModelId;
|
|
15045
|
+
state.aiPromptSent = true;
|
|
15046
|
+
syncAiTaskOptions();
|
|
15047
|
+
renderAiRoleplayCharacterSelect();
|
|
15048
|
+
renderAiQuickActions();
|
|
15049
|
+
clearAiPromptComposer();
|
|
15050
|
+
}
|
|
14164
15051
|
mountAssistantMessage();
|
|
14165
15052
|
}
|
|
14166
15053
|
} else if (eventName === "delta") {
|
|
14167
15054
|
mountAssistantMessage();
|
|
14168
|
-
const firstFinalDelta = streamedText.length === 0;
|
|
14169
15055
|
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
14170
15056
|
streamedText += delta;
|
|
14171
|
-
|
|
15057
|
+
streamedPendingText += delta;
|
|
14172
15058
|
typewriter.append(delta);
|
|
14173
|
-
if (firstFinalDelta && processSteps.length) renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
14174
15059
|
meta.textContent = "正在生成回复……";
|
|
14175
15060
|
} else if (eventName === "process_step") {
|
|
14176
15061
|
mountAssistantMessage();
|
|
14177
15062
|
const step = { ...payload };
|
|
14178
15063
|
const append = step.append === true;
|
|
14179
15064
|
delete step.append;
|
|
15065
|
+
if (step.type === "intermediate" && typeof step.content === "string" && streamedPendingText.endsWith(step.content)) {
|
|
15066
|
+
streamedPendingText = streamedPendingText.slice(0, -step.content.length);
|
|
15067
|
+
streamedText = streamedText.slice(0, -step.content.length);
|
|
15068
|
+
typewriter.replace(streamedText);
|
|
15069
|
+
}
|
|
14180
15070
|
const existing = append ? processSteps.find((item) => item.id === step.id && item.type === step.type) : null;
|
|
14181
15071
|
if (existing && typeof step.content === "string") existing.content += step.content;
|
|
14182
15072
|
else processSteps.push(step);
|
|
@@ -14184,32 +15074,32 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14184
15074
|
if (typeof step.content === "string" && step.content.length > 0 && step.type === "thinking") {
|
|
14185
15075
|
processStepTypewriter(targetStep).append(step.content);
|
|
14186
15076
|
}
|
|
14187
|
-
renderStreamingProcessSteps(
|
|
15077
|
+
renderStreamingProcessSteps(false, elapsedProcessTime());
|
|
14188
15078
|
meta.textContent = step.type === "thinking"
|
|
14189
15079
|
? `正在思考 · 第 ${Number(step.round) || 1} 轮`
|
|
14190
15080
|
: step.type === "context_compaction"
|
|
14191
15081
|
? `已压缩第 ${Number(step.round) || 1} 轮工具上下文`
|
|
14192
15082
|
: `正在处理第 ${Number(step.round) || 1} 轮中间结果`;
|
|
14193
|
-
scrollAiFeedToBottom();
|
|
15083
|
+
scrollAiFeedToBottom(feed);
|
|
14194
15084
|
} else if (eventName === "tool_call") {
|
|
14195
15085
|
mountAssistantMessage();
|
|
14196
15086
|
const toolCall = { ...payload };
|
|
14197
15087
|
const round = toolCall.round;
|
|
14198
15088
|
delete toolCall.round;
|
|
14199
|
-
if (toolCall.status === "failed")
|
|
15089
|
+
if (toolCall.status === "failed") setAiChatTabStatus(tab, "error");
|
|
14200
15090
|
toolCalls.push(toolCall);
|
|
14201
15091
|
processSteps.push(aiToolProcessStep(toolCall, round));
|
|
14202
|
-
renderStreamingProcessSteps(
|
|
15092
|
+
renderStreamingProcessSteps(false, elapsedProcessTime());
|
|
14203
15093
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
14204
|
-
scrollAiFeedToBottom();
|
|
15094
|
+
scrollAiFeedToBottom(feed);
|
|
14205
15095
|
} else if (eventName === "context_compacted") {
|
|
14206
15096
|
streamContextCompacted = true;
|
|
14207
|
-
|
|
15097
|
+
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
14208
15098
|
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
14209
15099
|
} else if (eventName === "complete") {
|
|
14210
15100
|
if (payload.warningOnly === true) {
|
|
14211
15101
|
warningOnly = true;
|
|
14212
|
-
|
|
15102
|
+
setAiChatTabContextUsage(tab, payload.contextUsage);
|
|
14213
15103
|
return;
|
|
14214
15104
|
}
|
|
14215
15105
|
mountAssistantMessage();
|
|
@@ -14217,7 +15107,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14217
15107
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
14218
15108
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
14219
15109
|
const announcedCompaction = contextAction === "compacted" || streamContextCompacted;
|
|
14220
|
-
|
|
15110
|
+
setAiChatTabContextUsage(tab, payload.contextUsage, announcedCompaction);
|
|
14221
15111
|
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
14222
15112
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
14223
15113
|
message.classList.remove("is-streaming");
|
|
@@ -14232,11 +15122,11 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14232
15122
|
renderAiProcessSteps(message, processSteps, true, processDurationMs);
|
|
14233
15123
|
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
|
|
14234
15124
|
attachAssistantCopyAction(message, streamedText);
|
|
14235
|
-
scrollAiFeedToBottom();
|
|
15125
|
+
scrollAiFeedToBottom(feed);
|
|
14236
15126
|
} else if (eventName === "request_status") {
|
|
14237
15127
|
streamError = createClientError(payload, "AI 请求状态不可重放", response.status);
|
|
14238
15128
|
} else if (eventName === "error") {
|
|
14239
|
-
|
|
15129
|
+
setAiChatTabStatus(tab, "error");
|
|
14240
15130
|
streamError = createClientError(payload, "AI 流式调用失败", response.status);
|
|
14241
15131
|
}
|
|
14242
15132
|
};
|
|
@@ -14279,7 +15169,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14279
15169
|
message.dataset.status = "interrupted";
|
|
14280
15170
|
content.setAttribute("aria-busy", "false");
|
|
14281
15171
|
const headingRole = message.querySelector(".message-heading > span");
|
|
14282
|
-
headingRole.textContent = aiAssistantLabel(interruptionLabel);
|
|
15172
|
+
headingRole.textContent = aiAssistantLabel(interruptionLabel, tab.roleplayCharacter);
|
|
14283
15173
|
const interruptionBadge = document.createElement("strong");
|
|
14284
15174
|
interruptionBadge.className = "ai-message-status is-error";
|
|
14285
15175
|
interruptionBadge.textContent = "中断";
|
|
@@ -14288,12 +15178,14 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
14288
15178
|
renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
14289
15179
|
meta.textContent = formatAiStreamInterruptionMeta(interruptionCode, streamedText.length);
|
|
14290
15180
|
if (streamedText) attachAssistantCopyAction(message, streamedText);
|
|
14291
|
-
scrollAiFeedToBottom();
|
|
15181
|
+
scrollAiFeedToBottom(feed);
|
|
14292
15182
|
throw streamFailure;
|
|
14293
15183
|
}
|
|
14294
15184
|
}
|
|
14295
15185
|
|
|
14296
|
-
function appendMessage(role, text, citations = [], createdAt = null, metadata = {}, messageId = null) {
|
|
15186
|
+
function appendMessage(role, text, citations = [], createdAt = null, metadata = {}, messageId = null, options = {}) {
|
|
15187
|
+
const tab = options.tab ?? activeAiChatTab();
|
|
15188
|
+
const feed = options.feed ?? tab?.feed ?? $("#ai-feed");
|
|
14297
15189
|
const message = document.createElement("div");
|
|
14298
15190
|
const isFailure = role === "assistant" && text.startsWith("调用失败:");
|
|
14299
15191
|
const isInterrupted = role === "assistant" && metadata?.interrupted === true;
|
|
@@ -14309,8 +15201,9 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
14309
15201
|
});
|
|
14310
15202
|
const heading = attachMessageHeading(
|
|
14311
15203
|
message,
|
|
14312
|
-
role === "user" ? "作者" : aiAssistantLabel(isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : ""),
|
|
14313
|
-
createdAt ?? undefined
|
|
15204
|
+
role === "user" ? "作者" : aiAssistantLabel(isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "", tab?.roleplayCharacter),
|
|
15205
|
+
createdAt ?? undefined,
|
|
15206
|
+
tab
|
|
14314
15207
|
);
|
|
14315
15208
|
if (isFailure || isInterrupted) {
|
|
14316
15209
|
message.dataset.status = isInterrupted ? "interrupted" : "failed";
|
|
@@ -14320,20 +15213,27 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
14320
15213
|
failureBadge.setAttribute("aria-label", `消息状态:${isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "失败"}`);
|
|
14321
15214
|
heading.firstElementChild?.append(failureBadge);
|
|
14322
15215
|
}
|
|
14323
|
-
const
|
|
14324
|
-
?
|
|
15216
|
+
const mentionGroups = role === "user"
|
|
15217
|
+
? [
|
|
15218
|
+
["角色", metadata?.mentionCharacterIds, state.characters],
|
|
15219
|
+
["种族", metadata?.mentionRaceIds, state.races],
|
|
15220
|
+
["组织", metadata?.mentionOrganizationIds, state.organizations]
|
|
15221
|
+
]
|
|
15222
|
+
.flatMap(([kind, ids, items]) => userMessageMentionNames(ids, items).map((name) => ({ kind, name })))
|
|
14325
15223
|
: [];
|
|
14326
|
-
if (
|
|
15224
|
+
if (mentionGroups.length) {
|
|
14327
15225
|
const references = document.createElement("div");
|
|
14328
15226
|
references.className = "user-message-mentions";
|
|
14329
15227
|
const label = document.createElement("span");
|
|
14330
15228
|
label.className = "user-message-mentions-label";
|
|
14331
|
-
label.textContent = "
|
|
15229
|
+
label.textContent = "引用";
|
|
14332
15230
|
references.append(label);
|
|
14333
|
-
for (const name of
|
|
15231
|
+
for (const { kind, name } of mentionGroups) {
|
|
14334
15232
|
const reference = document.createElement("span");
|
|
14335
15233
|
reference.className = "user-message-mention";
|
|
14336
15234
|
reference.textContent = name;
|
|
15235
|
+
reference.title = `${kind}:${name}`;
|
|
15236
|
+
reference.setAttribute("aria-label", `${kind}:${name}`);
|
|
14337
15237
|
references.append(reference);
|
|
14338
15238
|
}
|
|
14339
15239
|
message.append(references);
|
|
@@ -14359,8 +15259,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
14359
15259
|
if (role === "assistant") {
|
|
14360
15260
|
renderAiProcessSteps(message, processSteps, true, processDurationMs);
|
|
14361
15261
|
}
|
|
15262
|
+
if (role === "user") attachUserCopyAction(message, text);
|
|
14362
15263
|
if (role === "assistant" && !text.startsWith("调用失败:")) {
|
|
14363
|
-
const
|
|
15264
|
+
const selectedModelId = tab?.modelId ?? tab?.selectedModelId ?? $("#ai-model").value;
|
|
15265
|
+
const selectedModel = state.models.find((model) => model.id === selectedModelId) ?? state.models[0];
|
|
14364
15266
|
const modelDisplayName = metadata?.modelDisplayName || selectedModel?.displayName || "模型";
|
|
14365
15267
|
const outputTokens = Number.isFinite(metadata?.outputTokens) ? metadata.outputTokens : estimateAiMessageTokens(text);
|
|
14366
15268
|
const meta = document.createElement("div");
|
|
@@ -14372,18 +15274,20 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
14372
15274
|
attachAssistantCopyAction(message, text);
|
|
14373
15275
|
}
|
|
14374
15276
|
attachMessageIdentity(message, messageId);
|
|
14375
|
-
|
|
14376
|
-
scrollAiFeedToBottom();
|
|
15277
|
+
feed.append(message);
|
|
15278
|
+
scrollAiFeedToBottom(feed);
|
|
14377
15279
|
}
|
|
14378
15280
|
|
|
14379
|
-
function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
15281
|
+
function appendSuggestion(suggestion, createdAt = null, messageId = null, options = {}) {
|
|
15282
|
+
const tab = options.tab ?? activeAiChatTab();
|
|
15283
|
+
const feed = options.feed ?? tab?.feed ?? $("#ai-feed");
|
|
14380
15284
|
const message = document.createElement("div");
|
|
14381
15285
|
message.className = "assistant-message";
|
|
14382
15286
|
const applicable = suggestion.action !== "note";
|
|
14383
15287
|
const guard = suggestion.guard;
|
|
14384
15288
|
const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
|
|
14385
15289
|
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, suggestion.cacheHitPercent, `基于 v${suggestion.chapterVersion ?? "-"}`, suggestion.processDurationMs))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
14386
|
-
attachMessageHeading(message, "助手建议", createdAt ?? undefined);
|
|
15290
|
+
attachMessageHeading(message, "助手建议", createdAt ?? undefined, tab);
|
|
14387
15291
|
attachAssistantCopyAction(message, suggestion.content);
|
|
14388
15292
|
attachMessageIdentity(message, messageId);
|
|
14389
15293
|
if (applicable) {
|
|
@@ -14406,8 +15310,8 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
14406
15310
|
message.querySelector(".message-actions").innerHTML = "<span>已拒绝</span>";
|
|
14407
15311
|
});
|
|
14408
15312
|
}
|
|
14409
|
-
|
|
14410
|
-
scrollAiFeedToBottom();
|
|
15313
|
+
feed.append(message);
|
|
15314
|
+
scrollAiFeedToBottom(feed);
|
|
14411
15315
|
return message;
|
|
14412
15316
|
}
|
|
14413
15317
|
|
|
@@ -15696,6 +16600,20 @@ $("#shelf-new-work").addEventListener("click", openWorkDialog);
|
|
|
15696
16600
|
$("#shelf-recycle-bin").addEventListener("click", () => { void openWorkRecycleBin(); });
|
|
15697
16601
|
$("#welcome-new-work").addEventListener("click", () => state.work ? openChapterDialog() : openWorkDialog());
|
|
15698
16602
|
$("#save-button").addEventListener("click", saveChapter);
|
|
16603
|
+
$("#chapter-search-close").addEventListener("click", () => closeChapterSearchPanel());
|
|
16604
|
+
$("#chapter-search-query").addEventListener("input", () => {
|
|
16605
|
+
chapterSearchMatchIndex = -1;
|
|
16606
|
+
renderChapterSearchStatus();
|
|
16607
|
+
});
|
|
16608
|
+
$("#chapter-search-query").addEventListener("keydown", (event) => {
|
|
16609
|
+
if (event.key !== "Enter") return;
|
|
16610
|
+
event.preventDefault();
|
|
16611
|
+
moveChapterSearchMatch(event.shiftKey ? -1 : 1);
|
|
16612
|
+
});
|
|
16613
|
+
$("#chapter-search-previous").addEventListener("click", () => moveChapterSearchMatch(-1));
|
|
16614
|
+
$("#chapter-search-next").addEventListener("click", () => moveChapterSearchMatch(1));
|
|
16615
|
+
$("#chapter-search-replace").addEventListener("click", replaceCurrentChapterSearchMatch);
|
|
16616
|
+
$("#chapter-search-replace-all").addEventListener("click", replaceAllChapterSearchMatches);
|
|
15699
16617
|
$("#chapter-foreshadow-reminder-previous").addEventListener("click", () => {
|
|
15700
16618
|
showChapterForeshadowReminderAt(chapterForeshadowReminderIndex - 1);
|
|
15701
16619
|
});
|
|
@@ -15816,6 +16734,7 @@ $("#chapter-content").addEventListener("contextmenu", (event) => {
|
|
|
15816
16734
|
showLineCitationMenu(event, lineIndexAtPointer(event.clientY));
|
|
15817
16735
|
});
|
|
15818
16736
|
$("#chapter-line-numbers-inner").addEventListener("pointerdown", (event) => {
|
|
16737
|
+
if (event.target.closest(".chapter-line-annotation-count")) return;
|
|
15819
16738
|
const row = event.target.closest(".chapter-line-number");
|
|
15820
16739
|
if (!row || event.button !== 0) return;
|
|
15821
16740
|
event.preventDefault();
|
|
@@ -15838,7 +16757,23 @@ const finishChapterLineDrag = (event) => {
|
|
|
15838
16757
|
};
|
|
15839
16758
|
$("#chapter-line-numbers-inner").addEventListener("pointerup", finishChapterLineDrag);
|
|
15840
16759
|
$("#chapter-line-numbers-inner").addEventListener("pointercancel", finishChapterLineDrag);
|
|
16760
|
+
$("#chapter-line-numbers-inner").addEventListener("click", (event) => {
|
|
16761
|
+
const bubble = event.target.closest(".chapter-line-annotation-count");
|
|
16762
|
+
if (bubble) {
|
|
16763
|
+
event.preventDefault();
|
|
16764
|
+
event.stopPropagation();
|
|
16765
|
+
openChapterAnnotationsDialog(Number(bubble.dataset.lineIndex)).catch((error) => toast(error.message, "error"));
|
|
16766
|
+
return;
|
|
16767
|
+
}
|
|
16768
|
+
const lineSelect = event.target.closest(".chapter-line-number-select");
|
|
16769
|
+
if (!lineSelect) return;
|
|
16770
|
+
const lineIndex = Number(lineSelect.closest(".chapter-line-number")?.dataset.lineIndex);
|
|
16771
|
+
if (!Number.isInteger(lineIndex)) return;
|
|
16772
|
+
paintChapterLineSelection(lineIndex, lineIndex);
|
|
16773
|
+
selectChapterLines(lineIndex, lineIndex);
|
|
16774
|
+
});
|
|
15841
16775
|
$("#chapter-line-numbers-inner").addEventListener("contextmenu", (event) => {
|
|
16776
|
+
if (event.target.closest(".chapter-line-annotation-count")) return;
|
|
15842
16777
|
const row = event.target.closest(".chapter-line-number");
|
|
15843
16778
|
if (row) showLineCitationMenu(event, Number(row.dataset.lineIndex));
|
|
15844
16779
|
});
|
|
@@ -15848,7 +16783,10 @@ $("#add-line-todo").addEventListener("click", () => createSelectedLineAnnotation
|
|
|
15848
16783
|
$("#chapter-annotations-button").addEventListener("click", () => openChapterAnnotationsDialog().catch((error) => toast(error.message, "error")));
|
|
15849
16784
|
$("#chapter-annotations-close").addEventListener("click", () => $("#chapter-annotations-dialog").close());
|
|
15850
16785
|
$("#chapter-annotations-done").addEventListener("click", () => $("#chapter-annotations-dialog").close());
|
|
15851
|
-
$("#chapter-annotations-refresh").addEventListener("click", () =>
|
|
16786
|
+
$("#chapter-annotations-refresh").addEventListener("click", () => Promise.all([
|
|
16787
|
+
loadChapterAnnotationCounts(),
|
|
16788
|
+
loadChapterAnnotations(chapterAnnotationsLineIndex)
|
|
16789
|
+
]).catch((error) => toast(error.message, "error")));
|
|
15852
16790
|
$("#left-panel-toggle").addEventListener("click", () => {
|
|
15853
16791
|
panelLayout.leftCollapsed = !panelLayout.leftCollapsed;
|
|
15854
16792
|
applyPanelLayout(true);
|
|
@@ -15862,6 +16800,10 @@ $("#mobile-panel-backdrop").addEventListener("click", () => {
|
|
|
15862
16800
|
applyPanelLayout(true);
|
|
15863
16801
|
});
|
|
15864
16802
|
$("#ai-panel-toggle").addEventListener("click", () => {
|
|
16803
|
+
if (aiConversationWorkspaceOpen) {
|
|
16804
|
+
setAiConversationWorkspaceVisible(false);
|
|
16805
|
+
return;
|
|
16806
|
+
}
|
|
15865
16807
|
panelLayout.aiCollapsed = !panelLayout.aiCollapsed;
|
|
15866
16808
|
applyPanelLayout(true);
|
|
15867
16809
|
});
|
|
@@ -15874,6 +16816,19 @@ window.addEventListener("resize", () => {
|
|
|
15874
16816
|
applyPanelLayout();
|
|
15875
16817
|
scheduleChapterLineNumbers();
|
|
15876
16818
|
});
|
|
16819
|
+
$("#ai-assistant-entry").addEventListener("click", () => {
|
|
16820
|
+
if (!state.work || !canReadPermissionModule(state.work, "ai-chat")) {
|
|
16821
|
+
return toast("当前账户没有创作助手读取权限", "error");
|
|
16822
|
+
}
|
|
16823
|
+
setModuleNavExpanded(false);
|
|
16824
|
+
if (isMobileViewport()) {
|
|
16825
|
+
panelLayout.leftCollapsed = true;
|
|
16826
|
+
applyPanelLayout(true);
|
|
16827
|
+
setAiConversationWorkspaceVisible(true);
|
|
16828
|
+
return;
|
|
16829
|
+
}
|
|
16830
|
+
setAiConversationWorkspaceVisible(true);
|
|
16831
|
+
});
|
|
15877
16832
|
$("#module-nav").addEventListener("click", (event) => {
|
|
15878
16833
|
const button = event.target.closest("button");
|
|
15879
16834
|
if (!button || button.id === "module-more-button") return;
|
|
@@ -16139,6 +17094,7 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
16139
17094
|
closeManuscriptExportMenu(true);
|
|
16140
17095
|
}
|
|
16141
17096
|
if (!event.target.closest("#ai-history-action-menu") && !event.target.closest(".ai-history-more")) closeAiHistoryActionMenu();
|
|
17097
|
+
if (!event.target.closest("#ai-conversation-switcher-menu") && !event.target.closest("#ai-conversation-switcher")) setAiConversationSwitcherVisible(false);
|
|
16142
17098
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
16143
17099
|
if (!event.target.closest("#ai-context-meter") && !event.target.closest("#ai-context-popover")) setAiContextDistributionVisible(false);
|
|
16144
17100
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
@@ -16158,7 +17114,20 @@ document.addEventListener("keydown", (event) => {
|
|
|
16158
17114
|
return;
|
|
16159
17115
|
}
|
|
16160
17116
|
if (event.key === "Escape") {
|
|
17117
|
+
if (!$("#chapter-search-panel").classList.contains("hidden")) {
|
|
17118
|
+
closeChapterSearchPanel();
|
|
17119
|
+
return;
|
|
17120
|
+
}
|
|
16161
17121
|
if (!$("#ai-history-action-menu").classList.contains("hidden")) return;
|
|
17122
|
+
if (!$("#ai-conversation-switcher-menu").classList.contains("hidden")) {
|
|
17123
|
+
setAiConversationSwitcherVisible(false);
|
|
17124
|
+
$("#ai-conversation-switcher").focus();
|
|
17125
|
+
return;
|
|
17126
|
+
}
|
|
17127
|
+
if (aiConversationWorkspaceOpen) {
|
|
17128
|
+
setAiConversationWorkspaceVisible(false);
|
|
17129
|
+
return;
|
|
17130
|
+
}
|
|
16162
17131
|
closeChapterTypeMenu();
|
|
16163
17132
|
closeLineCitationMenu();
|
|
16164
17133
|
closeMarkdownTableMenu(true);
|
|
@@ -16173,14 +17142,37 @@ document.addEventListener("keydown", (event) => {
|
|
|
16173
17142
|
event.preventDefault();
|
|
16174
17143
|
event.stopPropagation();
|
|
16175
17144
|
if (event.repeat) return;
|
|
17145
|
+
if (isChapterSearchContext()) {
|
|
17146
|
+
openChapterSearchPanel();
|
|
17147
|
+
return;
|
|
17148
|
+
}
|
|
16176
17149
|
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
16177
17150
|
}, { capture: true });
|
|
16178
17151
|
$("#ai-context-meter").addEventListener("click", () => {
|
|
16179
17152
|
setAiContextDistributionVisible($("#ai-context-popover").classList.contains("hidden"));
|
|
16180
17153
|
});
|
|
16181
17154
|
$("#ai-context-popover-close").addEventListener("click", () => setAiContextDistributionVisible(false));
|
|
16182
|
-
$("#ai-send").addEventListener("click",
|
|
17155
|
+
$("#ai-send").addEventListener("click", activateAiSendControl);
|
|
17156
|
+
$("#ai-conversation-switcher").addEventListener("click", () => {
|
|
17157
|
+
setAiConversationSwitcherVisible($("#ai-conversation-switcher-menu").classList.contains("hidden"));
|
|
17158
|
+
});
|
|
17159
|
+
$("#ai-conversation-switcher-close").addEventListener("click", () => {
|
|
17160
|
+
setAiConversationSwitcherVisible(false);
|
|
17161
|
+
$("#ai-conversation-switcher").focus();
|
|
17162
|
+
});
|
|
17163
|
+
$("#ai-workspace-open").addEventListener("click", () => setAiConversationWorkspaceVisible(true));
|
|
17164
|
+
$("#ai-workspace-close").addEventListener("click", () => setAiConversationWorkspaceVisible(false));
|
|
17165
|
+
$("#ai-switcher-history").addEventListener("click", async () => {
|
|
17166
|
+
setAiConversationSwitcherVisible(false);
|
|
17167
|
+
try {
|
|
17168
|
+
await ensureAiConversationsLoaded();
|
|
17169
|
+
setAiHistoryVisible(true);
|
|
17170
|
+
} catch (error) {
|
|
17171
|
+
toast(`对话历史加载失败:${error.message}`, "error");
|
|
17172
|
+
}
|
|
17173
|
+
});
|
|
16183
17174
|
$("#ai-new-conversation").addEventListener("click", async () => {
|
|
17175
|
+
setAiConversationSwitcherVisible(false);
|
|
16184
17176
|
const button = $("#ai-new-conversation");
|
|
16185
17177
|
button.disabled = true;
|
|
16186
17178
|
try {
|
|
@@ -16244,6 +17236,7 @@ $("#ai-context-dismiss").addEventListener("click", async () => {
|
|
|
16244
17236
|
}
|
|
16245
17237
|
});
|
|
16246
17238
|
$("#ai-history-toggle").addEventListener("click", async () => {
|
|
17239
|
+
setAiConversationSwitcherVisible(false);
|
|
16247
17240
|
if ($("#ai-history-dialog").open) return setAiHistoryVisible(false);
|
|
16248
17241
|
try {
|
|
16249
17242
|
await ensureAiConversationsLoaded();
|
|
@@ -16304,6 +17297,20 @@ $("#ai-history-action-menu").addEventListener("click", async (event) => {
|
|
|
16304
17297
|
}
|
|
16305
17298
|
return;
|
|
16306
17299
|
}
|
|
17300
|
+
if (action === "copy-session-id") {
|
|
17301
|
+
option.disabled = true;
|
|
17302
|
+
try {
|
|
17303
|
+
await copyAiConversationSessionId(conversation);
|
|
17304
|
+
closeAiHistoryActionMenu(true);
|
|
17305
|
+
toast("对话 Session ID 已复制");
|
|
17306
|
+
} catch (error) {
|
|
17307
|
+
toast(`Session ID 复制失败:${error.message}`, "error");
|
|
17308
|
+
option.focus();
|
|
17309
|
+
} finally {
|
|
17310
|
+
option.disabled = false;
|
|
17311
|
+
}
|
|
17312
|
+
return;
|
|
17313
|
+
}
|
|
16307
17314
|
if (action === "delete") {
|
|
16308
17315
|
option.disabled = true;
|
|
16309
17316
|
try {
|
|
@@ -16356,7 +17363,7 @@ $("#ai-prompt").addEventListener("keydown", (event) => {
|
|
|
16356
17363
|
}
|
|
16357
17364
|
if (shouldSendAiPrompt(event)) {
|
|
16358
17365
|
event.preventDefault();
|
|
16359
|
-
if (!$("#ai-send").disabled) void sendAi();
|
|
17366
|
+
if (!$("#ai-send").disabled && !aiRequestManager.hasActive(activeAiChatTab()?.id)) void sendAi();
|
|
16360
17367
|
}
|
|
16361
17368
|
});
|
|
16362
17369
|
$(".quick-actions").addEventListener("click", (event) => {
|
|
@@ -16452,7 +17459,6 @@ $("#reader-font-size").addEventListener("change", (event) => updateReadingPrefer
|
|
|
16452
17459
|
$("#reader-line-height").addEventListener("change", (event) => updateReadingPreference({ lineHeight: Number(event.currentTarget.value) }));
|
|
16453
17460
|
$("#reader-theme").addEventListener("change", (event) => updateReadingPreference({ theme: event.currentTarget.value }));
|
|
16454
17461
|
$("#reader-view").addEventListener("keydown", handleReadingKeyboard);
|
|
16455
|
-
$("#reader-viewport").addEventListener("wheel", handleReadingWheel, { passive: false });
|
|
16456
17462
|
$("#reader-viewport").addEventListener("scroll", () => {
|
|
16457
17463
|
if (readingPreferences.mode === "scroll") scheduleReadingPositionSave();
|
|
16458
17464
|
}, { passive: true });
|
|
@@ -16469,7 +17475,7 @@ window.addEventListener("pagehide", dismissDeleteToasts);
|
|
|
16469
17475
|
window.addEventListener("beforeunload", (event) => {
|
|
16470
17476
|
if (hasUnsavedEditorChanges()) event.preventDefault();
|
|
16471
17477
|
if (aiRequestManager.hasActive()) {
|
|
16472
|
-
toast("
|
|
17478
|
+
toast("仍有 Agent turn 尚未结束,刷新会中断全部生成;已收到的内容会保留在历史记录中", "warning");
|
|
16473
17479
|
}
|
|
16474
17480
|
});
|
|
16475
17481
|
window.addEventListener("online", () => {
|
|
@@ -16486,6 +17492,7 @@ window.addEventListener("resize", () => {
|
|
|
16486
17492
|
});
|
|
16487
17493
|
});
|
|
16488
17494
|
|
|
17495
|
+
initializeAiChatTabs();
|
|
16489
17496
|
initializePage().catch((error) => {
|
|
16490
17497
|
restoringPageRoute = false;
|
|
16491
17498
|
if (state.user) {
|