@musnows/scriverse 0.5.10 → 0.5.11
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 +57 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +42 -3
- 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 +48 -1
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +397 -58
- 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/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 +79 -0
- package/dist/public/styles.css +65 -6
- package/dist/public/work-permissions.d.ts +2 -2
- package/dist/public/work-permissions.js +8 -0
- package/dist/store.js +108 -2
- 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/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=20260729-ai-stream-typewriter-v1";
|
|
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,12 +36,12 @@ 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";
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
} from "/avatar-crop.js?v=20260725-avatar-crop";
|
|
60
61
|
|
|
61
62
|
const defaultPageSizes = Object.freeze({
|
|
63
|
+
drafts: 30,
|
|
62
64
|
settings: 30,
|
|
63
65
|
characters: 30,
|
|
64
66
|
races: 30,
|
|
@@ -871,7 +873,9 @@ let entityEditorReadOnly = false;
|
|
|
871
873
|
let chapterEditorReadOnly = true;
|
|
872
874
|
let characterListPage = 1;
|
|
873
875
|
let taskListPage = 1;
|
|
876
|
+
let draftTypeFilter = "all";
|
|
874
877
|
const moduleListPages = {
|
|
878
|
+
drafts: 1,
|
|
875
879
|
settings: 1,
|
|
876
880
|
organizations: 1,
|
|
877
881
|
timeline: 1,
|
|
@@ -901,6 +905,7 @@ let characterSectionEditorDirty = false;
|
|
|
901
905
|
let settingEditorVditor = null;
|
|
902
906
|
let knowledgeSectionVditor = null;
|
|
903
907
|
let characterSectionVditor = null;
|
|
908
|
+
let formDialogVditors = [];
|
|
904
909
|
let entityHistoryContext = null;
|
|
905
910
|
let moduleContentInteractionsBound = false;
|
|
906
911
|
|
|
@@ -1367,7 +1372,8 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1367
1372
|
read_chapters: "读取章节",
|
|
1368
1373
|
grep: "查询正文关键字",
|
|
1369
1374
|
search_story_entities: "搜索作品实体",
|
|
1370
|
-
read_character_sections: "读取人物 Markdown 章节"
|
|
1375
|
+
read_character_sections: "读取人物 Markdown 章节",
|
|
1376
|
+
search_drafts: "搜索草稿"
|
|
1371
1377
|
};
|
|
1372
1378
|
|
|
1373
1379
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1375,10 +1381,50 @@ const AI_TOOL_DESCRIPTIONS = {
|
|
|
1375
1381
|
read_chapters: "读取指定章节的概要、正文或两者。",
|
|
1376
1382
|
grep: "查询正文关键字所在的完整段落及章节信息。",
|
|
1377
1383
|
search_story_entities: "按实体名、拼音或短关键词混合检索设定、人物、组织等结构化记录;非语义问答。",
|
|
1378
|
-
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。"
|
|
1384
|
+
read_character_sections: "读取指定人物 Markdown 档案章节的摘要或原文。",
|
|
1385
|
+
search_drafts: "搜索可能采用、也可能永远不会进入正文或正式设定的未确认临时想法。"
|
|
1379
1386
|
};
|
|
1380
1387
|
|
|
1381
1388
|
let aiFeedScrollFrame = null;
|
|
1389
|
+
let markdownTableMenuTarget = null;
|
|
1390
|
+
let markdownTableMenuTrigger = null;
|
|
1391
|
+
|
|
1392
|
+
function closeMarkdownTableMenu(restoreFocus = false) {
|
|
1393
|
+
const trigger = markdownTableMenuTrigger;
|
|
1394
|
+
$("#markdown-table-menu").classList.add("hidden");
|
|
1395
|
+
markdownTableMenuTarget = null;
|
|
1396
|
+
markdownTableMenuTrigger = null;
|
|
1397
|
+
if (restoreFocus && trigger?.isConnected) trigger.focus();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function setMarkdownTableWrapping(table, wrapping) {
|
|
1401
|
+
table.classList.toggle("is-wrapping", wrapping);
|
|
1402
|
+
table.setAttribute("aria-label", wrapping ? "Markdown 表格,内容自动换行" : "Markdown 表格,内容不换行");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function openMarkdownTableMenu(header, clientX, clientY) {
|
|
1406
|
+
const table = header.closest(".markdown-table-scroll");
|
|
1407
|
+
if (!table) return;
|
|
1408
|
+
if (header.closest(".is-streaming")) {
|
|
1409
|
+
toast("回复生成完成后可以设置表格换行");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
closeChapterTypeMenu();
|
|
1413
|
+
closeLineCitationMenu();
|
|
1414
|
+
markdownTableMenuTarget = table;
|
|
1415
|
+
markdownTableMenuTrigger = header;
|
|
1416
|
+
const menu = $("#markdown-table-menu");
|
|
1417
|
+
const toggle = $("#markdown-table-wrap-toggle");
|
|
1418
|
+
toggle.setAttribute("aria-checked", String(table.classList.contains("is-wrapping")));
|
|
1419
|
+
menu.classList.remove("hidden");
|
|
1420
|
+
const menuRect = menu.getBoundingClientRect();
|
|
1421
|
+
const headerRect = header.getBoundingClientRect();
|
|
1422
|
+
const anchorX = clientX > 0 ? clientX : headerRect.left;
|
|
1423
|
+
const anchorY = clientY > 0 ? clientY : headerRect.bottom;
|
|
1424
|
+
menu.style.left = `${Math.max(8, Math.min(anchorX, window.innerWidth - menuRect.width - 8))}px`;
|
|
1425
|
+
menu.style.top = `${Math.max(8, Math.min(anchorY, window.innerHeight - menuRect.height - 8))}px`;
|
|
1426
|
+
toggle.focus();
|
|
1427
|
+
}
|
|
1382
1428
|
|
|
1383
1429
|
function scrollAiFeedToBottom() {
|
|
1384
1430
|
const feed = $("#ai-feed");
|
|
@@ -2706,6 +2752,7 @@ function renderSettingsHub() {
|
|
|
2706
2752
|
const hasWork = Boolean(state.work);
|
|
2707
2753
|
const canManageWork = hasWork && ["admin", "owner"].includes(String(state.work.accessRole));
|
|
2708
2754
|
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
2755
|
+
const canReadFullExport = canReadAggregate && canReadModule("drafts");
|
|
2709
2756
|
const isAdmin = state.user?.role === "admin";
|
|
2710
2757
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
2711
2758
|
$("#platform-usage-button").classList.toggle("hidden", !isAdmin);
|
|
@@ -2715,7 +2762,7 @@ function renderSettingsHub() {
|
|
|
2715
2762
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
2716
2763
|
$("#work-audit-button").disabled = !canManageWork;
|
|
2717
2764
|
$("#top-search-button").disabled = !canReadAggregate;
|
|
2718
|
-
$("#export-button").disabled = !
|
|
2765
|
+
$("#export-button").disabled = !canReadFullExport;
|
|
2719
2766
|
$("#settings-return").textContent = settingsReturnContext?.view === "shelf" || !hasWork ? "返回书架" : "返回当前作品";
|
|
2720
2767
|
$("#settings-work-note").textContent = hasWork
|
|
2721
2768
|
? `当前作品:《${state.work.title}》。导出的 ZIP 内含 Markdown 正文,仅包含分卷、章节标题与正文。`
|
|
@@ -2796,11 +2843,15 @@ const workAuditActionLabels = {
|
|
|
2796
2843
|
"chapter.deleted": "删除章节",
|
|
2797
2844
|
"chapter.purged": "彻底删除章节",
|
|
2798
2845
|
"chapter.restored": "恢复章节",
|
|
2846
|
+
"draft.created": "创建草稿",
|
|
2847
|
+
"draft.updated": "更新草稿",
|
|
2848
|
+
"draft.deleted": "删除草稿",
|
|
2849
|
+
"draft.restored": "恢复草稿",
|
|
2799
2850
|
"work.imported": "导入正文"
|
|
2800
2851
|
};
|
|
2801
2852
|
|
|
2802
2853
|
function workAuditEntityLabel(type) {
|
|
2803
|
-
return ({ work: "作品", volume: "分卷", chapter: "章节", user: "用户" })[type] ?? type;
|
|
2854
|
+
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "草稿", user: "用户" })[type] ?? type;
|
|
2804
2855
|
}
|
|
2805
2856
|
|
|
2806
2857
|
function workAuditDetailText(detail) {
|
|
@@ -2911,6 +2962,7 @@ async function openPlatformUiSettingsDialog() {
|
|
|
2911
2962
|
const settings = await api("/api/platform/ui-settings");
|
|
2912
2963
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2913
2964
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
2965
|
+
$("#page-size-drafts").value = String(pageSizes.drafts);
|
|
2914
2966
|
$("#page-size-settings").value = String(pageSizes.settings);
|
|
2915
2967
|
$("#page-size-characters").value = String(pageSizes.characters);
|
|
2916
2968
|
$("#page-size-organizations").value = String(pageSizes.organizations);
|
|
@@ -3292,6 +3344,7 @@ function resetWorkScopedUiCaches() {
|
|
|
3292
3344
|
state.settings = [];
|
|
3293
3345
|
state.races = [];
|
|
3294
3346
|
characterListPage = 1;
|
|
3347
|
+
draftTypeFilter = "all";
|
|
3295
3348
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
3296
3349
|
relationshipFilters.fromCharacterIds = [];
|
|
3297
3350
|
relationshipFilters.toCharacterIds = [];
|
|
@@ -3730,6 +3783,7 @@ function markActiveModule(module) {
|
|
|
3730
3783
|
}
|
|
3731
3784
|
|
|
3732
3785
|
const moduleMeta = {
|
|
3786
|
+
drafts: ["临时想法", "创作草稿", "记录可能采用、也可能永远不会写入正文或正式设定的想法。", "新建草稿"],
|
|
3733
3787
|
settings: ["世界事实", "世界观与设定库", "锁定的设定会成为 AI 续写与校对的硬约束。", "新建设定"],
|
|
3734
3788
|
characters: ["人物档案", "角色与人物属性", "维护别名、属性、当前状态及不可被 AI 覆盖的字段。", "新建角色"],
|
|
3735
3789
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
@@ -3785,6 +3839,7 @@ async function showModule(module) {
|
|
|
3785
3839
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
3786
3840
|
bindModuleContentInteractions();
|
|
3787
3841
|
try {
|
|
3842
|
+
if (module === "drafts") await renderDrafts();
|
|
3788
3843
|
if (module === "settings") await renderSettings();
|
|
3789
3844
|
if (module === "characters") await renderCharacters(characterListPage);
|
|
3790
3845
|
if (module === "races") await renderRaces();
|
|
@@ -4185,6 +4240,135 @@ function settingRecordActions(item) {
|
|
|
4185
4240
|
: recordHistoryButton("setting", item.id, item.title);
|
|
4186
4241
|
}
|
|
4187
4242
|
|
|
4243
|
+
function draftTypeLabel(draftType) {
|
|
4244
|
+
return draftType === "setting" ? "设定草稿" : "正文草稿";
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
async function deleteDraft(item) {
|
|
4248
|
+
if (!item || !canEditModule("drafts")) return;
|
|
4249
|
+
if (!await confirmToast(`确认删除草稿“${item.title}”吗?草稿将从当前列表移除。`, {
|
|
4250
|
+
title: "删除草稿",
|
|
4251
|
+
confirmLabel: "确认删除"
|
|
4252
|
+
})) return;
|
|
4253
|
+
try {
|
|
4254
|
+
await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
4255
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4256
|
+
toast("草稿已删除");
|
|
4257
|
+
} catch (error) {
|
|
4258
|
+
toast(error.message, "error");
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
4263
|
+
const viewOnly = readOnly || !canEditModule("drafts");
|
|
4264
|
+
const fields = `<p class="form-field-note">草稿只记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。</p>`
|
|
4265
|
+
+ field("draftType", "草稿类型", "select", item?.draftType ?? "prose", [["prose", "正文草稿"], ["setting", "设定草稿"]])
|
|
4266
|
+
+ field("title", "标题", "text", item?.title ?? "")
|
|
4267
|
+
+ field("content", "内容", "markdown", item?.content ?? "", {
|
|
4268
|
+
placeholder: "记录尚未定稿的片段、方向或设定想法……",
|
|
4269
|
+
readOnly: viewOnly
|
|
4270
|
+
});
|
|
4271
|
+
openDialog(item ? viewOnly ? "查看草稿" : "编辑草稿" : "新建草稿", fields, async (form) => {
|
|
4272
|
+
if (viewOnly) return;
|
|
4273
|
+
const title = String(form.get("title") ?? "").trim();
|
|
4274
|
+
if (!title) throw new Error("请填写草稿标题");
|
|
4275
|
+
const body = {
|
|
4276
|
+
draftType: form.get("draftType"),
|
|
4277
|
+
title,
|
|
4278
|
+
content: String(form.get("content") ?? ""),
|
|
4279
|
+
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
4280
|
+
};
|
|
4281
|
+
await api(item ? `/api/drafts/${encodeURIComponent(item.id)}` : `/api/works/${encodeURIComponent(state.work.id)}/drafts`, {
|
|
4282
|
+
method: item ? "PATCH" : "POST",
|
|
4283
|
+
body
|
|
4284
|
+
});
|
|
4285
|
+
await renderDrafts(moduleListPages.drafts);
|
|
4286
|
+
toast(item ? "草稿已保存" : "草稿已创建");
|
|
4287
|
+
}, item ? draftTypeLabel(item.draftType) : "未确认想法", {
|
|
4288
|
+
submitLabel: viewOnly ? "关闭" : "保存草稿",
|
|
4289
|
+
hideCancel: viewOnly,
|
|
4290
|
+
wide: true,
|
|
4291
|
+
errorPrefix: "草稿保存失败:"
|
|
4292
|
+
});
|
|
4293
|
+
if (viewOnly) {
|
|
4294
|
+
$("#dialog-fields").querySelectorAll("input, select, textarea").forEach((control) => {
|
|
4295
|
+
if (control instanceof HTMLSelectElement) control.disabled = true;
|
|
4296
|
+
else control.readOnly = true;
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
async function renderDrafts(page = moduleListPages.drafts) {
|
|
4302
|
+
const allDrafts = await apiAllPages(`/api/works/${state.work.id}/drafts`);
|
|
4303
|
+
const drafts = draftTypeFilter === "all"
|
|
4304
|
+
? allDrafts
|
|
4305
|
+
: allDrafts.filter((draft) => draft.draftType === draftTypeFilter);
|
|
4306
|
+
mountModuleCount(drafts.length);
|
|
4307
|
+
const pageResult = paginateModuleItems(drafts, page, "drafts");
|
|
4308
|
+
moduleListPages.drafts = pageResult.page;
|
|
4309
|
+
const layout = readModuleLayout();
|
|
4310
|
+
if (drafts.length) mountModuleLayoutToggle(layout, "草稿列表样式");
|
|
4311
|
+
else $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
4312
|
+
const filterToolbar = `<section class="draft-filter-toolbar" aria-label="草稿筛选">
|
|
4313
|
+
<label for="draft-type-filter">草稿类型</label>
|
|
4314
|
+
<select id="draft-type-filter" aria-label="按草稿类型筛选">
|
|
4315
|
+
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}>全部草稿</option>
|
|
4316
|
+
<option value="prose" ${draftTypeFilter === "prose" ? "selected" : ""}>正文草稿</option>
|
|
4317
|
+
<option value="setting" ${draftTypeFilter === "setting" ? "selected" : ""}>设定草稿</option>
|
|
4318
|
+
</select>
|
|
4319
|
+
${draftTypeFilter === "all" ? "" : `<span aria-live="polite">筛选后剩余 ${drafts.length} 篇草稿</span>`}
|
|
4320
|
+
</section>`;
|
|
4321
|
+
const actions = (item) => canEditModule("drafts")
|
|
4322
|
+
? `${recordCardEditButton("edit-draft", item.id, `草稿“${item.title}”`)}<button type="button" data-delete-draft="${esc(item.id)}">删除</button>${recordHistoryButton("draft", item.id, item.title)}`
|
|
4323
|
+
: recordHistoryButton("draft", item.id, item.title);
|
|
4324
|
+
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
4325
|
+
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看草稿 ${esc(item.title)}">
|
|
4326
|
+
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4327
|
+
<h3>${esc(item.title)}</h3>
|
|
4328
|
+
<div class="record-markdown-preview">${esc(item.contentPreview || "暂无内容")}</div>
|
|
4329
|
+
<div class="card-actions">${actions(item)}</div>
|
|
4330
|
+
</article>`).join("")}</div>`;
|
|
4331
|
+
const rows = `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
4332
|
+
const preview = moduleRowPreview(item.contentPreview || "暂无内容");
|
|
4333
|
+
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)}">
|
|
4334
|
+
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4335
|
+
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
4336
|
+
<div class="card-actions">${actions(item)}</div>
|
|
4337
|
+
</article>`;
|
|
4338
|
+
}).join("")}</div>`;
|
|
4339
|
+
const emptyDrafts = allDrafts.length
|
|
4340
|
+
? emptyModule("没有符合筛选条件的草稿", "可以切换草稿类型查看其他想法。")
|
|
4341
|
+
: emptyModule("还没有草稿", "把不一定会进入正文或设定的片段、方向和备选想法先记在这里。");
|
|
4342
|
+
$("#module-content").innerHTML = filterToolbar + (drafts.length
|
|
4343
|
+
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "草稿列表")}`
|
|
4344
|
+
: emptyDrafts);
|
|
4345
|
+
$("#draft-type-filter").addEventListener("change", async (event) => {
|
|
4346
|
+
draftTypeFilter = ["prose", "setting"].includes(event.currentTarget.value) ? event.currentTarget.value : "all";
|
|
4347
|
+
moduleListPages.drafts = 1;
|
|
4348
|
+
await renderDrafts(1);
|
|
4349
|
+
});
|
|
4350
|
+
bindModuleLayoutToggle(() => renderDrafts(pageResult.page));
|
|
4351
|
+
bindModulePagination("drafts", renderDrafts);
|
|
4352
|
+
const draftById = (draftId) => drafts.find((draft) => draft.id === draftId);
|
|
4353
|
+
$("#module-content").querySelectorAll("[data-open-draft]").forEach((card) => {
|
|
4354
|
+
const open = async () => openDraftDialog(await api(`/api/drafts/${encodeURIComponent(card.dataset.openDraft)}`), { readOnly: true });
|
|
4355
|
+
card.addEventListener("click", (event) => { if (!event.target.closest("button, a")) void open(); });
|
|
4356
|
+
card.addEventListener("keydown", (event) => {
|
|
4357
|
+
if ((event.key === "Enter" || event.key === " ") && !event.target.closest("button, a")) {
|
|
4358
|
+
event.preventDefault();
|
|
4359
|
+
void open();
|
|
4360
|
+
}
|
|
4361
|
+
});
|
|
4362
|
+
});
|
|
4363
|
+
$("#module-content").querySelectorAll("[data-edit-draft]").forEach((button) => button.addEventListener("click", async () => {
|
|
4364
|
+
openDraftDialog(await api(`/api/drafts/${encodeURIComponent(button.dataset.editDraft)}`));
|
|
4365
|
+
}));
|
|
4366
|
+
$("#module-content").querySelectorAll("[data-delete-draft]").forEach((button) => button.addEventListener("click", () => {
|
|
4367
|
+
void deleteDraft(draftById(button.dataset.deleteDraft));
|
|
4368
|
+
}));
|
|
4369
|
+
bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4188
4372
|
function renderSettingCards(records) {
|
|
4189
4373
|
return `<div class="card-grid">${records.map((item) => `
|
|
4190
4374
|
<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>
|
|
@@ -4803,13 +4987,15 @@ async function renderTasks(page = taskListPage) {
|
|
|
4803
4987
|
: "";
|
|
4804
4988
|
$("#module-content").innerHTML = `
|
|
4805
4989
|
<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
|
-
|
|
4990
|
+
<div class="task-auto-run-header">
|
|
4991
|
+
<div class="task-auto-run-copy">
|
|
4992
|
+
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
4993
|
+
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
4994
|
+
<small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
|
|
4995
|
+
</div>
|
|
4996
|
+
<div class="task-auto-run-actions">
|
|
4997
|
+
${autoRunEditing ? "" : '<button id="task-auto-run-edit" class="ghost-button" type="button">编辑</button>'}
|
|
4998
|
+
</div>
|
|
4813
4999
|
</div>
|
|
4814
5000
|
<div class="task-auto-run-controls">
|
|
4815
5001
|
<label class="checkbox-field"><input id="task-auto-run-enabled" type="checkbox" ${settings.autoRunEnabled ? "checked" : ""} ${autoRunEditing ? "" : "disabled"}><span>持续自动执行</span></label>
|
|
@@ -5929,7 +6115,7 @@ async function renderBookAiSettings() {
|
|
|
5929
6115
|
]);
|
|
5930
6116
|
const host = $("#module-content");
|
|
5931
6117
|
const workId = String(state.work.id);
|
|
5932
|
-
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"]);
|
|
6118
|
+
const agentTools = new Set(settings.agentTools ?? ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections", "search_drafts"]);
|
|
5933
6119
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
5934
6120
|
title: "本书 Token 用量",
|
|
5935
6121
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
@@ -5943,6 +6129,10 @@ async function renderBookAiSettings() {
|
|
|
5943
6129
|
"afterend",
|
|
5944
6130
|
`<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
6131
|
);
|
|
6132
|
+
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
6133
|
+
"beforeend",
|
|
6134
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索草稿</strong><small>查询正文草稿和设定草稿。草稿只是可能采用、也可能永远不会进入正文或正式设定的临时想法,Agent 不会把它当作已确认事实。</small></span></label>`
|
|
6135
|
+
);
|
|
5946
6136
|
if (!canEditModule("ai-settings")) {
|
|
5947
6137
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
5948
6138
|
host.querySelectorAll(".config-save-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
@@ -6256,7 +6446,7 @@ async function ensureAiReferencesLoaded() {
|
|
|
6256
6446
|
|
|
6257
6447
|
function field(name, label, type = "text", value = "", options = []) {
|
|
6258
6448
|
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>`;
|
|
6449
|
+
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
6450
|
if (type === "item-list") {
|
|
6261
6451
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
6262
6452
|
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 +6616,8 @@ function commitRelationshipKeywordInputs(container) {
|
|
|
6426
6616
|
}
|
|
6427
6617
|
|
|
6428
6618
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
6619
|
+
formDialogVditors.forEach(destroyVditorEditor);
|
|
6620
|
+
formDialogVditors = [];
|
|
6429
6621
|
void discardPendingMarkdownAttachments();
|
|
6430
6622
|
const dialog = $("#form-dialog");
|
|
6431
6623
|
const form = $("#dynamic-form");
|
|
@@ -6448,9 +6640,10 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
6448
6640
|
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
6449
6641
|
dialog.classList.toggle("wide-dialog", Boolean(options.wide));
|
|
6450
6642
|
dialog.classList.toggle("trace-dialog", Boolean(options.trace));
|
|
6643
|
+
dialog.classList.toggle("large-dialog", Boolean(options.large));
|
|
6451
6644
|
bindDynamicListControls($("#dialog-fields"));
|
|
6452
6645
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
6453
|
-
bindVditorEditors($("#dialog-fields"));
|
|
6646
|
+
formDialogVditors = bindVditorEditors($("#dialog-fields"));
|
|
6454
6647
|
form.onclick = null;
|
|
6455
6648
|
form.onkeydown = null;
|
|
6456
6649
|
dialog.oncancel = (event) => {
|
|
@@ -7224,6 +7417,7 @@ function destroyVditorEditor(editor) {
|
|
|
7224
7417
|
}
|
|
7225
7418
|
|
|
7226
7419
|
function bindVditorEditors(container) {
|
|
7420
|
+
const editors = [];
|
|
7227
7421
|
container.querySelectorAll("[data-vditor-editor]").forEach((host) => {
|
|
7228
7422
|
const valueField = host.parentElement?.querySelector("[data-vditor-value]");
|
|
7229
7423
|
const editor = createVditorEditor(host, valueField?.value ?? "", {
|
|
@@ -7231,10 +7425,12 @@ function bindVditorEditors(container) {
|
|
|
7231
7425
|
if (valueField) valueField.value = markdown;
|
|
7232
7426
|
markEntityEditorDirty();
|
|
7233
7427
|
},
|
|
7234
|
-
placeholder: "",
|
|
7428
|
+
placeholder: host.dataset.placeholder ?? "",
|
|
7235
7429
|
readOnly: Boolean(valueField?.readOnly)
|
|
7236
7430
|
});
|
|
7431
|
+
if (editor) editors.push(editor);
|
|
7237
7432
|
});
|
|
7433
|
+
return editors;
|
|
7238
7434
|
}
|
|
7239
7435
|
|
|
7240
7436
|
function characterSectionImageLabel(file, fallback = "图片附件") {
|
|
@@ -7945,49 +8141,151 @@ function openOutlineDialog(item) {
|
|
|
7945
8141
|
}, "章节大纲");
|
|
7946
8142
|
}
|
|
7947
8143
|
|
|
8144
|
+
const foreshadowOccurrenceRoleOptions = [["setup", "埋设"], ["reminder", "提醒"], ["payoff", "回收"]];
|
|
8145
|
+
|
|
8146
|
+
function foreshadowOccurrenceRow(record, chapters, index) {
|
|
8147
|
+
const chapterOptions = [["", "暂不关联"], ...chapters];
|
|
8148
|
+
const occurrenceId = record?.id ?? "";
|
|
8149
|
+
const role = record?.role ?? "setup";
|
|
8150
|
+
const chapterId = record?.chapterId ?? "";
|
|
8151
|
+
const chapterLabel = chapters.find(([value]) => value === chapterId)?.[1] ?? "未关联章节";
|
|
8152
|
+
return `<fieldset class="foreshadow-occurrence-row" data-foreshadow-occurrence data-occurrence-id="${esc(occurrenceId)}" data-occurrence-role="${esc(role)}">
|
|
8153
|
+
<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>
|
|
8154
|
+
<button class="foreshadow-occurrence-remove" type="button" data-foreshadow-occurrence-remove aria-label="删除第 ${index + 1} 个章节节点">删除节点</button>
|
|
8155
|
+
<div class="foreshadow-occurrence-grid">
|
|
8156
|
+
<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>
|
|
8157
|
+
<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>
|
|
8158
|
+
</div>
|
|
8159
|
+
<label>节点说明<textarea name="occurrenceNote" data-foreshadow-occurrence-note aria-label="第 ${index + 1} 个节点说明">${esc(record?.note ?? "")}</textarea></label>
|
|
8160
|
+
</fieldset>`;
|
|
8161
|
+
}
|
|
8162
|
+
|
|
8163
|
+
function foreshadowOccurrencesField(item, chapters) {
|
|
8164
|
+
const occurrences = item?.occurrences?.length ? item.occurrences : [null];
|
|
8165
|
+
return `<section class="form-field foreshadow-editor-section foreshadow-occurrence-field" data-foreshadow-occurrences aria-labelledby="foreshadow-occurrence-title">
|
|
8166
|
+
<div class="foreshadow-occurrence-toolbar"><div><h3 id="foreshadow-occurrence-title">章节节点</h3><p>每张卡片代表一次独立的埋设、提醒或回收,类型、章节和说明始终绑定在同一组内。</p></div><button type="button" data-foreshadow-occurrence-add>添加节点</button></div>
|
|
8167
|
+
<div class="foreshadow-occurrence-rows" data-foreshadow-occurrence-rows>${occurrences.map((record, index) => foreshadowOccurrenceRow(record, chapters, index)).join("")}</div>
|
|
8168
|
+
<p class="foreshadow-occurrence-empty hidden" data-foreshadow-occurrence-empty>尚未添加章节节点。</p>
|
|
8169
|
+
</section>`;
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
function foreshadowOverviewField(item, options, firstPayoff) {
|
|
8173
|
+
return `<section class="foreshadow-editor-section foreshadow-overview-field" aria-labelledby="foreshadow-overview-title">
|
|
8174
|
+
<header><h3 id="foreshadow-overview-title">伏笔信息</h3><p>维护伏笔本身的内容、状态与计划回收位置。</p></header>
|
|
8175
|
+
<div class="foreshadow-overview-grid">
|
|
8176
|
+
${field("title", "伏笔名称", "text", item?.title)}
|
|
8177
|
+
${field("importance", "重要程度", "select", item?.importance ?? "medium", [["low", "低"], ["medium", "中"], ["high", "高"]])}
|
|
8178
|
+
${field("status", "状态", "select", item?.status ?? "planned", [["planned", "计划中"], ["planted", "已埋设"], ["resolved", "已回收"], ["abandoned", "已放弃"]])}
|
|
8179
|
+
${field("plannedPayoffChapterId", "计划回收章节", "select", item?.plannedPayoffChapterId ?? firstPayoff?.chapterId ?? "", options)}
|
|
8180
|
+
</div>
|
|
8181
|
+
<div class="foreshadow-overview-description">${field("description", "内容与预期作用", "textarea", item?.description)}</div>
|
|
8182
|
+
</section>`;
|
|
8183
|
+
}
|
|
8184
|
+
|
|
8185
|
+
function foreshadowResolutionField(item) {
|
|
8186
|
+
return `<section class="foreshadow-editor-section foreshadow-resolution-field" aria-labelledby="foreshadow-resolution-title">
|
|
8187
|
+
<header><h3 id="foreshadow-resolution-title">回收结果</h3><p>记录伏笔最终如何揭示或完成;它不属于上方任意单个章节节点。</p></header>
|
|
8188
|
+
${field("resolutionNote", "回收结论", "textarea", item?.resolutionNote)}
|
|
8189
|
+
</section>`;
|
|
8190
|
+
}
|
|
8191
|
+
|
|
8192
|
+
function updateForeshadowOccurrenceRowSummary(row) {
|
|
8193
|
+
const role = row.querySelector("[data-foreshadow-occurrence-role]").value;
|
|
8194
|
+
const chapter = row.querySelector("[data-foreshadow-occurrence-chapter]");
|
|
8195
|
+
row.dataset.occurrenceRole = role;
|
|
8196
|
+
row.querySelector("[data-foreshadow-occurrence-role-label]").textContent = occurrenceRoleLabel(role);
|
|
8197
|
+
row.querySelector("[data-foreshadow-occurrence-summary]").textContent = chapter.value ? chapter.selectedOptions[0]?.textContent ?? "已关联章节" : "未关联章节";
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
function renumberForeshadowOccurrenceRows(section) {
|
|
8201
|
+
const rows = [...section.querySelectorAll("[data-foreshadow-occurrence]")];
|
|
8202
|
+
rows.forEach((row, index) => {
|
|
8203
|
+
const sequence = index + 1;
|
|
8204
|
+
row.querySelector("[data-foreshadow-occurrence-index]").textContent = `节点 ${sequence}`;
|
|
8205
|
+
row.querySelector("[data-foreshadow-occurrence-remove]").setAttribute("aria-label", `删除第 ${sequence} 个章节节点`);
|
|
8206
|
+
row.querySelector("[data-foreshadow-occurrence-role]").setAttribute("aria-label", `第 ${sequence} 个节点类型`);
|
|
8207
|
+
row.querySelector("[data-foreshadow-occurrence-chapter]").setAttribute("aria-label", `第 ${sequence} 个关联章节`);
|
|
8208
|
+
row.querySelector("[data-foreshadow-occurrence-note]").setAttribute("aria-label", `第 ${sequence} 个节点说明`);
|
|
8209
|
+
updateForeshadowOccurrenceRowSummary(row);
|
|
8210
|
+
});
|
|
8211
|
+
section.querySelector("[data-foreshadow-occurrence-empty]").classList.toggle("hidden", rows.length > 0);
|
|
8212
|
+
}
|
|
8213
|
+
|
|
8214
|
+
function bindForeshadowOccurrenceControls(container, chapters) {
|
|
8215
|
+
const section = container.querySelector("[data-foreshadow-occurrences]");
|
|
8216
|
+
if (!section) return;
|
|
8217
|
+
const rows = section.querySelector("[data-foreshadow-occurrence-rows]");
|
|
8218
|
+
section.addEventListener("change", (event) => {
|
|
8219
|
+
const row = event.target.closest("[data-foreshadow-occurrence]");
|
|
8220
|
+
if (row && event.target.matches("[data-foreshadow-occurrence-role], [data-foreshadow-occurrence-chapter]")) updateForeshadowOccurrenceRowSummary(row);
|
|
8221
|
+
});
|
|
8222
|
+
section.addEventListener("click", (event) => {
|
|
8223
|
+
if (event.target.closest("[data-foreshadow-occurrence-add]")) {
|
|
8224
|
+
const index = rows.querySelectorAll("[data-foreshadow-occurrence]").length;
|
|
8225
|
+
rows.insertAdjacentHTML("beforeend", foreshadowOccurrenceRow(null, chapters, index));
|
|
8226
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8227
|
+
rows.lastElementChild?.querySelector("[data-foreshadow-occurrence-role]")?.focus();
|
|
8228
|
+
return;
|
|
8229
|
+
}
|
|
8230
|
+
const remove = event.target.closest("[data-foreshadow-occurrence-remove]");
|
|
8231
|
+
if (!remove) return;
|
|
8232
|
+
const row = remove.closest("[data-foreshadow-occurrence]");
|
|
8233
|
+
const nextFocus = row.nextElementSibling?.querySelector("[data-foreshadow-occurrence-role]")
|
|
8234
|
+
?? row.previousElementSibling?.querySelector("[data-foreshadow-occurrence-role]")
|
|
8235
|
+
?? section.querySelector("[data-foreshadow-occurrence-add]");
|
|
8236
|
+
row.remove();
|
|
8237
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8238
|
+
nextFocus?.focus();
|
|
8239
|
+
});
|
|
8240
|
+
renumberForeshadowOccurrenceRows(section);
|
|
8241
|
+
}
|
|
8242
|
+
|
|
8243
|
+
function readForeshadowOccurrences(item) {
|
|
8244
|
+
const previousById = new Map((item?.occurrences ?? []).map((record) => [record.id, record]));
|
|
8245
|
+
const seen = new Map();
|
|
8246
|
+
return [...$("#dialog-fields").querySelectorAll("[data-foreshadow-occurrence]")].flatMap((row, index) => {
|
|
8247
|
+
const role = row.querySelector("[data-foreshadow-occurrence-role]").value;
|
|
8248
|
+
const chapterId = row.querySelector("[data-foreshadow-occurrence-chapter]").value;
|
|
8249
|
+
const note = row.querySelector("[data-foreshadow-occurrence-note]").value;
|
|
8250
|
+
if (!chapterId) {
|
|
8251
|
+
if (note.trim()) throw new Error(`第 ${index + 1} 个章节节点填写了说明,请选择关联章节`);
|
|
8252
|
+
return [];
|
|
8253
|
+
}
|
|
8254
|
+
const key = `${role}:${chapterId}`;
|
|
8255
|
+
const duplicateIndex = seen.get(key);
|
|
8256
|
+
if (duplicateIndex) throw new Error(`第 ${index + 1} 个章节节点与第 ${duplicateIndex} 个重复`);
|
|
8257
|
+
seen.set(key, index + 1);
|
|
8258
|
+
const previous = previousById.get(row.dataset.occurrenceId);
|
|
8259
|
+
return [{
|
|
8260
|
+
chapterId,
|
|
8261
|
+
role,
|
|
8262
|
+
note,
|
|
8263
|
+
evidence: previous?.chapterId === chapterId && previous?.role === role ? previous.evidence : []
|
|
8264
|
+
}];
|
|
8265
|
+
});
|
|
8266
|
+
}
|
|
8267
|
+
|
|
7948
8268
|
function openForeshadowDialog(item) {
|
|
7949
8269
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
|
|
7950
8270
|
if (!chapters.length) return toast("请先创建章节", "error");
|
|
7951
8271
|
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 }));
|
|
8272
|
+
const firstPayoff = item?.occurrences?.find((record) => record.role === "payoff");
|
|
7957
8273
|
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),
|
|
8274
|
+
foreshadowOverviewField(item, options, firstPayoff) +
|
|
8275
|
+
foreshadowOccurrencesField(item, chapters) +
|
|
8276
|
+
foreshadowResolutionField(item),
|
|
7969
8277
|
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];
|
|
8278
|
+
const occurrences = readForeshadowOccurrences(item);
|
|
7982
8279
|
const body = {
|
|
7983
8280
|
title: form.get("title"), description: form.get("description"), importance: form.get("importance"), status: form.get("status"),
|
|
7984
|
-
plannedPayoffChapterId: form.get("
|
|
8281
|
+
plannedPayoffChapterId: form.get("plannedPayoffChapterId") || null, resolutionNote: form.get("resolutionNote"), occurrences,
|
|
7985
8282
|
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
7986
8283
|
};
|
|
7987
8284
|
await api(item ? `/api/foreshadows/${item.id}` : `/api/works/${state.work.id}/foreshadows`, { method: item ? "PATCH" : "POST", body });
|
|
7988
8285
|
await renderOutlines();
|
|
7989
8286
|
toast(item ? "伏笔已更新" : "伏笔已创建");
|
|
7990
|
-
}, item ? "伏笔管理" : "创作线索");
|
|
8287
|
+
}, item ? "伏笔管理" : "创作线索", { large: true, meta: "完整编辑伏笔信息、章节节点与回收结果" });
|
|
8288
|
+
bindForeshadowOccurrenceControls($("#dialog-fields"), chapters);
|
|
7991
8289
|
}
|
|
7992
8290
|
|
|
7993
8291
|
function openTimelineSplitDialog(item) {
|
|
@@ -8496,12 +8794,22 @@ async function streamChat(body) {
|
|
|
8496
8794
|
const message = document.createElement("div");
|
|
8497
8795
|
message.className = "assistant-message is-streaming";
|
|
8498
8796
|
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>';
|
|
8797
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
|
|
8500
8798
|
attachMessageHeading(message, "助手 · 正在生成");
|
|
8501
8799
|
$("#ai-feed").append(message);
|
|
8502
8800
|
scrollAiFeedToBottom();
|
|
8503
8801
|
const content = message.querySelector(".message-body");
|
|
8504
8802
|
const meta = message.querySelector(".message-meta");
|
|
8803
|
+
const typewriter = createStreamTypewriter({
|
|
8804
|
+
onRender: (text, progress) => {
|
|
8805
|
+
content.innerHTML = renderMarkdown(text);
|
|
8806
|
+
const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
|
|
8807
|
+
meta.textContent = progress.pendingCharacters > 0
|
|
8808
|
+
? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
|
|
8809
|
+
: `正在生成 · ${progress.visibleCharacters} 字`;
|
|
8810
|
+
scrollAiFeedToBottom();
|
|
8811
|
+
}
|
|
8812
|
+
});
|
|
8505
8813
|
let streamedText = "";
|
|
8506
8814
|
let generatedMetadata = {};
|
|
8507
8815
|
let toolCalls = [];
|
|
@@ -8523,7 +8831,7 @@ async function streamChat(body) {
|
|
|
8523
8831
|
const decoder = new TextDecoder();
|
|
8524
8832
|
let buffer = "";
|
|
8525
8833
|
let streamError = null;
|
|
8526
|
-
const consume = (eventText) => {
|
|
8834
|
+
const consume = async (eventText) => {
|
|
8527
8835
|
let eventName = "message";
|
|
8528
8836
|
const dataLines = [];
|
|
8529
8837
|
for (const line of eventText.split(/\r?\n/)) {
|
|
@@ -8534,12 +8842,12 @@ async function streamChat(body) {
|
|
|
8534
8842
|
const payload = JSON.parse(dataLines.join("\n"));
|
|
8535
8843
|
if (eventName === "delta") {
|
|
8536
8844
|
const firstFinalDelta = streamedText.length === 0;
|
|
8537
|
-
|
|
8845
|
+
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
8846
|
+
streamedText += delta;
|
|
8538
8847
|
if (streamedText.length > 0) finalAnswerStarted = true;
|
|
8539
|
-
|
|
8848
|
+
typewriter.append(delta);
|
|
8540
8849
|
if (firstFinalDelta && processSteps.length) renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
8541
|
-
meta.textContent =
|
|
8542
|
-
scrollAiFeedToBottom();
|
|
8850
|
+
meta.textContent = "正在生成回复……";
|
|
8543
8851
|
} else if (eventName === "process_step") {
|
|
8544
8852
|
const step = { ...payload };
|
|
8545
8853
|
const append = step.append === true;
|
|
@@ -8560,7 +8868,9 @@ async function streamChat(body) {
|
|
|
8560
8868
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
8561
8869
|
scrollAiFeedToBottom();
|
|
8562
8870
|
} else if (eventName === "complete") {
|
|
8871
|
+
await typewriter.finish();
|
|
8563
8872
|
message.classList.remove("is-streaming");
|
|
8873
|
+
content.setAttribute("aria-busy", "false");
|
|
8564
8874
|
message.querySelector(".message-heading > span").textContent = "助手";
|
|
8565
8875
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
|
|
8566
8876
|
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
|
|
@@ -8579,14 +8889,17 @@ async function streamChat(body) {
|
|
|
8579
8889
|
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
8580
8890
|
const events = buffer.split(/\r?\n\r?\n/);
|
|
8581
8891
|
buffer = events.pop() ?? "";
|
|
8582
|
-
events
|
|
8892
|
+
for (const eventText of events) await consume(eventText);
|
|
8583
8893
|
if (chunk.done) break;
|
|
8584
8894
|
}
|
|
8585
|
-
if (buffer.trim()) consume(buffer);
|
|
8895
|
+
if (buffer.trim()) await consume(buffer);
|
|
8896
|
+
await typewriter.finish();
|
|
8586
8897
|
if (streamError) throw streamError;
|
|
8587
8898
|
return { content: streamedText, message, metadata: generatedMetadata };
|
|
8588
8899
|
} catch (error) {
|
|
8900
|
+
typewriter.reveal();
|
|
8589
8901
|
message.classList.remove("is-streaming");
|
|
8902
|
+
content.setAttribute("aria-busy", "false");
|
|
8590
8903
|
message.querySelector(".message-heading > span").textContent = "助手 · 生成中断";
|
|
8591
8904
|
renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
8592
8905
|
meta.textContent = "生成中断";
|
|
@@ -9565,6 +9878,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
9565
9878
|
body: {
|
|
9566
9879
|
toastPosition: $("#toast-position").value,
|
|
9567
9880
|
pageSizes: {
|
|
9881
|
+
drafts: Number($("#page-size-drafts").value),
|
|
9568
9882
|
settings: Number($("#page-size-settings").value),
|
|
9569
9883
|
characters: Number($("#page-size-characters").value),
|
|
9570
9884
|
organizations: Number($("#page-size-organizations").value),
|
|
@@ -9597,6 +9911,9 @@ $("#members-dialog").addEventListener("close", () => {
|
|
|
9597
9911
|
memberDialogDirectory = [];
|
|
9598
9912
|
});
|
|
9599
9913
|
$("#form-dialog").addEventListener("close", () => {
|
|
9914
|
+
formDialogVditors.forEach(destroyVditorEditor);
|
|
9915
|
+
formDialogVditors = [];
|
|
9916
|
+
void discardPendingMarkdownAttachments();
|
|
9600
9917
|
if (relationshipPresenceId && !$("#form-dialog").open) setRelationshipPresence(null);
|
|
9601
9918
|
});
|
|
9602
9919
|
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
@@ -9793,7 +10110,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
9793
10110
|
}
|
|
9794
10111
|
});
|
|
9795
10112
|
$("#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]?.());
|
|
10113
|
+
$("#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
10114
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
9798
10115
|
updateAiMentionMenu();
|
|
9799
10116
|
scheduleAiContextUsage();
|
|
@@ -9934,9 +10251,24 @@ $("#chapter-type-menu").addEventListener("click", async (event) => {
|
|
|
9934
10251
|
toast(error.message, "error");
|
|
9935
10252
|
}
|
|
9936
10253
|
});
|
|
10254
|
+
$("#markdown-table-wrap-toggle").addEventListener("click", () => {
|
|
10255
|
+
const table = markdownTableMenuTarget;
|
|
10256
|
+
if (!table?.isConnected) return closeMarkdownTableMenu();
|
|
10257
|
+
const wrapping = !table.classList.contains("is-wrapping");
|
|
10258
|
+
setMarkdownTableWrapping(table, wrapping);
|
|
10259
|
+
closeMarkdownTableMenu(true);
|
|
10260
|
+
toast(wrapping ? "表格已启用自动换行" : "表格已恢复横向滚动");
|
|
10261
|
+
});
|
|
10262
|
+
document.addEventListener("contextmenu", (event) => {
|
|
10263
|
+
const header = event.target.closest?.("[data-markdown-table-header]");
|
|
10264
|
+
if (!header) return;
|
|
10265
|
+
event.preventDefault();
|
|
10266
|
+
openMarkdownTableMenu(header, event.clientX, event.clientY);
|
|
10267
|
+
});
|
|
9937
10268
|
document.addEventListener("pointerdown", (event) => {
|
|
9938
10269
|
if (!event.target.closest("#chapter-type-menu")) closeChapterTypeMenu();
|
|
9939
10270
|
if (!event.target.closest("#line-citation-menu")) closeLineCitationMenu();
|
|
10271
|
+
if (!event.target.closest("#markdown-table-menu")) closeMarkdownTableMenu();
|
|
9940
10272
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
9941
10273
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
9942
10274
|
$("#account-menu").classList.add("hidden");
|
|
@@ -9948,9 +10280,16 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
9948
10280
|
}
|
|
9949
10281
|
});
|
|
9950
10282
|
document.addEventListener("keydown", (event) => {
|
|
10283
|
+
const header = event.target.closest?.("[data-markdown-table-header]");
|
|
10284
|
+
if (header && (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10"))) {
|
|
10285
|
+
event.preventDefault();
|
|
10286
|
+
openMarkdownTableMenu(header, 0, 0);
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
9951
10289
|
if (event.key === "Escape") {
|
|
9952
10290
|
closeChapterTypeMenu();
|
|
9953
10291
|
closeLineCitationMenu();
|
|
10292
|
+
closeMarkdownTableMenu(true);
|
|
9954
10293
|
hideAiMentionMenu();
|
|
9955
10294
|
}
|
|
9956
10295
|
});
|