@musnows/scriverse 0.5.10 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +80 -5
- package/dist/ai.js.map +1 -1
- package/dist/app.js +61 -4
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +25 -0
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +3 -0
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +66 -1
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +666 -110
- package/dist/public/entity-version.js +2 -0
- package/dist/public/index.html +10 -2
- package/dist/public/markdown.js +6 -1
- package/dist/public/module-request-cache.d.ts +17 -0
- package/dist/public/module-request-cache.js +35 -0
- package/dist/public/page-route.d.ts +1 -1
- package/dist/public/page-route.js +1 -0
- package/dist/public/stream-typewriter.d.ts +19 -0
- package/dist/public/stream-typewriter.js +83 -0
- package/dist/public/styles.css +70 -6
- package/dist/public/work-permissions.d.ts +2 -2
- package/dist/public/work-permissions.js +8 -0
- package/dist/store.js +164 -18
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -5
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +9 -0
- package/dist/work-permissions.js.map +1 -1
- package/dist/writing-progress-time.js +71 -0
- package/dist/writing-progress-time.js.map +1 -0
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260728-galaxy-edge-stars-v3";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
|
-
import { renderMarkdown } from "/markdown.js?v=
|
|
3
|
+
import { renderMarkdown } from "/markdown.js?v=20260730-table-wrap-menu-v1";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
5
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
@@ -8,6 +8,7 @@ import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260
|
|
|
8
8
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
9
9
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
10
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
11
|
+
import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
|
|
11
12
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
12
13
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
13
14
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
@@ -15,7 +16,7 @@ import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-ma
|
|
|
15
16
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
16
17
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
17
18
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260725-unified-permissions";
|
|
18
|
-
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=
|
|
19
|
+
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260729-drafts-v1";
|
|
19
20
|
import {
|
|
20
21
|
chapterVersionSourceLabel,
|
|
21
22
|
foreshadowStatusLabel,
|
|
@@ -35,18 +36,19 @@ import {
|
|
|
35
36
|
timelineStatusLabel,
|
|
36
37
|
characterStateFieldLabel
|
|
37
38
|
} from "/display-labels.js?v=20260728-hybrid-search-v1";
|
|
38
|
-
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=
|
|
39
|
+
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260729-drafts-v1";
|
|
39
40
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
40
41
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
41
42
|
import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
|
|
42
43
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
43
|
-
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=
|
|
44
|
+
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260729-drafts-v1";
|
|
44
45
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
45
46
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
46
47
|
import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
|
|
47
48
|
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
48
49
|
import { filterRelationships } from "/relationship-filters.js?v=20260726-relationship-filters";
|
|
49
50
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260726-background-task-center-v1";
|
|
51
|
+
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
50
52
|
import {
|
|
51
53
|
clampCropRect,
|
|
52
54
|
containImageRect,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
60
62
|
|
|
61
63
|
const defaultPageSizes = Object.freeze({
|
|
64
|
+
drafts: 30,
|
|
62
65
|
settings: 30,
|
|
63
66
|
characters: 30,
|
|
64
67
|
races: 30,
|
|
@@ -115,6 +118,21 @@ const state = {
|
|
|
115
118
|
contextChapterId: null
|
|
116
119
|
};
|
|
117
120
|
|
|
121
|
+
const moduleRequestCache = createModuleRequestCache();
|
|
122
|
+
const cachedWorkModules = new Set([
|
|
123
|
+
"drafts",
|
|
124
|
+
"settings",
|
|
125
|
+
"characters",
|
|
126
|
+
"races",
|
|
127
|
+
"organizations",
|
|
128
|
+
"timeline",
|
|
129
|
+
"outlines",
|
|
130
|
+
"relationships",
|
|
131
|
+
"reviews",
|
|
132
|
+
"tasks",
|
|
133
|
+
"ai-settings"
|
|
134
|
+
]);
|
|
135
|
+
|
|
118
136
|
function createPresenceClientId() {
|
|
119
137
|
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
120
138
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
@@ -369,6 +387,9 @@ let aiReferencesLoadWorkId = null;
|
|
|
369
387
|
let aiConversationsLoadPromise = null;
|
|
370
388
|
let aiConversationsLoadWorkId = null;
|
|
371
389
|
let workScopedUiGeneration = 0;
|
|
390
|
+
const loadedVolumeChapterIds = new Set();
|
|
391
|
+
const volumeChapterLoadingIds = new Set();
|
|
392
|
+
const volumeChapterRequests = new Map();
|
|
372
393
|
let raceHierarchyLoadPromise = null;
|
|
373
394
|
let raceHierarchyLoadWorkId = null;
|
|
374
395
|
let loadedRaceHierarchyWorkId = null;
|
|
@@ -871,7 +892,10 @@ let entityEditorReadOnly = false;
|
|
|
871
892
|
let chapterEditorReadOnly = true;
|
|
872
893
|
let characterListPage = 1;
|
|
873
894
|
let taskListPage = 1;
|
|
895
|
+
let draftTypeFilter = "all";
|
|
896
|
+
let draftFiltersPanelOpen = false;
|
|
874
897
|
const moduleListPages = {
|
|
898
|
+
drafts: 1,
|
|
875
899
|
settings: 1,
|
|
876
900
|
organizations: 1,
|
|
877
901
|
timeline: 1,
|
|
@@ -901,6 +925,7 @@ let characterSectionEditorDirty = false;
|
|
|
901
925
|
let settingEditorVditor = null;
|
|
902
926
|
let knowledgeSectionVditor = null;
|
|
903
927
|
let characterSectionVditor = null;
|
|
928
|
+
let formDialogVditors = [];
|
|
904
929
|
let entityHistoryContext = null;
|
|
905
930
|
let moduleContentInteractionsBound = false;
|
|
906
931
|
|
|
@@ -1367,7 +1392,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1367
1392
|
read_chapters: "读取章节",
|
|
1368
1393
|
grep: "查询正文关键字",
|
|
1369
1394
|
search_story_entities: "搜索作品实体",
|
|
1370
|
-
read_character_sections: "读取人物 Markdown 章节"
|
|
1395
|
+
read_character_sections: "读取人物 Markdown 章节",
|
|
1396
|
+
search_drafts: "搜索草稿"
|
|
1371
1397
|
};
|
|
1372
1398
|
|
|
1373
1399
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1375,10 +1401,50 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
1375
1401
|
read_chapters: "读取指定章节的概要、正文或两者。",
|
|
1376
1402
|
grep: "查询正文关键字所在的完整段落及章节信息。",
|
|
1377
1403
|
search_story_entities: "按实体名、拼音或短关键词混合检索设定、人物、组织等结构化记录;非语义问答。",
|
|
1378
|
-
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。"
|
|
1404
|
+
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。",
|
|
1405
|
+
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。"
|
|
1379
1406
|
};
|
|
1380
1407
|
|
|
1381
1408
|
let aiFeedScrollFrame = null;
|
|
1409
|
+
let markdownTableMenuTarget = null;
|
|
1410
|
+
let markdownTableMenuTrigger = null;
|
|
1411
|
+
|
|
1412
|
+
function closeMarkdownTableMenu(restoreFocus = false) {
|
|
1413
|
+
const trigger = markdownTableMenuTrigger;
|
|
1414
|
+
$("#markdown-table-menu").classList.add("hidden");
|
|
1415
|
+
markdownTableMenuTarget = null;
|
|
1416
|
+
markdownTableMenuTrigger = null;
|
|
1417
|
+
if (restoreFocus && trigger?.isConnected) trigger.focus();
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function setMarkdownTableWrapping(table, wrapping) {
|
|
1421
|
+
table.classList.toggle("is-wrapping", wrapping);
|
|
1422
|
+
table.setAttribute("aria-label", wrapping ? "Markdown 表格,内容自动换行" : "Markdown 表格,内容不换行");
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function openMarkdownTableMenu(header, clientX, clientY) {
|
|
1426
|
+
const table = header.closest(".markdown-table-scroll");
|
|
1427
|
+
if (!table) return;
|
|
1428
|
+
if (header.closest(".is-streaming")) {
|
|
1429
|
+
toast("回复生成完成后可以设置表格换行");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
closeChapterTypeMenu();
|
|
1433
|
+
closeLineCitationMenu();
|
|
1434
|
+
markdownTableMenuTarget = table;
|
|
1435
|
+
markdownTableMenuTrigger = header;
|
|
1436
|
+
const menu = $("#markdown-table-menu");
|
|
1437
|
+
const toggle = $("#markdown-table-wrap-toggle");
|
|
1438
|
+
toggle.setAttribute("aria-checked", String(table.classList.contains("is-wrapping")));
|
|
1439
|
+
menu.classList.remove("hidden");
|
|
1440
|
+
const menuRect = menu.getBoundingClientRect();
|
|
1441
|
+
const headerRect = header.getBoundingClientRect();
|
|
1442
|
+
const anchorX = clientX > 0 ? clientX : headerRect.left;
|
|
1443
|
+
const anchorY = clientY > 0 ? clientY : headerRect.bottom;
|
|
1444
|
+
menu.style.left = `${Math.max(8, Math.min(anchorX, window.innerWidth - menuRect.width - 8))}px`;
|
|
1445
|
+
menu.style.top = `${Math.max(8, Math.min(anchorY, window.innerHeight - menuRect.height - 8))}px`;
|
|
1446
|
+
toggle.focus();
|
|
1447
|
+
}
|
|
1382
1448
|
|
|
1383
1449
|
function scrollAiFeedToBottom() {
|
|
1384
1450
|
const feed = $("#ai-feed");
|
|
@@ -2080,14 +2146,19 @@ async function api(path, options = {}) {
|
|
|
2080
2146
|
if (response.status === 401 && !path.startsWith("/api/auth/") && !path.includes("/presence")) {
|
|
2081
2147
|
state.user = null;
|
|
2082
2148
|
state.csrfToken = null;
|
|
2149
|
+
moduleRequestCache.clear();
|
|
2083
2150
|
showAuth(false);
|
|
2084
2151
|
}
|
|
2085
2152
|
const error = new Error(payload.error?.message ?? `请求失败:${response.status}`);
|
|
2086
2153
|
error.code = payload.error?.code;
|
|
2087
2154
|
throw error;
|
|
2088
2155
|
}
|
|
2089
|
-
if (response.status === 204)
|
|
2156
|
+
if (response.status === 204) {
|
|
2157
|
+
invalidateModuleRequestsAfterMutation(path, method);
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2090
2160
|
const payload = await response.json();
|
|
2161
|
+
invalidateModuleRequestsAfterMutation(path, method);
|
|
2091
2162
|
return payload.data;
|
|
2092
2163
|
}
|
|
2093
2164
|
|
|
@@ -2109,6 +2180,63 @@ async function apiAllPages(path, limit = 100) {
|
|
|
2109
2180
|
}
|
|
2110
2181
|
}
|
|
2111
2182
|
|
|
2183
|
+
function cachedModuleRequest(module, requestKey, loader, options = {}) {
|
|
2184
|
+
const workId = state.work?.id;
|
|
2185
|
+
if (!workId) return Promise.resolve().then(loader);
|
|
2186
|
+
return moduleRequestCache.request(workId, module, requestKey, loader, options);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function moduleApi(module, path, options = {}) {
|
|
2190
|
+
return cachedModuleRequest(module, `api:${path}`, () => api(path), options);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function moduleApiPage(module, path, page = 1, limit = 30, options = {}) {
|
|
2194
|
+
return cachedModuleRequest(module, `page:${path}:${page}:${limit}`, () => apiPage(path, page, limit), options);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function moduleApiAllPages(module, path, limit = 100, options = {}) {
|
|
2198
|
+
return cachedModuleRequest(module, `all:${path}:${limit}`, () => apiAllPages(path, limit), options);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function invalidateModuleRequestsAfterMutation(path, method) {
|
|
2202
|
+
if (["GET", "HEAD", "OPTIONS"].includes(method) || !state.work) return;
|
|
2203
|
+
if (
|
|
2204
|
+
path.startsWith("/api/auth/")
|
|
2205
|
+
|| path.includes("/presence")
|
|
2206
|
+
|| path.includes("/context/prepare")
|
|
2207
|
+
|| path.includes("/ai-context-usage")
|
|
2208
|
+
|| path.includes("/chat/stream")
|
|
2209
|
+
) return;
|
|
2210
|
+
|
|
2211
|
+
const affected = new Set();
|
|
2212
|
+
if (path.includes("/ai-settings") || path.includes("/task-defaults") || path.includes("/providers") || path.includes("/models")) {
|
|
2213
|
+
affected.add("ai-settings");
|
|
2214
|
+
}
|
|
2215
|
+
if (path.includes("/tasks")) affected.add("tasks");
|
|
2216
|
+
if (path.includes("/drafts")) affected.add("drafts");
|
|
2217
|
+
if (path.includes("/settings") && !path.includes("/ai-settings")) affected.add("settings");
|
|
2218
|
+
if (path.includes("/characters") || path.includes("/character-sections")) affected.add("characters");
|
|
2219
|
+
if (path.includes("/races")) affected.add("races");
|
|
2220
|
+
if (path.includes("/organizations")) affected.add("organizations");
|
|
2221
|
+
if (path.includes("/timeline")) affected.add("timeline");
|
|
2222
|
+
if (path.includes("/outlines") || path.includes("/foreshadows")) affected.add("outlines");
|
|
2223
|
+
if (path.includes("/relationships")) affected.add("relationships");
|
|
2224
|
+
if (path.includes("/reviews")) affected.add("reviews");
|
|
2225
|
+
if (path.includes("/entity-versions/")) {
|
|
2226
|
+
if (path.includes("/draft/")) affected.add("drafts");
|
|
2227
|
+
if (path.includes("/setting/")) affected.add("settings");
|
|
2228
|
+
if (path.includes("/character/")) affected.add("characters");
|
|
2229
|
+
if (path.includes("/race/")) affected.add("races");
|
|
2230
|
+
if (path.includes("/organization/")) affected.add("organizations");
|
|
2231
|
+
if (path.includes("/timeline-event/") || path.includes("/timeline-track/")) affected.add("timeline");
|
|
2232
|
+
if (path.includes("/chapter-outline/") || path.includes("/foreshadow/")) affected.add("outlines");
|
|
2233
|
+
if (path.includes("/relationship/")) affected.add("relationships");
|
|
2234
|
+
if (path.includes("/review/")) affected.add("reviews");
|
|
2235
|
+
}
|
|
2236
|
+
if (cachedWorkModules.has(state.module)) affected.add(state.module);
|
|
2237
|
+
affected.forEach((module) => moduleRequestCache.invalidate(state.work.id, module));
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2112
2240
|
async function initializeProductFooters() {
|
|
2113
2241
|
const year = String(new Date().getFullYear());
|
|
2114
2242
|
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
@@ -2706,6 +2834,7 @@ function renderSettingsHub() {
|
|
|
2706
2834
|
const hasWork = Boolean(state.work);
|
|
2707
2835
|
const canManageWork = hasWork && ["admin", "owner"].includes(String(state.work.accessRole));
|
|
2708
2836
|
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
2837
|
+
const canReadFullExport = canReadAggregate && canReadModule("drafts");
|
|
2709
2838
|
const isAdmin = state.user?.role === "admin";
|
|
2710
2839
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
2711
2840
|
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
@@ -2715,7 +2844,7 @@ function renderSettingsHub() {
|
|
|
2715
2844
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
2716
2845
|
$("#work-audit-button").disabled = !canManageWork;
|
|
2717
2846
|
$("#top-search-button").disabled = !canReadAggregate;
|
|
2718
|
-
$("#export-button").disabled = !
|
|
2847
|
+
$("#export-button").disabled = !canReadFullExport;
|
|
2719
2848
|
$("#settings-return").textContent = settingsReturnContext?.view === "shelf" || !hasWork ? "返回书架" : "返回当前作品";
|
|
2720
2849
|
$("#settings-work-note").textContent = hasWork
|
|
2721
2850
|
? `当前作品:《${state.work.title}》。导出的 ZIP 内含 Markdown 正文,仅包含分卷、章节标题与正文。`
|
|
@@ -2796,11 +2925,15 @@ const workAuditActionLabels = {
|
|
|
2796
2925
|
"chapter.deleted": "删除章节",
|
|
2797
2926
|
"chapter.purged": "彻底删除章节",
|
|
2798
2927
|
"chapter.restored": "恢复章节",
|
|
2928
|
+
"draft.created": "创建草稿",
|
|
2929
|
+
"draft.updated": "更新草稿",
|
|
2930
|
+
"draft.deleted": "删除草稿",
|
|
2931
|
+
"draft.restored": "恢复草稿",
|
|
2799
2932
|
"work.imported": "导入正文"
|
|
2800
2933
|
};
|
|
2801
2934
|
|
|
2802
2935
|
function workAuditEntityLabel(type) {
|
|
2803
|
-
return ({ work: "作品", volume: "分卷", chapter: "章节", user: "用户" })[type] ?? type;
|
|
2936
|
+
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "草稿", user: "用户" })[type] ?? type;
|
|
2804
2937
|
}
|
|
2805
2938
|
|
|
2806
2939
|
function workAuditDetailText(detail) {
|
|
@@ -2911,6 +3044,7 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2911
3044
|
const settings = await api("/api/platform/ui-settings");
|
|
2912
3045
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2913
3046
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
3047
|
+
$("#page-size-drafts").value = String(pageSizes.drafts);
|
|
2914
3048
|
$("#page-size-settings").value = String(pageSizes.settings);
|
|
2915
3049
|
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2916
3050
|
$("#page-size-organizations").value = String(pageSizes.organizations);
|
|
@@ -3292,11 +3426,16 @@ function resetWorkScopedUiCaches() {
|
|
|
3292
3426
|
state.settings = [];
|
|
3293
3427
|
state.races = [];
|
|
3294
3428
|
characterListPage = 1;
|
|
3429
|
+
draftTypeFilter = "all";
|
|
3430
|
+
draftFiltersPanelOpen = false;
|
|
3295
3431
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
3296
3432
|
relationshipFilters.fromCharacterIds = [];
|
|
3297
3433
|
relationshipFilters.toCharacterIds = [];
|
|
3298
3434
|
taskListPage = 1;
|
|
3299
3435
|
state.collapsedVolumeIds.clear();
|
|
3436
|
+
loadedVolumeChapterIds.clear();
|
|
3437
|
+
volumeChapterLoadingIds.clear();
|
|
3438
|
+
volumeChapterRequests.clear();
|
|
3300
3439
|
state.collapsedRaceIds.clear();
|
|
3301
3440
|
lastSavedChapterSnapshot = null;
|
|
3302
3441
|
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
@@ -3320,7 +3459,13 @@ function resetWorkScopedUiCaches() {
|
|
|
3320
3459
|
async function selectWork(workId, preferredChapterId = null) {
|
|
3321
3460
|
const discarding = state.work?.id !== workId && state.dirty;
|
|
3322
3461
|
if (discarding && !(await confirmDiscardChanges())) return false;
|
|
3323
|
-
|
|
3462
|
+
if (state.work?.id === workId) {
|
|
3463
|
+
workScopedUiGeneration += 1;
|
|
3464
|
+
loadedVolumeChapterIds.clear();
|
|
3465
|
+
volumeChapterLoadingIds.clear();
|
|
3466
|
+
volumeChapterRequests.clear();
|
|
3467
|
+
}
|
|
3468
|
+
const nextWork = await api(`/api/works/${workId}?directory=volumes`);
|
|
3324
3469
|
if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
|
|
3325
3470
|
if (discarding) setSaveState("就绪");
|
|
3326
3471
|
$("#app").classList.remove("shelf-mode");
|
|
@@ -3331,6 +3476,8 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
3331
3476
|
$("#settings-button").removeAttribute("aria-current");
|
|
3332
3477
|
settingsReturnContext = null;
|
|
3333
3478
|
state.work = nextWork;
|
|
3479
|
+
state.work.volumes = state.work.volumes.map((volume) => ({ ...volume, chapters: Array.isArray(volume.chapters) ? volume.chapters : [] }));
|
|
3480
|
+
state.collapsedVolumeIds = new Set(state.work.volumes.map((volume) => volume.id));
|
|
3334
3481
|
state.chapter = null;
|
|
3335
3482
|
chapterEditorReadOnly = true;
|
|
3336
3483
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
@@ -3340,40 +3487,121 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
3340
3487
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
3341
3488
|
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
3342
3489
|
renderTree();
|
|
3343
|
-
|
|
3344
|
-
const targetChapter = chapters.find((chapter) => chapter.id === preferredChapterId) ?? chapters[0];
|
|
3490
|
+
void loadAllVolumeChapters(nextWork.id);
|
|
3345
3491
|
if (state.module === "editor" && preferredChapterId) await selectChapter(preferredChapterId);
|
|
3346
|
-
else if (state.module === "editor" && targetChapter) await selectChapter(targetChapter.id);
|
|
3347
3492
|
else if (state.module === "editor" && canReadModule("editor")) showWelcome(true);
|
|
3348
3493
|
else if (!canReadModule(state.module)) showWelcome(true);
|
|
3349
3494
|
else await showModule(state.module);
|
|
3350
3495
|
return true;
|
|
3351
3496
|
}
|
|
3352
3497
|
|
|
3498
|
+
async function loadVolumeChapters(volumeId) {
|
|
3499
|
+
if (!state.work || loadedVolumeChapterIds.has(volumeId)) return;
|
|
3500
|
+
const existingRequest = volumeChapterRequests.get(volumeId);
|
|
3501
|
+
if (existingRequest) return existingRequest;
|
|
3502
|
+
const workId = state.work.id;
|
|
3503
|
+
const generation = workScopedUiGeneration;
|
|
3504
|
+
volumeChapterLoadingIds.add(volumeId);
|
|
3505
|
+
renderTree();
|
|
3506
|
+
const request = (async () => {
|
|
3507
|
+
try {
|
|
3508
|
+
const chapters = await apiAllPages(`/api/volumes/${encodeURIComponent(volumeId)}/chapters`, 100);
|
|
3509
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
3510
|
+
const volume = state.work.volumes.find((item) => item.id === volumeId);
|
|
3511
|
+
if (!volume) return;
|
|
3512
|
+
volume.chapters = chapters;
|
|
3513
|
+
volume.chapterCount = chapters.length;
|
|
3514
|
+
loadedVolumeChapterIds.add(volumeId);
|
|
3515
|
+
renderTree();
|
|
3516
|
+
} catch (error) {
|
|
3517
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) {
|
|
3518
|
+
toast(`加载分卷章节失败:${error.message}`, "error");
|
|
3519
|
+
renderTree();
|
|
3520
|
+
}
|
|
3521
|
+
} finally {
|
|
3522
|
+
volumeChapterLoadingIds.delete(volumeId);
|
|
3523
|
+
volumeChapterRequests.delete(volumeId);
|
|
3524
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) renderTree();
|
|
3525
|
+
}
|
|
3526
|
+
})();
|
|
3527
|
+
volumeChapterRequests.set(volumeId, request);
|
|
3528
|
+
return request;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
async function loadAllVolumeChapters(workId) {
|
|
3532
|
+
const volumeIds = state.work?.id === workId ? state.work.volumes.map((volume) => volume.id) : [];
|
|
3533
|
+
for (const volumeId of volumeIds) {
|
|
3534
|
+
if (state.work?.id !== workId) return;
|
|
3535
|
+
await loadVolumeChapters(volumeId);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function mergeChapterDirectoryEntry(chapter) {
|
|
3540
|
+
if (!state.work || !chapter?.volumeId) return;
|
|
3541
|
+
const volume = state.work.volumes.find((item) => item.id === chapter.volumeId);
|
|
3542
|
+
if (!volume) return;
|
|
3543
|
+
const directoryEntry = {
|
|
3544
|
+
id: chapter.id,
|
|
3545
|
+
workId: chapter.workId,
|
|
3546
|
+
volumeId: chapter.volumeId,
|
|
3547
|
+
title: chapter.title,
|
|
3548
|
+
chapterType: chapter.chapterType,
|
|
3549
|
+
sortOrder: chapter.sortOrder,
|
|
3550
|
+
wordCount: chapter.wordCount,
|
|
3551
|
+
versionNo: chapter.versionNo,
|
|
3552
|
+
analysisStatus: chapter.analysisStatus,
|
|
3553
|
+
excludedFromAnalysis: chapter.excludedFromAnalysis,
|
|
3554
|
+
createdAt: chapter.createdAt,
|
|
3555
|
+
updatedAt: chapter.updatedAt
|
|
3556
|
+
};
|
|
3557
|
+
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
3558
|
+
const existingIndex = chapters.findIndex((item) => item.id === chapter.id);
|
|
3559
|
+
if (existingIndex >= 0) chapters[existingIndex] = directoryEntry;
|
|
3560
|
+
else chapters.push(directoryEntry);
|
|
3561
|
+
chapters.sort((left, right) => Number(left.sortOrder) - Number(right.sortOrder));
|
|
3562
|
+
volume.chapters = chapters;
|
|
3563
|
+
volume.chapterCount = Math.max(Number(volume.chapterCount ?? 0), chapters.length);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3353
3566
|
function renderTree() {
|
|
3354
3567
|
if (!state.work) return;
|
|
3355
|
-
const count = state.work.volumes.reduce((total, volume) => total + volume.chapters
|
|
3568
|
+
const count = state.work.volumes.reduce((total, volume) => total + Number(volume.chapterCount ?? volume.chapters?.length ?? 0), 0);
|
|
3356
3569
|
const proseEditable = canEditProse();
|
|
3357
3570
|
$("#chapter-count").textContent = `${count} 章`;
|
|
3358
3571
|
$("#novel-tree").classList.remove("empty-copy");
|
|
3359
|
-
$("#novel-tree").innerHTML = state.work.volumes.map((volume) =>
|
|
3360
|
-
|
|
3572
|
+
$("#novel-tree").innerHTML = state.work.volumes.map((volume) => {
|
|
3573
|
+
const collapsed = state.collapsedVolumeIds.has(volume.id);
|
|
3574
|
+
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
3575
|
+
const chapterContent = collapsed
|
|
3576
|
+
? ""
|
|
3577
|
+
: volumeChapterLoadingIds.has(volume.id)
|
|
3578
|
+
? '<p class="entity-history-empty">正在加载章节……</p>'
|
|
3579
|
+
: !loadedVolumeChapterIds.has(volume.id)
|
|
3580
|
+
? '<p class="entity-history-empty">展开后加载章节。</p>'
|
|
3581
|
+
: chapters.length
|
|
3582
|
+
? chapters.map((chapter) => `
|
|
3583
|
+
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="${proseEditable ? "true" : "false"}" title="${proseEditable ? "拖拽排序;Alt+方向键排序,Alt+Shift+方向键跨卷" : ""}">
|
|
3584
|
+
<span>${esc(chapter.title)}</span><span class="chapter-node-meta">${chapter.chapterType && chapter.chapterType !== "正文" ? `<em class="chapter-type-badge">${esc(chapter.chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small></span>
|
|
3585
|
+
</button>`).join("")
|
|
3586
|
+
: '<p class="entity-history-empty">本卷还没有章节。</p>';
|
|
3587
|
+
return `
|
|
3588
|
+
<div class="volume-node ${collapsed ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
|
|
3361
3589
|
<div class="volume-title">
|
|
3362
|
-
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${
|
|
3590
|
+
<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>
|
|
3363
3591
|
${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>` : ""}
|
|
3364
3592
|
</div>
|
|
3365
3593
|
<div class="volume-chapters">
|
|
3366
|
-
${
|
|
3367
|
-
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="${proseEditable ? "true" : "false"}" title="${proseEditable ? "拖拽排序;Alt+方向键排序,Alt+Shift+方向键跨卷" : ""}">
|
|
3368
|
-
<span>${esc(chapter.title)}</span><span class="chapter-node-meta">${chapter.chapterType && chapter.chapterType !== "正文" ? `<em class="chapter-type-badge">${esc(chapter.chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small></span>
|
|
3369
|
-
</button>`).join("")}
|
|
3594
|
+
${chapterContent}
|
|
3370
3595
|
</div>
|
|
3371
|
-
</div
|
|
3596
|
+
</div>`;
|
|
3597
|
+
}).join("");
|
|
3372
3598
|
$("#novel-tree").querySelectorAll("[data-volume-toggle]").forEach((button) => {
|
|
3373
3599
|
button.addEventListener("click", () => {
|
|
3374
3600
|
const volumeId = button.dataset.volumeToggle;
|
|
3375
|
-
if (state.collapsedVolumeIds.has(volumeId))
|
|
3376
|
-
|
|
3601
|
+
if (state.collapsedVolumeIds.has(volumeId)) {
|
|
3602
|
+
state.collapsedVolumeIds.delete(volumeId);
|
|
3603
|
+
void loadVolumeChapters(volumeId);
|
|
3604
|
+
} else state.collapsedVolumeIds.add(volumeId);
|
|
3377
3605
|
renderTree();
|
|
3378
3606
|
});
|
|
3379
3607
|
button.addEventListener("contextmenu", (event) => {
|
|
@@ -3393,7 +3621,7 @@ function renderTree() {
|
|
|
3393
3621
|
button.closest(".volume-node")?.classList.remove("is-drag-target");
|
|
3394
3622
|
const chapterId = event.dataTransfer?.getData("text/plain");
|
|
3395
3623
|
const volume = state.work?.volumes.find((item) => item.id === button.dataset.volumeToggle);
|
|
3396
|
-
if (chapterId && volume) await moveChapterInTree(chapterId, volume.id, volume.chapters.length);
|
|
3624
|
+
if (chapterId && volume) await moveChapterInTree(chapterId, volume.id, Number(volume.chapterCount ?? volume.chapters.length));
|
|
3397
3625
|
});
|
|
3398
3626
|
}
|
|
3399
3627
|
});
|
|
@@ -3662,7 +3890,11 @@ async function deleteChapter(chapterId) {
|
|
|
3662
3890
|
async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
3663
3891
|
if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) return;
|
|
3664
3892
|
cancelChapterAutoSave();
|
|
3665
|
-
state.chapter
|
|
3893
|
+
if (state.chapter?.id !== chapterId) {
|
|
3894
|
+
state.chapter = await api(`/api/chapters/${chapterId}`);
|
|
3895
|
+
mergeChapterDirectoryEntry(state.chapter);
|
|
3896
|
+
}
|
|
3897
|
+
state.collapsedVolumeIds.delete(state.chapter.volumeId);
|
|
3666
3898
|
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
3667
3899
|
chapterEditorReadOnly = !canEditProse() || !editMode;
|
|
3668
3900
|
state.module = "editor";
|
|
@@ -3730,6 +3962,7 @@ function markActiveModule(module) {
|
|
|
3730
3962
|
}
|
|
3731
3963
|
|
|
3732
3964
|
const moduleMeta = {
|
|
3965
|
+
drafts: ["临时想法", "创作草稿", "记录可能采用、也可能永远不会写入正文或正式设定的想法。", "新建草稿"],
|
|
3733
3966
|
settings: ["世界事实", "世界观与设定库", "锁定的设定会成为 AI 续写与校对的硬约束。", "新建设定"],
|
|
3734
3967
|
characters: ["人物档案", "角色与人物属性", "维护别名、属性、当前状态及不可被 AI 覆盖的字段。", "新建角色"],
|
|
3735
3968
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
@@ -3785,6 +4018,7 @@ async function showModule(module) {
|
|
|
3785
4018
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
3786
4019
|
bindModuleContentInteractions();
|
|
3787
4020
|
try {
|
|
4021
|
+
if (module === "drafts") await renderDrafts();
|
|
3788
4022
|
if (module === "settings") await renderSettings();
|
|
3789
4023
|
if (module === "characters") await renderCharacters(characterListPage);
|
|
3790
4024
|
if (module === "races") await renderRaces();
|
|
@@ -4097,6 +4331,15 @@ function mountRelationshipFilterToggle() {
|
|
|
4097
4331
|
});
|
|
4098
4332
|
}
|
|
4099
4333
|
|
|
4334
|
+
function mountDraftFilterToggle() {
|
|
4335
|
+
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.remove();
|
|
4336
|
+
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="draft-filter-toggle" aria-label="筛选草稿" aria-controls="draft-filter-panel" aria-expanded="${draftFiltersPanelOpen}" title="筛选草稿"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16l-6.5 7.2v5.3l-3 1.5v-6.8L4 5Z"></path></svg></button>`);
|
|
4337
|
+
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.addEventListener("click", async () => {
|
|
4338
|
+
draftFiltersPanelOpen = !draftFiltersPanelOpen;
|
|
4339
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4340
|
+
});
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4100
4343
|
function bindRecordPreview(selector, open) {
|
|
4101
4344
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
4102
4345
|
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
@@ -4185,6 +4428,150 @@ function settingRecordActions(item) {
|
|
|
4185
4428
|
: recordHistoryButton("setting", item.id, item.title);
|
|
4186
4429
|
}
|
|
4187
4430
|
|
|
4431
|
+
function draftTypeLabel(draftType) {
|
|
4432
|
+
return draftType === "setting" ? "设定草稿" : "正文草稿";
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
async function deleteDraft(item) {
|
|
4436
|
+
if (!item || !canEditModule("drafts")) return;
|
|
4437
|
+
const dialog = $("#form-dialog");
|
|
4438
|
+
dialog.close();
|
|
4439
|
+
if (!await confirmToast(`确认删除草稿“${item.title}”吗?草稿将从当前列表移除。`, {
|
|
4440
|
+
title: "删除草稿",
|
|
4441
|
+
confirmLabel: "确认删除"
|
|
4442
|
+
})) {
|
|
4443
|
+
openDraftDialog(item);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
try {
|
|
4447
|
+
await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
4448
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4449
|
+
toast("草稿已删除");
|
|
4450
|
+
} catch (error) {
|
|
4451
|
+
toast(error.message, "error");
|
|
4452
|
+
try {
|
|
4453
|
+
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(item.id)}`));
|
|
4454
|
+
} catch (reloadError) {
|
|
4455
|
+
toast(reloadError.message, "error");
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
4461
|
+
const viewOnly = readOnly || !canEditModule("drafts");
|
|
4462
|
+
const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="草稿操作">
|
|
4463
|
+
<div><strong>草稿操作</strong><small>删除后将从草稿列表移除,版本历史仍会保留。</small></div>
|
|
4464
|
+
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除草稿</button></div>
|
|
4465
|
+
</section>` : "";
|
|
4466
|
+
const fields = `<p class="form-field-note">草稿只记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。</p>`
|
|
4467
|
+
+ field("draftType", "草稿类型", "select", item?.draftType ?? "prose", [["prose", "正文草稿"], ["setting", "设定草稿"]])
|
|
4468
|
+
+ field("title", "标题", "text", item?.title ?? "")
|
|
4469
|
+
+ field("content", "内容", "markdown", item?.content ?? "", {
|
|
4470
|
+
placeholder: "记录尚未定稿的片段、方向或设定想法……",
|
|
4471
|
+
readOnly: viewOnly
|
|
4472
|
+
}) + management;
|
|
4473
|
+
openDialog(item ? viewOnly ? "查看草稿" : "编辑草稿" : "新建草稿", fields, async (form) => {
|
|
4474
|
+
if (viewOnly) return;
|
|
4475
|
+
const title = String(form.get("title") ?? "").trim();
|
|
4476
|
+
if (!title) throw new Error("请填写草稿标题");
|
|
4477
|
+
const body = {
|
|
4478
|
+
draftType: form.get("draftType"),
|
|
4479
|
+
title,
|
|
4480
|
+
content: String(form.get("content") ?? ""),
|
|
4481
|
+
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
4482
|
+
};
|
|
4483
|
+
await api(item ? `/api/drafts/${encodeURIComponent(item.id)}` : `/api/works/${encodeURIComponent(state.work.id)}/drafts`, {
|
|
4484
|
+
method: item ? "PATCH" : "POST",
|
|
4485
|
+
body
|
|
4486
|
+
});
|
|
4487
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4488
|
+
toast(item ? "草稿已保存" : "草稿已创建");
|
|
4489
|
+
}, item ? draftTypeLabel(item.draftType) : "未确认想法", {
|
|
4490
|
+
submitLabel: viewOnly ? "关闭" : "保存草稿",
|
|
4491
|
+
hideCancel: viewOnly,
|
|
4492
|
+
editor: true,
|
|
4493
|
+
errorPrefix: "草稿保存失败:"
|
|
4494
|
+
});
|
|
4495
|
+
if (viewOnly) {
|
|
4496
|
+
$("#dialog-fields").querySelectorAll("input, select, textarea").forEach((control) => {
|
|
4497
|
+
if (control instanceof HTMLSelectElement) control.disabled = true;
|
|
4498
|
+
else control.readOnly = true;
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
$("#dialog-fields").querySelector("[data-dialog-draft-delete]")?.addEventListener("click", () => {
|
|
4502
|
+
void deleteDraft(item);
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
async function renderDrafts(page = moduleListPages.drafts) {
|
|
4507
|
+
const allDrafts = await moduleApiAllPages("drafts", `/api/works/${state.work.id}/drafts`);
|
|
4508
|
+
const drafts = draftTypeFilter === "all"
|
|
4509
|
+
? allDrafts
|
|
4510
|
+
: allDrafts.filter((draft) => draft.draftType === draftTypeFilter);
|
|
4511
|
+
mountModuleCount(drafts.length);
|
|
4512
|
+
const pageResult = paginateModuleItems(drafts, page, "drafts");
|
|
4513
|
+
moduleListPages.drafts = pageResult.page;
|
|
4514
|
+
const layout = readModuleLayout();
|
|
4515
|
+
if (drafts.length) mountModuleLayoutToggle(layout, "草稿列表样式");
|
|
4516
|
+
else $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
4517
|
+
mountDraftFilterToggle();
|
|
4518
|
+
const filterToolbar = `<section id="draft-filter-panel" class="draft-filter-toolbar${draftFiltersPanelOpen ? "" : " hidden"}" aria-label="草稿筛选">
|
|
4519
|
+
<label for="draft-type-filter">草稿类型</label>
|
|
4520
|
+
<select id="draft-type-filter" aria-label="按草稿类型筛选">
|
|
4521
|
+
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}>全部草稿</option>
|
|
4522
|
+
<option value="prose" ${draftTypeFilter === "prose" ? "selected" : ""}>正文草稿</option>
|
|
4523
|
+
<option value="setting" ${draftTypeFilter === "setting" ? "selected" : ""}>设定草稿</option>
|
|
4524
|
+
</select>
|
|
4525
|
+
${draftTypeFilter === "all" ? "" : `<span aria-live="polite">筛选后剩余 ${drafts.length} 篇草稿</span>`}
|
|
4526
|
+
</section>`;
|
|
4527
|
+
const actions = (item) => canEditModule("drafts")
|
|
4528
|
+
? `${recordCardEditButton("edit-draft", item.id, `草稿“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
4529
|
+
: recordHistoryButton("draft", item.id, item.title);
|
|
4530
|
+
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
4531
|
+
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看草稿 ${esc(item.title)}">
|
|
4532
|
+
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4533
|
+
<h3>${esc(item.title)}</h3>
|
|
4534
|
+
<div class="record-markdown-preview">${esc(item.contentPreview || "暂无内容")}</div>
|
|
4535
|
+
<div class="card-actions">${actions(item)}</div>
|
|
4536
|
+
</article>`).join("")}</div>`;
|
|
4537
|
+
const rows = `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
4538
|
+
const preview = moduleRowPreview(item.contentPreview || "暂无内容");
|
|
4539
|
+
return `<article class="record-card module-row preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看草稿 ${esc(item.title)}">
|
|
4540
|
+
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4541
|
+
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
4542
|
+
<div class="card-actions">${actions(item)}</div>
|
|
4543
|
+
</article>`;
|
|
4544
|
+
}).join("")}</div>`;
|
|
4545
|
+
const emptyDrafts = allDrafts.length
|
|
4546
|
+
? emptyModule("没有符合筛选条件的草稿", "可以切换草稿类型查看其他想法。")
|
|
4547
|
+
: emptyModule("还没有草稿", "把不一定会进入正文或设定的片段、方向和备选想法先记在这里。");
|
|
4548
|
+
$("#module-content").innerHTML = filterToolbar + (drafts.length
|
|
4549
|
+
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "草稿列表")}`
|
|
4550
|
+
: emptyDrafts);
|
|
4551
|
+
$("#draft-type-filter").addEventListener("change", async (event) => {
|
|
4552
|
+
draftTypeFilter = ["prose", "setting"].includes(event.currentTarget.value) ? event.currentTarget.value : "all";
|
|
4553
|
+
draftFiltersPanelOpen = true;
|
|
4554
|
+
moduleListPages.drafts = 1;
|
|
4555
|
+
await renderDrafts(1);
|
|
4556
|
+
});
|
|
4557
|
+
bindModuleLayoutToggle(() => renderDrafts(pageResult.page));
|
|
4558
|
+
bindModulePagination("drafts", renderDrafts);
|
|
4559
|
+
$("#module-content").querySelectorAll("[data-open-draft]").forEach((card) => {
|
|
4560
|
+
const open = async () => openDraftDialog(await api(`/api/drafts/${encodeURIComponent(card.dataset.openDraft)}`), { readOnly: true });
|
|
4561
|
+
card.addEventListener("click", (event) => { if (!event.target.closest("button, a")) void open(); });
|
|
4562
|
+
card.addEventListener("keydown", (event) => {
|
|
4563
|
+
if ((event.key === "Enter" || event.key === " ") && !event.target.closest("button, a")) {
|
|
4564
|
+
event.preventDefault();
|
|
4565
|
+
void open();
|
|
4566
|
+
}
|
|
4567
|
+
});
|
|
4568
|
+
});
|
|
4569
|
+
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
4570
|
+
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
4571
|
+
}));
|
|
4572
|
+
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4188
4575
|
function renderSettingCards(records) {
|
|
4189
4576
|
return `<div class="card-grid">${records.map((item) => `
|
|
4190
4577
|
<article class="record-card preview-record-card" data-open-setting="${esc(item.id)}" role="button" tabindex="0" aria-label="查看设定 ${esc(item.title)}"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(settingStatusLabel(item.status))}</small>
|
|
@@ -4203,7 +4590,7 @@ function renderSettingRows(records) {
|
|
|
4203
4590
|
}
|
|
4204
4591
|
|
|
4205
4592
|
async function renderSettings(page = moduleListPages.settings) {
|
|
4206
|
-
const records = await
|
|
4593
|
+
const records = await moduleApiAllPages("settings", `/api/works/${state.work.id}/settings`);
|
|
4207
4594
|
state.settings = records;
|
|
4208
4595
|
mountModuleCount(records.length);
|
|
4209
4596
|
const pageResult = paginateModuleItems(records, page, "settings");
|
|
@@ -4224,9 +4611,11 @@ async function renderCharacters(page = characterListPage) {
|
|
|
4224
4611
|
const hasCharacterFilters = characterFilters.raceIds.length > 0 || characterFilters.organizationIds.length > 0;
|
|
4225
4612
|
const pageSize = pageSizeFor("characters");
|
|
4226
4613
|
const [characterSource, races, organizations] = await Promise.all([
|
|
4227
|
-
hasCharacterFilters
|
|
4228
|
-
|
|
4229
|
-
|
|
4614
|
+
hasCharacterFilters
|
|
4615
|
+
? moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
|
|
4616
|
+
: moduleApiPage("characters", `/api/works/${state.work.id}/characters`, page, pageSize),
|
|
4617
|
+
canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4618
|
+
canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
4230
4619
|
]);
|
|
4231
4620
|
const characterPage = hasCharacterFilters
|
|
4232
4621
|
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
|
|
@@ -4402,7 +4791,7 @@ async function renderRaces() {
|
|
|
4402
4791
|
const workId = state.work.id;
|
|
4403
4792
|
const generation = workScopedUiGeneration;
|
|
4404
4793
|
const requestId = ++raceListRequestId;
|
|
4405
|
-
const roots = await
|
|
4794
|
+
const roots = await moduleApi("races", `/api/works/${workId}/races?scope=roots`);
|
|
4406
4795
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4407
4796
|
state.races = roots.items;
|
|
4408
4797
|
loadedRaceHierarchyWorkId = roots.items.length === roots.total ? workId : null;
|
|
@@ -4410,7 +4799,7 @@ async function renderRaces() {
|
|
|
4410
4799
|
if (loadedRaceHierarchyWorkId === workId) return;
|
|
4411
4800
|
|
|
4412
4801
|
const dismissLoadingToast = persistentToast("正在加载子种族……");
|
|
4413
|
-
const loadPromise =
|
|
4802
|
+
const loadPromise = moduleApi("races", `/api/works/${workId}/races?scope=descendants`).then((descendants) => {
|
|
4414
4803
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== raceListRequestId) return;
|
|
4415
4804
|
state.races = [...roots.items, ...descendants];
|
|
4416
4805
|
loadedRaceHierarchyWorkId = workId;
|
|
@@ -4434,8 +4823,8 @@ async function renderRaces() {
|
|
|
4434
4823
|
|
|
4435
4824
|
async function renderOrganizations(page = moduleListPages.organizations) {
|
|
4436
4825
|
[state.organizations, state.characters] = await Promise.all([
|
|
4437
|
-
|
|
4438
|
-
canReadModule("characters") ?
|
|
4826
|
+
moduleApiAllPages("organizations", `/api/works/${state.work.id}/organizations`),
|
|
4827
|
+
canReadModule("characters") ? moduleApiAllPages("organizations", `/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
4439
4828
|
]);
|
|
4440
4829
|
mountModuleCount(state.organizations.length);
|
|
4441
4830
|
const pageResult = paginateModuleItems(state.organizations, page, "organizations");
|
|
@@ -4501,8 +4890,8 @@ function setTimelineMultiSelectMode(enabled) {
|
|
|
4501
4890
|
|
|
4502
4891
|
async function renderTimeline(page = moduleListPages.timeline) {
|
|
4503
4892
|
const [events, tracks] = await Promise.all([
|
|
4504
|
-
|
|
4505
|
-
|
|
4893
|
+
moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline`),
|
|
4894
|
+
moduleApiAllPages("timeline", `/api/works/${state.work.id}/timeline-tracks`)
|
|
4506
4895
|
]);
|
|
4507
4896
|
mountModuleCount(events.length);
|
|
4508
4897
|
const pageResult = paginateModuleItems(events, page, "timeline");
|
|
@@ -4545,8 +4934,8 @@ async function renderTimeline(page = moduleListPages.timeline) {
|
|
|
4545
4934
|
async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foreshadowPage = moduleListPages.foreshadows) {
|
|
4546
4935
|
const currentChapterId = state.chapter?.id;
|
|
4547
4936
|
const [outlines, foreshadows] = await Promise.all([
|
|
4548
|
-
|
|
4549
|
-
|
|
4937
|
+
moduleApiAllPages("outlines", `/api/works/${state.work.id}/outlines`),
|
|
4938
|
+
moduleApiAllPages("outlines", `/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`)
|
|
4550
4939
|
]);
|
|
4551
4940
|
mountModuleCount(outlines.length + foreshadows.length);
|
|
4552
4941
|
const outlinePageResult = paginateModuleItems(outlines, outlinePage, "outlines");
|
|
@@ -4599,8 +4988,8 @@ async function renderOutlines(outlinePage = moduleListPages.outlinePlans, foresh
|
|
|
4599
4988
|
}
|
|
4600
4989
|
|
|
4601
4990
|
async function renderRelationships(page = moduleListPages.relationships) {
|
|
4602
|
-
state.characters = canReadModule("characters") ? await
|
|
4603
|
-
const relationships = await
|
|
4991
|
+
state.characters = canReadModule("characters") ? await moduleApiAllPages("relationships", `/api/works/${state.work.id}/characters`) : [];
|
|
4992
|
+
const relationships = await moduleApiAllPages("relationships", `/api/works/${state.work.id}/relationships`);
|
|
4604
4993
|
const filteredRelationships = filterRelationships(relationships, relationshipFilters);
|
|
4605
4994
|
const pageResult = paginateModuleItems(filteredRelationships, page, "relationships");
|
|
4606
4995
|
moduleListPages.relationships = pageResult.page;
|
|
@@ -4674,8 +5063,8 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
4674
5063
|
const canMergeCharacters = canResolveReview
|
|
4675
5064
|
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
4676
5065
|
const [reviews, characters] = await Promise.all([
|
|
4677
|
-
|
|
4678
|
-
canReadCharacters ?
|
|
5066
|
+
moduleApiAllPages("reviews", `/api/works/${state.work.id}/reviews`),
|
|
5067
|
+
canReadCharacters ? moduleApiAllPages("reviews", `/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
4679
5068
|
]);
|
|
4680
5069
|
mountModuleCount(reviews.length);
|
|
4681
5070
|
const pageResult = paginateModuleItems(reviews, page, "reviews");
|
|
@@ -4763,16 +5152,16 @@ async function renderReviews(page = moduleListPages.reviews) {
|
|
|
4763
5152
|
}));
|
|
4764
5153
|
}
|
|
4765
5154
|
|
|
4766
|
-
async function renderTasks(page = taskListPage) {
|
|
5155
|
+
async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
4767
5156
|
stopTaskProgressRefresh();
|
|
4768
5157
|
const pageSize = pageSizeFor("analysisTasks");
|
|
4769
5158
|
const [taskPage, settings] = await Promise.all([
|
|
4770
|
-
|
|
5159
|
+
moduleApiPage("tasks", `/api/works/${state.work.id}/tasks`, page, pageSize, { refresh }),
|
|
4771
5160
|
canReadModule("ai-settings")
|
|
4772
|
-
?
|
|
5161
|
+
? moduleApi("tasks", `/api/works/${state.work.id}/ai-settings`, { refresh })
|
|
4773
5162
|
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunDailyTaskLimit: 0, autoRunFailureThreshold: 3, autoRunPaused: false })
|
|
4774
5163
|
]);
|
|
4775
|
-
if (!taskPage.items.length && page > 1) return renderTasks(page - 1);
|
|
5164
|
+
if (!taskPage.items.length && page > 1) return renderTasks(page - 1, { refresh });
|
|
4776
5165
|
taskListPage = taskPage.page;
|
|
4777
5166
|
const tasks = taskPage.items;
|
|
4778
5167
|
const taskTotal = Number(taskPage.total ?? taskPage.stats?.total ?? tasks.length);
|
|
@@ -4803,13 +5192,15 @@ async function renderTasks(page = taskListPage) {
|
|
|
4803
5192
|
: "";
|
|
4804
5193
|
$("#module-content").innerHTML = `
|
|
4805
5194
|
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
4806
|
-
<div class="task-auto-run-
|
|
4807
|
-
<
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
5195
|
+
<div class="task-auto-run-header">
|
|
5196
|
+
<div class="task-auto-run-copy">
|
|
5197
|
+
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
5198
|
+
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
5199
|
+
<small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
|
|
5200
|
+
</div>
|
|
5201
|
+
<div class="task-auto-run-actions">
|
|
5202
|
+
${autoRunEditing ? "" : '<button id="task-auto-run-edit" class="ghost-button" type="button">编辑</button>'}
|
|
5203
|
+
</div>
|
|
4813
5204
|
</div>
|
|
4814
5205
|
<div class="task-auto-run-controls">
|
|
4815
5206
|
<label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""} ${autoRunEditing ? "" : "disabled"}><span>持续自动执行</span></label>
|
|
@@ -4881,7 +5272,7 @@ async function renderTasks(page = taskListPage) {
|
|
|
4881
5272
|
: "自动执行已关闭");
|
|
4882
5273
|
taskAutoRunEditing = false;
|
|
4883
5274
|
taskAutoRunEditingWorkId = null;
|
|
4884
|
-
await renderTasks();
|
|
5275
|
+
await renderTasks(taskListPage, { refresh: true });
|
|
4885
5276
|
} catch (error) {
|
|
4886
5277
|
toast(error.message, "error");
|
|
4887
5278
|
button.disabled = false;
|
|
@@ -5040,7 +5431,7 @@ function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
|
5040
5431
|
return;
|
|
5041
5432
|
}
|
|
5042
5433
|
try {
|
|
5043
|
-
await renderTasks();
|
|
5434
|
+
await renderTasks(taskListPage, { refresh: true });
|
|
5044
5435
|
} catch (error) {
|
|
5045
5436
|
console.error("Failed to refresh task progress", error);
|
|
5046
5437
|
scheduleTaskProgressRefresh(workId, runningCount);
|
|
@@ -5920,16 +6311,16 @@ async function renderBookAiSettings() {
|
|
|
5920
6311
|
relationshipSearchIndexRefreshTimer = null;
|
|
5921
6312
|
}
|
|
5922
6313
|
const [settings, providers, models, taskDefaults, relationshipIndex, usage] = await Promise.all([
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
6314
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings`),
|
|
6315
|
+
moduleApi("ai-settings", "/api/platform/ai/providers"),
|
|
6316
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/models`),
|
|
6317
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/task-defaults`),
|
|
6318
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/relationship-search-index`),
|
|
6319
|
+
moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/usage?timezoneOffset=${-new Date().getTimezoneOffset()}`)
|
|
5929
6320
|
]);
|
|
5930
6321
|
const host = $("#module-content");
|
|
5931
6322
|
const workId = String(state.work.id);
|
|
5932
|
-
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"]);
|
|
6323
|
+
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"]);
|
|
5933
6324
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
5934
6325
|
title: "本书 Token 用量",
|
|
5935
6326
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
@@ -5943,6 +6334,10 @@ async function renderBookAiSettings() {
|
|
|
5943
6334
|
"afterend",
|
|
5944
6335
|
`<label><input name="agent-tool" type="checkbox" value="read_character_sections" ${agentTools.has("read_character_sections") ? "checked" : ""}><span><strong>读取人物 Markdown 章节</strong><small>根据知识查询返回的章节 ID 精读人物背景、能力与经历原文。</small></span></label>`
|
|
5945
6336
|
);
|
|
6337
|
+
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
6338
|
+
"beforeend",
|
|
6339
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索草稿</strong><small>查询正文草稿和设定草稿。草稿只是可能采用、也可能永远不会进入正文或正式设定的临时想法,Agent 不会把它当作已确认事实。</small></span></label>`
|
|
6340
|
+
);
|
|
5946
6341
|
if (!canEditModule("ai-settings")) {
|
|
5947
6342
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
5948
6343
|
host.querySelectorAll(".config-save-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
@@ -6256,7 +6651,7 @@ async function ensureAiReferencesLoaded() {
|
|
|
6256
6651
|
|
|
6257
6652
|
function field(name, label, type = "text", value = "", options = []) {
|
|
6258
6653
|
if (type === "textarea") return `<label>${esc(label)}<textarea name="${esc(name)}">${esc(value)}</textarea></label>`;
|
|
6259
|
-
if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><div class="vditor-editor-host" data-vditor-editor data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="Markdown 原文">${esc(value)}</textarea></div>`;
|
|
6654
|
+
if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><span>${esc(label)}</span><div class="vditor-editor-host" data-vditor-editor data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="${esc(label)} Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="${esc(label)} Markdown 原文" ${options.readOnly ? "readonly" : ""}>${esc(value)}</textarea></div>`;
|
|
6260
6655
|
if (type === "item-list") {
|
|
6261
6656
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
6262
6657
|
return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
|
|
@@ -6426,6 +6821,8 @@ function commitRelationshipKeywordInputs(container) {
|
|
|
6426
6821
|
}
|
|
6427
6822
|
|
|
6428
6823
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
6824
|
+
formDialogVditors.forEach(destroyVditorEditor);
|
|
6825
|
+
formDialogVditors = [];
|
|
6429
6826
|
void discardPendingMarkdownAttachments();
|
|
6430
6827
|
const dialog = $("#form-dialog");
|
|
6431
6828
|
const form = $("#dynamic-form");
|
|
@@ -6448,9 +6845,11 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
6448
6845
|
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
6449
6846
|
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
6450
6847
|
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
6848
|
+
dialog.classList.toggle("large-dialog", Boolean(options.large));
|
|
6849
|
+
dialog.classList.toggle("editor-dialog", Boolean(options.editor));
|
|
6451
6850
|
bindDynamicListControls($("#dialog-fields"));
|
|
6452
6851
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
6453
|
-
bindVditorEditors($("#dialog-fields"));
|
|
6852
|
+
formDialogVditors = bindVditorEditors($("#dialog-fields"));
|
|
6454
6853
|
form.onclick = null;
|
|
6455
6854
|
form.onkeydown = null;
|
|
6456
6855
|
dialog.oncancel = (event) => {
|
|
@@ -7224,6 +7623,7 @@ function destroyVditorEditor(editor) {
|
|
|
7224
7623
|
}
|
|
7225
7624
|
|
|
7226
7625
|
function bindVditorEditors(container) {
|
|
7626
|
+
const editors = [];
|
|
7227
7627
|
container.querySelectorAll("[data-vditor-editor]").forEach((host) => {
|
|
7228
7628
|
const valueField = host.parentElement?.querySelector("[data-vditor-value]");
|
|
7229
7629
|
const editor = createVditorEditor(host, valueField?.value ?? "", {
|
|
@@ -7231,10 +7631,12 @@ function bindVditorEditors(container) {
|
|
|
7231
7631
|
if (valueField) valueField.value = markdown;
|
|
7232
7632
|
markEntityEditorDirty();
|
|
7233
7633
|
},
|
|
7234
|
-
placeholder: "",
|
|
7634
|
+
placeholder: host.dataset.placeholder ?? "",
|
|
7235
7635
|
readOnly: Boolean(valueField?.readOnly)
|
|
7236
7636
|
});
|
|
7637
|
+
if (editor) editors.push(editor);
|
|
7237
7638
|
});
|
|
7639
|
+
return editors;
|
|
7238
7640
|
}
|
|
7239
7641
|
|
|
7240
7642
|
function characterSectionImageLabel(file, fallback = "图片附件") {
|
|
@@ -7945,49 +8347,151 @@ function openOutlineDialog(item) {
|
|
|
7945
8347
|
}, "章节大纲");
|
|
7946
8348
|
}
|
|
7947
8349
|
|
|
8350
|
+
const foreshadowOccurrenceRoleOptions = [["setup", "埋设"], ["reminder", "提醒"], ["payoff", "回收"]];
|
|
8351
|
+
|
|
8352
|
+
function foreshadowOccurrenceRow(record, chapters, index) {
|
|
8353
|
+
const chapterOptions = [["", "暂不关联"], ...chapters];
|
|
8354
|
+
const occurrenceId = record?.id ?? "";
|
|
8355
|
+
const role = record?.role ?? "setup";
|
|
8356
|
+
const chapterId = record?.chapterId ?? "";
|
|
8357
|
+
const chapterLabel = chapters.find(([value]) => value === chapterId)?.[1] ?? "未关联章节";
|
|
8358
|
+
return `<fieldset class="foreshadow-occurrence-row" data-foreshadow-occurrence data-occurrence-id="${esc(occurrenceId)}" data-occurrence-role="${esc(role)}">
|
|
8359
|
+
<legend><span class="foreshadow-occurrence-role-badge" data-foreshadow-occurrence-role-label>${esc(occurrenceRoleLabel(role))}</span><strong data-foreshadow-occurrence-index>节点 ${index + 1}</strong><small data-foreshadow-occurrence-summary>${esc(chapterLabel)}</small></legend>
|
|
8360
|
+
<button class="foreshadow-occurrence-remove" type="button" data-foreshadow-occurrence-remove aria-label="删除第 ${index + 1} 个章节节点">删除节点</button>
|
|
8361
|
+
<div class="foreshadow-occurrence-grid">
|
|
8362
|
+
<label>节点类型<select name="occurrenceRole" data-foreshadow-occurrence-role aria-label="第 ${index + 1} 个节点类型">${foreshadowOccurrenceRoleOptions.map(([value, label]) => `<option value="${value}" ${value === role ? "selected" : ""}>${label}</option>`).join("")}</select></label>
|
|
8363
|
+
<label>关联章节<select name="occurrenceChapterId" data-foreshadow-occurrence-chapter aria-label="第 ${index + 1} 个关联章节">${chapterOptions.map(([value, label]) => `<option value="${esc(value)}" ${value === chapterId ? "selected" : ""}>${esc(label)}</option>`).join("")}</select></label>
|
|
8364
|
+
</div>
|
|
8365
|
+
<label>节点说明<textarea name="occurrenceNote" data-foreshadow-occurrence-note aria-label="第 ${index + 1} 个节点说明">${esc(record?.note ?? "")}</textarea></label>
|
|
8366
|
+
</fieldset>`;
|
|
8367
|
+
}
|
|
8368
|
+
|
|
8369
|
+
function foreshadowOccurrencesField(item, chapters) {
|
|
8370
|
+
const occurrences = item?.occurrences?.length ? item.occurrences : [null];
|
|
8371
|
+
return `<section class="form-field foreshadow-editor-section foreshadow-occurrence-field" data-foreshadow-occurrences aria-labelledby="foreshadow-occurrence-title">
|
|
8372
|
+
<div class="foreshadow-occurrence-toolbar"><div><h3 id="foreshadow-occurrence-title">章节节点</h3><p>每张卡片代表一次独立的埋设、提醒或回收,类型、章节和说明始终绑定在同一组内。</p></div><button type="button" data-foreshadow-occurrence-add>添加节点</button></div>
|
|
8373
|
+
<div class="foreshadow-occurrence-rows" data-foreshadow-occurrence-rows>${occurrences.map((record, index) => foreshadowOccurrenceRow(record, chapters, index)).join("")}</div>
|
|
8374
|
+
<p class="foreshadow-occurrence-empty hidden" data-foreshadow-occurrence-empty>尚未添加章节节点。</p>
|
|
8375
|
+
</section>`;
|
|
8376
|
+
}
|
|
8377
|
+
|
|
8378
|
+
function foreshadowOverviewField(item, options, firstPayoff) {
|
|
8379
|
+
return `<section class="foreshadow-editor-section foreshadow-overview-field" aria-labelledby="foreshadow-overview-title">
|
|
8380
|
+
<header><h3 id="foreshadow-overview-title">伏笔信息</h3><p>维护伏笔本身的内容、状态与计划回收位置。</p></header>
|
|
8381
|
+
<div class="foreshadow-overview-grid">
|
|
8382
|
+
${field("title", "伏笔名称", "text", item?.title)}
|
|
8383
|
+
${field("importance", "重要程度", "select", item?.importance ?? "medium", [["low", "低"], ["medium", "中"], ["high", "高"]])}
|
|
8384
|
+
${field("status", "状态", "select", item?.status ?? "planned", [["planned", "计划中"], ["planted", "已埋设"], ["resolved", "已回收"], ["abandoned", "已放弃"]])}
|
|
8385
|
+
${field("plannedPayoffChapterId", "计划回收章节", "select", item?.plannedPayoffChapterId ?? firstPayoff?.chapterId ?? "", options)}
|
|
8386
|
+
</div>
|
|
8387
|
+
<div class="foreshadow-overview-description">${field("description", "内容与预期作用", "textarea", item?.description)}</div>
|
|
8388
|
+
</section>`;
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8391
|
+
function foreshadowResolutionField(item) {
|
|
8392
|
+
return `<section class="foreshadow-editor-section foreshadow-resolution-field" aria-labelledby="foreshadow-resolution-title">
|
|
8393
|
+
<header><h3 id="foreshadow-resolution-title">回收结果</h3><p>记录伏笔最终如何揭示或完成;它不属于上方任意单个章节节点。</p></header>
|
|
8394
|
+
${field("resolutionNote", "回收结论", "textarea", item?.resolutionNote)}
|
|
8395
|
+
</section>`;
|
|
8396
|
+
}
|
|
8397
|
+
|
|
8398
|
+
function updateForeshadowOccurrenceRowSummary(row) {
|
|
8399
|
+
const role = row.querySelector("[data-foreshadow-occurrence-role]").value;
|
|
8400
|
+
const chapter = row.querySelector("[data-foreshadow-occurrence-chapter]");
|
|
8401
|
+
row.dataset.occurrenceRole = role;
|
|
8402
|
+
row.querySelector("[data-foreshadow-occurrence-role-label]").textContent = occurrenceRoleLabel(role);
|
|
8403
|
+
row.querySelector("[data-foreshadow-occurrence-summary]").textContent = chapter.value ? chapter.selectedOptions[0]?.textContent ?? "已关联章节" : "未关联章节";
|
|
8404
|
+
}
|
|
8405
|
+
|
|
8406
|
+
function renumberForeshadowOccurrenceRows(section) {
|
|
8407
|
+
const rows = [...section.querySelectorAll("[data-foreshadow-occurrence]")];
|
|
8408
|
+
rows.forEach((row, index) => {
|
|
8409
|
+
const sequence = index + 1;
|
|
8410
|
+
row.querySelector("[data-foreshadow-occurrence-index]").textContent = `节点 ${sequence}`;
|
|
8411
|
+
row.querySelector("[data-foreshadow-occurrence-remove]").setAttribute("aria-label", `删除第 ${sequence} 个章节节点`);
|
|
8412
|
+
row.querySelector("[data-foreshadow-occurrence-role]").setAttribute("aria-label", `第 ${sequence} 个节点类型`);
|
|
8413
|
+
row.querySelector("[data-foreshadow-occurrence-chapter]").setAttribute("aria-label", `第 ${sequence} 个关联章节`);
|
|
8414
|
+
row.querySelector("[data-foreshadow-occurrence-note]").setAttribute("aria-label", `第 ${sequence} 个节点说明`);
|
|
8415
|
+
updateForeshadowOccurrenceRowSummary(row);
|
|
8416
|
+
});
|
|
8417
|
+
section.querySelector("[data-foreshadow-occurrence-empty]").classList.toggle("hidden", rows.length > 0);
|
|
8418
|
+
}
|
|
8419
|
+
|
|
8420
|
+
function bindForeshadowOccurrenceControls(container, chapters) {
|
|
8421
|
+
const section = container.querySelector("[data-foreshadow-occurrences]");
|
|
8422
|
+
if (!section) return;
|
|
8423
|
+
const rows = section.querySelector("[data-foreshadow-occurrence-rows]");
|
|
8424
|
+
section.addEventListener("change", (event) => {
|
|
8425
|
+
const row = event.target.closest("[data-foreshadow-occurrence]");
|
|
8426
|
+
if (row && event.target.matches("[data-foreshadow-occurrence-role], [data-foreshadow-occurrence-chapter]")) updateForeshadowOccurrenceRowSummary(row);
|
|
8427
|
+
});
|
|
8428
|
+
section.addEventListener("click", (event) => {
|
|
8429
|
+
if (event.target.closest("[data-foreshadow-occurrence-add]")) {
|
|
8430
|
+
const index = rows.querySelectorAll("[data-foreshadow-occurrence]").length;
|
|
8431
|
+
rows.insertAdjacentHTML("beforeend", foreshadowOccurrenceRow(null, chapters, index));
|
|
8432
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8433
|
+
rows.lastElementChild?.querySelector("[data-foreshadow-occurrence-role]")?.focus();
|
|
8434
|
+
return;
|
|
8435
|
+
}
|
|
8436
|
+
const remove = event.target.closest("[data-foreshadow-occurrence-remove]");
|
|
8437
|
+
if (!remove) return;
|
|
8438
|
+
const row = remove.closest("[data-foreshadow-occurrence]");
|
|
8439
|
+
const nextFocus = row.nextElementSibling?.querySelector("[data-foreshadow-occurrence-role]")
|
|
8440
|
+
?? row.previousElementSibling?.querySelector("[data-foreshadow-occurrence-role]")
|
|
8441
|
+
?? section.querySelector("[data-foreshadow-occurrence-add]");
|
|
8442
|
+
row.remove();
|
|
8443
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8444
|
+
nextFocus?.focus();
|
|
8445
|
+
});
|
|
8446
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8449
|
+
function readForeshadowOccurrences(item) {
|
|
8450
|
+
const previousById = new Map((item?.occurrences ?? []).map((record) => [record.id, record]));
|
|
8451
|
+
const seen = new Map();
|
|
8452
|
+
return [...$("#dialog-fields").querySelectorAll("[data-foreshadow-occurrence]")].flatMap((row, index) => {
|
|
8453
|
+
const role = row.querySelector("[data-foreshadow-occurrence-role]").value;
|
|
8454
|
+
const chapterId = row.querySelector("[data-foreshadow-occurrence-chapter]").value;
|
|
8455
|
+
const note = row.querySelector("[data-foreshadow-occurrence-note]").value;
|
|
8456
|
+
if (!chapterId) {
|
|
8457
|
+
if (note.trim()) throw new Error(`第 ${index + 1} 个章节节点填写了说明,请选择关联章节`);
|
|
8458
|
+
return [];
|
|
8459
|
+
}
|
|
8460
|
+
const key = `${role}:${chapterId}`;
|
|
8461
|
+
const duplicateIndex = seen.get(key);
|
|
8462
|
+
if (duplicateIndex) throw new Error(`第 ${index + 1} 个章节节点与第 ${duplicateIndex} 个重复`);
|
|
8463
|
+
seen.set(key, index + 1);
|
|
8464
|
+
const previous = previousById.get(row.dataset.occurrenceId);
|
|
8465
|
+
return [{
|
|
8466
|
+
chapterId,
|
|
8467
|
+
role,
|
|
8468
|
+
note,
|
|
8469
|
+
evidence: previous?.chapterId === chapterId && previous?.role === role ? previous.evidence : []
|
|
8470
|
+
}];
|
|
8471
|
+
});
|
|
8472
|
+
}
|
|
8473
|
+
|
|
7948
8474
|
function openForeshadowDialog(item) {
|
|
7949
8475
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
|
|
7950
8476
|
if (!chapters.length) return toast("请先创建章节", "error");
|
|
7951
8477
|
const options = [["", "暂不关联"], ...chapters];
|
|
7952
|
-
const
|
|
7953
|
-
const editableOccurrenceIds = new Set(["setup", "reminder", "payoff"].map((role) => occurrence(role)?.id).filter(Boolean));
|
|
7954
|
-
const preservedOccurrences = (item?.occurrences ?? [])
|
|
7955
|
-
.filter((record) => !editableOccurrenceIds.has(record.id))
|
|
7956
|
-
.map((record) => ({ chapterId: record.chapterId, role: record.role, note: record.note, evidence: record.evidence }));
|
|
8478
|
+
const firstPayoff = item?.occurrences?.find((record) => record.role === "payoff");
|
|
7957
8479
|
openDialog(item ? "编辑伏笔" : "新建伏笔",
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
field("status", "状态", "select", item?.status ?? "planned", [["planned", "计划中"], ["planted", "已埋设"], ["resolved", "已回收"], ["abandoned", "已放弃"]]) +
|
|
7962
|
-
field("setupChapterId", "埋设章节", "select", occurrence("setup")?.chapterId ?? "", options) +
|
|
7963
|
-
field("setupNote", "埋设说明", "textarea", occurrence("setup")?.note ?? "") +
|
|
7964
|
-
field("reminderChapterId", "提醒章节", "select", occurrence("reminder")?.chapterId ?? "", options) +
|
|
7965
|
-
field("reminderNote", "提醒说明", "textarea", occurrence("reminder")?.note ?? "") +
|
|
7966
|
-
field("payoffChapterId", "回收章节", "select", occurrence("payoff")?.chapterId ?? item?.plannedPayoffChapterId ?? "", options) +
|
|
7967
|
-
field("payoffNote", "回收说明", "textarea", occurrence("payoff")?.note ?? "") +
|
|
7968
|
-
field("resolutionNote", "回收结论", "textarea", item?.resolutionNote),
|
|
8480
|
+
foreshadowOverviewField(item, options, firstPayoff) +
|
|
8481
|
+
foreshadowOccurrencesField(item, chapters) +
|
|
8482
|
+
foreshadowResolutionField(item),
|
|
7969
8483
|
async (form) => {
|
|
7970
|
-
const
|
|
7971
|
-
const chapterId = form.get(`${role}ChapterId`);
|
|
7972
|
-
if (!chapterId) return [];
|
|
7973
|
-
const previous = occurrence(role);
|
|
7974
|
-
return [{
|
|
7975
|
-
chapterId,
|
|
7976
|
-
role,
|
|
7977
|
-
note: form.get(`${role}Note`),
|
|
7978
|
-
evidence: previous?.chapterId === chapterId ? previous.evidence : []
|
|
7979
|
-
}];
|
|
7980
|
-
});
|
|
7981
|
-
const occurrences = [...preservedOccurrences, ...editedOccurrences];
|
|
8484
|
+
const occurrences = readForeshadowOccurrences(item);
|
|
7982
8485
|
const body = {
|
|
7983
8486
|
title: form.get("title"), description: form.get("description"), importance: form.get("importance"), status: form.get("status"),
|
|
7984
|
-
plannedPayoffChapterId: form.get("
|
|
8487
|
+
plannedPayoffChapterId: form.get("plannedPayoffChapterId") || null, resolutionNote: form.get("resolutionNote"), occurrences,
|
|
7985
8488
|
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
7986
8489
|
};
|
|
7987
8490
|
await api(item ? `/api/foreshadows/${item.id}` : `/api/works/${state.work.id}/foreshadows`, { method: item ? "PATCH" : "POST", body });
|
|
7988
8491
|
await renderOutlines();
|
|
7989
8492
|
toast(item ? "伏笔已更新" : "伏笔已创建");
|
|
7990
|
-
}, item ? "伏笔管理" : "创作线索");
|
|
8493
|
+
}, item ? "伏笔管理" : "创作线索", { large: true, meta: "完整编辑伏笔信息、章节节点与回收结果" });
|
|
8494
|
+
bindForeshadowOccurrenceControls($("#dialog-fields"), chapters);
|
|
7991
8495
|
}
|
|
7992
8496
|
|
|
7993
8497
|
function openTimelineSplitDialog(item) {
|
|
@@ -8460,23 +8964,30 @@ async function sendAi() {
|
|
|
8460
8964
|
let assistantContent = "";
|
|
8461
8965
|
let assistantMessage;
|
|
8462
8966
|
let assistantMetadata = {};
|
|
8967
|
+
let persistedStreamMessage = null;
|
|
8463
8968
|
let suggestion = null;
|
|
8464
8969
|
if (taskType === "chat") {
|
|
8465
8970
|
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId, currentMessageId: persistedUserMessage.id });
|
|
8466
8971
|
assistantContent = streamed.content;
|
|
8467
8972
|
assistantMessage = streamed.message;
|
|
8468
8973
|
assistantMetadata = streamed.metadata;
|
|
8974
|
+
persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
|
|
8469
8975
|
} else {
|
|
8470
8976
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
8471
8977
|
assistantContent = suggestion.content;
|
|
8472
8978
|
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
8473
8979
|
}
|
|
8474
8980
|
try {
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8981
|
+
if (persistedStreamMessage) {
|
|
8982
|
+
updateMessageCreatedAt(assistantMessage, persistedStreamMessage.createdAt);
|
|
8983
|
+
attachMessageIdentity(assistantMessage, persistedStreamMessage.id);
|
|
8984
|
+
} else {
|
|
8985
|
+
const persistedAssistantMessage = await persistAiConversationMessage("assistant", assistantContent, [], assistantMetadata);
|
|
8986
|
+
if (assistantMessage) {
|
|
8987
|
+
updateMessageCreatedAt(assistantMessage, persistedAssistantMessage.createdAt);
|
|
8988
|
+
attachMessageIdentity(assistantMessage, persistedAssistantMessage.id);
|
|
8989
|
+
} else if (suggestion) appendSuggestion(suggestion, persistedAssistantMessage.createdAt, persistedAssistantMessage.id);
|
|
8990
|
+
}
|
|
8480
8991
|
} catch (error) {
|
|
8481
8992
|
if (suggestion) appendSuggestion(suggestion);
|
|
8482
8993
|
toast(`AI 回复已生成,但历史记录保存失败:${error.message}`, "error");
|
|
@@ -8496,16 +9007,28 @@ async function streamChat(body) {
|
|
|
8496
9007
|
const message = document.createElement("div");
|
|
8497
9008
|
message.className = "assistant-message is-streaming";
|
|
8498
9009
|
message.dataset.testid = "ai-stream-message";
|
|
8499
|
-
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite"></div><div class="message-meta">正在连接模型流……</div>';
|
|
9010
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
|
|
8500
9011
|
attachMessageHeading(message, "助手 · 正在生成");
|
|
8501
9012
|
$("#ai-feed").append(message);
|
|
8502
9013
|
scrollAiFeedToBottom();
|
|
8503
9014
|
const content = message.querySelector(".message-body");
|
|
8504
9015
|
const meta = message.querySelector(".message-meta");
|
|
9016
|
+
const typewriter = createStreamTypewriter({
|
|
9017
|
+
onRender: (text, progress) => {
|
|
9018
|
+
content.innerHTML = renderMarkdown(text);
|
|
9019
|
+
const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
|
|
9020
|
+
meta.textContent = progress.pendingCharacters > 0
|
|
9021
|
+
? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
|
|
9022
|
+
: `正在生成 · ${progress.visibleCharacters} 字`;
|
|
9023
|
+
scrollAiFeedToBottom();
|
|
9024
|
+
}
|
|
9025
|
+
});
|
|
8505
9026
|
let streamedText = "";
|
|
8506
9027
|
let generatedMetadata = {};
|
|
8507
9028
|
let toolCalls = [];
|
|
8508
9029
|
let processSteps = [];
|
|
9030
|
+
let persistedMessageId = null;
|
|
9031
|
+
let persistedMessageCreatedAt = null;
|
|
8509
9032
|
let finalAnswerStarted = false;
|
|
8510
9033
|
const processStartedAt = Date.now();
|
|
8511
9034
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
@@ -8523,7 +9046,7 @@ async function streamChat(body) {
|
|
|
8523
9046
|
const decoder = new TextDecoder();
|
|
8524
9047
|
let buffer = "";
|
|
8525
9048
|
let streamError = null;
|
|
8526
|
-
const consume = (eventText) => {
|
|
9049
|
+
const consume = async (eventText) => {
|
|
8527
9050
|
let eventName = "message";
|
|
8528
9051
|
const dataLines = [];
|
|
8529
9052
|
for (const line of eventText.split(/\r?\n/)) {
|
|
@@ -8534,12 +9057,12 @@ async function streamChat(body) {
|
|
|
8534
9057
|
const payload = JSON.parse(dataLines.join("\n"));
|
|
8535
9058
|
if (eventName === "delta") {
|
|
8536
9059
|
const firstFinalDelta = streamedText.length === 0;
|
|
8537
|
-
|
|
9060
|
+
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
9061
|
+
streamedText += delta;
|
|
8538
9062
|
if (streamedText.length > 0) finalAnswerStarted = true;
|
|
8539
|
-
|
|
9063
|
+
typewriter.append(delta);
|
|
8540
9064
|
if (firstFinalDelta && processSteps.length) renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
8541
|
-
meta.textContent =
|
|
8542
|
-
scrollAiFeedToBottom();
|
|
9065
|
+
meta.textContent = "正在生成回复……";
|
|
8543
9066
|
} else if (eventName === "process_step") {
|
|
8544
9067
|
const step = { ...payload };
|
|
8545
9068
|
const append = step.append === true;
|
|
@@ -8560,7 +9083,11 @@ async function streamChat(body) {
|
|
|
8560
9083
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
8561
9084
|
scrollAiFeedToBottom();
|
|
8562
9085
|
} else if (eventName === "complete") {
|
|
9086
|
+
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
9087
|
+
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
9088
|
+
await typewriter.finish();
|
|
8563
9089
|
message.classList.remove("is-streaming");
|
|
9090
|
+
content.setAttribute("aria-busy", "false");
|
|
8564
9091
|
message.querySelector(".message-heading > span").textContent = "助手";
|
|
8565
9092
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
|
|
8566
9093
|
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
|
|
@@ -8579,14 +9106,17 @@ async function streamChat(body) {
|
|
|
8579
9106
|
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
8580
9107
|
const events = buffer.split(/\r?\n\r?\n/);
|
|
8581
9108
|
buffer = events.pop() ?? "";
|
|
8582
|
-
events
|
|
9109
|
+
for (const eventText of events) await consume(eventText);
|
|
8583
9110
|
if (chunk.done) break;
|
|
8584
9111
|
}
|
|
8585
|
-
if (buffer.trim()) consume(buffer);
|
|
9112
|
+
if (buffer.trim()) await consume(buffer);
|
|
9113
|
+
await typewriter.finish();
|
|
8586
9114
|
if (streamError) throw streamError;
|
|
8587
|
-
return { content: streamedText, message, metadata: generatedMetadata };
|
|
9115
|
+
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt };
|
|
8588
9116
|
} catch (error) {
|
|
9117
|
+
typewriter.reveal();
|
|
8589
9118
|
message.classList.remove("is-streaming");
|
|
9119
|
+
content.setAttribute("aria-busy", "false");
|
|
8590
9120
|
message.querySelector(".message-heading > span").textContent = "助手 · 生成中断";
|
|
8591
9121
|
renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
8592
9122
|
meta.textContent = "生成中断";
|
|
@@ -9565,6 +10095,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
9565
10095
|
body: {
|
|
9566
10096
|
toastPosition: $("#toast-position").value,
|
|
9567
10097
|
pageSizes: {
|
|
10098
|
+
drafts: Number($("#page-size-drafts").value),
|
|
9568
10099
|
settings: Number($("#page-size-settings").value),
|
|
9569
10100
|
characters: Number($("#page-size-characters").value),
|
|
9570
10101
|
organizations: Number($("#page-size-organizations").value),
|
|
@@ -9597,6 +10128,9 @@ $("#members-dialog").addEventListener("close", () => {
|
|
|
9597
10128
|
memberDialogDirectory = [];
|
|
9598
10129
|
});
|
|
9599
10130
|
$("#form-dialog").addEventListener("close", () => {
|
|
10131
|
+
formDialogVditors.forEach(destroyVditorEditor);
|
|
10132
|
+
formDialogVditors = [];
|
|
10133
|
+
void discardPendingMarkdownAttachments();
|
|
9600
10134
|
if (relationshipPresenceId && !$("#form-dialog").open) setRelationshipPresence(null);
|
|
9601
10135
|
});
|
|
9602
10136
|
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
@@ -9793,7 +10327,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
9793
10327
|
}
|
|
9794
10328
|
});
|
|
9795
10329
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
9796
|
-
$("#module-create-button").addEventListener("click", () => ({ settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
10330
|
+
$("#module-create-button").addEventListener("click", () => ({ drafts: openDraftDialog, settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
9797
10331
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
9798
10332
|
updateAiMentionMenu();
|
|
9799
10333
|
scheduleAiContextUsage();
|
|
@@ -9934,9 +10468,24 @@ $("#chapter-type-menu").addEventListener("click", async (event) => {
|
|
|
9934
10468
|
toast(error.message, "error");
|
|
9935
10469
|
}
|
|
9936
10470
|
});
|
|
10471
|
+
$("#markdown-table-wrap-toggle").addEventListener("click", () => {
|
|
10472
|
+
const table = markdownTableMenuTarget;
|
|
10473
|
+
if (!table?.isConnected) return closeMarkdownTableMenu();
|
|
10474
|
+
const wrapping = !table.classList.contains("is-wrapping");
|
|
10475
|
+
setMarkdownTableWrapping(table, wrapping);
|
|
10476
|
+
closeMarkdownTableMenu(true);
|
|
10477
|
+
toast(wrapping ? "表格已启用自动换行" : "表格已恢复横向滚动");
|
|
10478
|
+
});
|
|
10479
|
+
document.addEventListener("contextmenu", (event) => {
|
|
10480
|
+
const header = event.target.closest?.("[data-markdown-table-header]");
|
|
10481
|
+
if (!header) return;
|
|
10482
|
+
event.preventDefault();
|
|
10483
|
+
openMarkdownTableMenu(header, event.clientX, event.clientY);
|
|
10484
|
+
});
|
|
9937
10485
|
document.addEventListener("pointerdown", (event) => {
|
|
9938
10486
|
if (!event.target.closest("#chapter-type-menu")) closeChapterTypeMenu();
|
|
9939
10487
|
if (!event.target.closest("#line-citation-menu")) closeLineCitationMenu();
|
|
10488
|
+
if (!event.target.closest("#markdown-table-menu")) closeMarkdownTableMenu();
|
|
9940
10489
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
9941
10490
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
9942
10491
|
$("#account-menu").classList.add("hidden");
|
|
@@ -9948,9 +10497,16 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
9948
10497
|
}
|
|
9949
10498
|
});
|
|
9950
10499
|
document.addEventListener("keydown", (event) => {
|
|
10500
|
+
const header = event.target.closest?.("[data-markdown-table-header]");
|
|
10501
|
+
if (header && (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10"))) {
|
|
10502
|
+
event.preventDefault();
|
|
10503
|
+
openMarkdownTableMenu(header, 0, 0);
|
|
10504
|
+
return;
|
|
10505
|
+
}
|
|
9951
10506
|
if (event.key === "Escape") {
|
|
9952
10507
|
closeChapterTypeMenu();
|
|
9953
10508
|
closeLineCitationMenu();
|
|
10509
|
+
closeMarkdownTableMenu(true);
|
|
9954
10510
|
hideAiMentionMenu();
|
|
9955
10511
|
}
|
|
9956
10512
|
});
|