@musnows/scriverse 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +35 -1
- package/dist/app.js.map +1 -1
- package/dist/collaboration-presence.js +75 -0
- package/dist/collaboration-presence.js.map +1 -0
- package/dist/database.js +19 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +493 -130
- package/dist/public/character-version.js +1 -1
- package/dist/public/display-labels.d.ts +16 -0
- package/dist/public/display-labels.js +81 -0
- package/dist/public/entity-version.js +5 -3
- package/dist/public/index.html +37 -13
- package/dist/public/page-route.js +3 -1
- package/dist/public/styles.css +111 -26
- package/dist/public/work-permissions.js +1 -1
- package/dist/server-runtime.js +5 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/user-auth.js +77 -3
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -13,14 +13,32 @@ import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-ma
|
|
|
13
13
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
14
14
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
15
15
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260713-character-history";
|
|
16
|
-
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=
|
|
16
|
+
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260725-enum-labels-zh";
|
|
17
|
+
import {
|
|
18
|
+
chapterVersionSourceLabel,
|
|
19
|
+
characterVisibilityLabel,
|
|
20
|
+
foreshadowStatusLabel,
|
|
21
|
+
levelLabel,
|
|
22
|
+
occurrenceRoleLabel,
|
|
23
|
+
outlineStatusLabel,
|
|
24
|
+
providerConnectionLabel,
|
|
25
|
+
providerStatusLabel,
|
|
26
|
+
relationshipCategoryLabel,
|
|
27
|
+
relationshipConfirmationLabel,
|
|
28
|
+
reviewItemTypeLabel,
|
|
29
|
+
reviewStatusLabel,
|
|
30
|
+
searchResultTypeLabel,
|
|
31
|
+
settingStatusLabel,
|
|
32
|
+
taskScopeLabel,
|
|
33
|
+
timelineStatusLabel
|
|
34
|
+
} from "/display-labels.js?v=20260725-enum-labels-zh";
|
|
17
35
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260723-knowledge-editor-page";
|
|
18
36
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
19
37
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
20
38
|
import { buildRaceForest, eligibleRaceParents, racePathLabel } from "/race-hierarchy.js?v=20260721-race-hierarchy";
|
|
21
39
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
22
|
-
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=
|
|
23
|
-
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout
|
|
40
|
+
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
41
|
+
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
24
42
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
25
43
|
|
|
26
44
|
const state = {
|
|
@@ -53,6 +71,23 @@ const state = {
|
|
|
53
71
|
contextChapterId: null
|
|
54
72
|
};
|
|
55
73
|
|
|
74
|
+
function createPresenceClientId() {
|
|
75
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
76
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
77
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
78
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
79
|
+
const value = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
80
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const presenceClientId = createPresenceClientId();
|
|
84
|
+
const presenceHeartbeatInterval = 12_000;
|
|
85
|
+
let presenceParticipants = [];
|
|
86
|
+
let presenceHeartbeatTimer = null;
|
|
87
|
+
let presenceHeartbeatQueued = null;
|
|
88
|
+
let presenceHeartbeatRequest = 0;
|
|
89
|
+
let collaborationAutoSaveDisabled = false;
|
|
90
|
+
|
|
56
91
|
let timelineMultiSelectEnabled = false;
|
|
57
92
|
|
|
58
93
|
const chapterTypes = ["正文", "设定", "作者的话", "其他"];
|
|
@@ -70,7 +105,7 @@ const analysisTaskTypeLabels = new Map([
|
|
|
70
105
|
]);
|
|
71
106
|
|
|
72
107
|
function analysisTaskTypeLabel(taskType) {
|
|
73
|
-
return analysisTaskTypeLabels.get(String(taskType)) ??
|
|
108
|
+
return analysisTaskTypeLabels.get(String(taskType)) ?? "其他分析";
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
function analysisTaskStatusLabel(status) {
|
|
@@ -82,7 +117,11 @@ function analysisTaskStatusLabel(status) {
|
|
|
82
117
|
partial: "部分失败",
|
|
83
118
|
expired: "已过期",
|
|
84
119
|
cancelled: "已取消"
|
|
85
|
-
})[String(status)] ??
|
|
120
|
+
})[String(status)] ?? "未知状态";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function reviewSeverityLabel(severity) {
|
|
124
|
+
return levelLabel(severity);
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
function canEditWork(work = state.work) {
|
|
@@ -235,7 +274,7 @@ const workspaceOnboardingSteps = [
|
|
|
235
274
|
{ selector: "[data-new-chapter-volume]", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
|
|
236
275
|
{ selector: "#versions-button", eyebrow: "版本安全", title: "查看章节版本", description: "每次保存都会生成可恢复版本,误改内容时可以随时回溯。", placement: "bottom" },
|
|
237
276
|
{ selector: "[data-module=\"characters\"]", eyebrow: "作品知识", title: "维护角色与世界资料", description: "角色、种族、组织、设定和时间线共同构成 AI 可引用的作品知识。", placement: "right" },
|
|
238
|
-
{ selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "
|
|
277
|
+
{ selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "跟踪大纲/伏笔", description: "记录剧情目标、冲突、转折和伏笔回收,避免长线遗漏。", placement: "right" },
|
|
239
278
|
{ selector: "[data-module=\"tasks\"]", eyebrow: "AI 分析中心", title: "从这里理解整部小说", description: "运行人物、关系、世界观、设定、事件和一致性分析,并查看每次分析的结果与进度。", placement: "right" },
|
|
240
279
|
{ selector: "#top-search-button", eyebrow: "全文检索", title: "搜索整部作品", description: "一次检索正文、角色、设定、种族与组织,快速定位创作依据。", placement: "bottom" },
|
|
241
280
|
{ selector: ".quick-actions button[data-task=\"continue\"]", eyebrow: "AI 快捷指令", title: "让创作助手基于正文工作", description: "总结、续写、剧情方向和冲突检查都以已保存内容为依据。", placement: "left" },
|
|
@@ -409,13 +448,134 @@ function replacePageRoute(route) {
|
|
|
409
448
|
if (restoringPageRoute) return;
|
|
410
449
|
const hash = serializePageRoute(route);
|
|
411
450
|
if (window.location.hash !== hash) window.history.replaceState(null, "", hash);
|
|
451
|
+
schedulePresenceHeartbeat();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function presencePageForRoute(route = currentPageRoute()) {
|
|
455
|
+
if (!state.work || route.view === "shelf" || route.view === "platform-ai") return null;
|
|
456
|
+
if (route.view === "editor") return { kind: "editor", resourceId: String(route.chapterId ?? "") || undefined };
|
|
457
|
+
if (route.view === "entity-editor") return { kind: "entity-editor", module: route.entity, resourceId: String(route.entityId ?? "") || undefined };
|
|
458
|
+
if (route.view === "module") return { kind: "module", module: route.module };
|
|
459
|
+
if (route.view === "settings" || route.view === "platform-ai") return { kind: "settings" };
|
|
460
|
+
return { kind: "welcome" };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function presencePageKey(page) {
|
|
464
|
+
if (!page) return "";
|
|
465
|
+
if (page.kind === "editor") return `editor:${page.resourceId ?? ""}`;
|
|
466
|
+
if (page.kind === "entity-editor") return `entity-editor:${page.module ?? ""}:${page.resourceId ?? ""}`;
|
|
467
|
+
if (page.kind === "module") return `module:${page.module ?? ""}`;
|
|
468
|
+
return page.kind;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function groupedPresenceParticipants() {
|
|
472
|
+
const groups = new Map();
|
|
473
|
+
for (const participant of presenceParticipants) {
|
|
474
|
+
const current = groups.get(participant.userId) ?? { ...participant, pages: [], clientIds: [] };
|
|
475
|
+
if (!current.pages.some((page) => page.key === participant.page.key)) current.pages.push(participant.page);
|
|
476
|
+
current.clientIds.push(participant.clientId);
|
|
477
|
+
groups.set(participant.userId, current);
|
|
478
|
+
}
|
|
479
|
+
return [...groups.values()];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function hasOtherCollaborators() {
|
|
483
|
+
return presenceParticipants.some((participant) => participant.userId !== state.user?.userId);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function syncChapterAutoSaveWithPresence() {
|
|
487
|
+
const wasDisabled = collaborationAutoSaveDisabled;
|
|
488
|
+
collaborationAutoSaveDisabled = hasOtherCollaborators();
|
|
489
|
+
if (collaborationAutoSaveDisabled) {
|
|
490
|
+
cancelChapterAutoSave();
|
|
491
|
+
if (state.chapter && canEditProse()) {
|
|
492
|
+
setSaveState(state.dirty ? "多人协作,自动保存已关闭" : "自动保存已关闭", state.dirty);
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (wasDisabled && state.dirty && state.chapter && canEditProse()) scheduleChapterAutoSave(250);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function renderPresence() {
|
|
500
|
+
const control = $("#presence-control");
|
|
501
|
+
if (!state.work || !presenceParticipants.length) {
|
|
502
|
+
syncChapterAutoSaveWithPresence();
|
|
503
|
+
control.classList.add("hidden");
|
|
504
|
+
$("#presence-panel").classList.add("hidden");
|
|
505
|
+
$("#presence-button").setAttribute("aria-expanded", "false");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const groups = groupedPresenceParticipants();
|
|
509
|
+
const localKey = presencePageKey(presencePageForRoute());
|
|
510
|
+
syncChapterAutoSaveWithPresence();
|
|
511
|
+
control.classList.remove("hidden");
|
|
512
|
+
$("#presence-count").textContent = `${groups.length} 人在线`;
|
|
513
|
+
$("#presence-list").innerHTML = groups.map((participant) => {
|
|
514
|
+
const isCurrent = participant.userId === state.user?.userId;
|
|
515
|
+
const samePage = !isCurrent && participant.pages.some((page) => page.key === localKey);
|
|
516
|
+
const pageLabels = participant.pages.map((page) => page.label).join("、");
|
|
517
|
+
const avatar = participant.avatarUrl
|
|
518
|
+
? `<span class="user-avatar"><span class="user-avatar-fallback">${esc(Array.from(participant.displayName || participant.username)[0] ?? "作")}</span><img src="${esc(participant.avatarUrl)}" alt=""></span>`
|
|
519
|
+
: `<span class="user-avatar"><span class="user-avatar-fallback">${esc(Array.from(participant.displayName || participant.username)[0] ?? "作")}</span></span>`;
|
|
520
|
+
return `<div class="presence-person${samePage ? " is-same-page" : ""}">${avatar}<div class="presence-person-copy"><strong>${esc(participant.displayName)}${isCurrent ? "(你)" : ""}</strong><small>${esc(pageLabels)}</small></div>${samePage ? '<span class="presence-same-page">同一页面</span>' : ""}</div>`;
|
|
521
|
+
}).join("");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function refreshPresence() {
|
|
525
|
+
const requestId = ++presenceHeartbeatRequest;
|
|
526
|
+
if (presenceHeartbeatTimer !== null) clearTimeout(presenceHeartbeatTimer);
|
|
527
|
+
presenceHeartbeatTimer = null;
|
|
528
|
+
const workId = state.work?.id;
|
|
529
|
+
const page = presencePageForRoute();
|
|
530
|
+
if (!workId || !page || !state.user) {
|
|
531
|
+
presenceParticipants = [];
|
|
532
|
+
renderPresence();
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const participants = await api(`/api/works/${encodeURIComponent(workId)}/presence`, {
|
|
537
|
+
method: "POST",
|
|
538
|
+
body: { clientId: presenceClientId, page },
|
|
539
|
+
skipOptimisticVersion: true
|
|
540
|
+
});
|
|
541
|
+
if (state.work?.id === workId && presenceHeartbeatRequest === requestId) {
|
|
542
|
+
presenceParticipants = participants;
|
|
543
|
+
renderPresence();
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
if (state.work?.id === workId) renderPresence();
|
|
547
|
+
} finally {
|
|
548
|
+
if (state.work?.id === workId && presenceHeartbeatRequest === requestId) presenceHeartbeatTimer = setTimeout(refreshPresence, presenceHeartbeatInterval);
|
|
549
|
+
}
|
|
550
|
+
return presenceParticipants;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function schedulePresenceHeartbeat() {
|
|
554
|
+
if (presenceHeartbeatQueued !== null) clearTimeout(presenceHeartbeatQueued);
|
|
555
|
+
presenceHeartbeatQueued = setTimeout(() => {
|
|
556
|
+
presenceHeartbeatQueued = null;
|
|
557
|
+
void refreshPresence();
|
|
558
|
+
}, 80);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function confirmConcurrentSave() {
|
|
562
|
+
await refreshPresence();
|
|
563
|
+
const localKey = presencePageKey(presencePageForRoute());
|
|
564
|
+
const peers = presenceParticipants.filter((participant) => participant.clientId !== presenceClientId && participant.page.key === localKey);
|
|
565
|
+
if (!peers.length) return true;
|
|
566
|
+
const names = [...new Set(peers.map((participant) => participant.displayName))];
|
|
567
|
+
return confirmToast(`${names.join("、")}也停留在这个编辑页面。当前项目尚未适配多人同时编辑,继续保存可能覆盖对方的修改。`, {
|
|
568
|
+
title: "检测到同页协作",
|
|
569
|
+
confirmLabel: "仍然保存",
|
|
570
|
+
cancelLabel: "暂不保存"
|
|
571
|
+
});
|
|
412
572
|
}
|
|
413
573
|
|
|
414
574
|
function currentPageRoute() {
|
|
415
575
|
const workId = state.work?.id ?? null;
|
|
416
576
|
if (!$("#entity-editor-view").classList.contains("hidden") && workId && entityEditorType) {
|
|
417
577
|
const entityId = entityEditorType === "setting" ? settingEditorItem?.id : entityEditorType === "character" ? characterEditorItem?.id : knowledgeEditorItem?.id;
|
|
418
|
-
return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null };
|
|
578
|
+
return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null, entityMode: entityEditorReadOnly ? "read" : "edit" };
|
|
419
579
|
}
|
|
420
580
|
if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
|
|
421
581
|
if (!$("#platform-ai-view").classList.contains("hidden")) return { view: "platform-ai", workId, ...settingsRouteContext() };
|
|
@@ -520,6 +680,7 @@ let chapterLineDrag = null;
|
|
|
520
680
|
let chapterWhitespaceVisible = true;
|
|
521
681
|
let chapterAutoSaveTimer = null;
|
|
522
682
|
let chapterSaveInFlight = null;
|
|
683
|
+
let chapterSaveGuardInFlight = null;
|
|
523
684
|
let lastSavedChapterSnapshot = null;
|
|
524
685
|
let moduleNavExpanded = false;
|
|
525
686
|
const chapterAutoSaveDelay = 800;
|
|
@@ -528,6 +689,8 @@ let aiMentionRange = null;
|
|
|
528
689
|
let settingsReturnContext = null;
|
|
529
690
|
let entityEditorType = null;
|
|
530
691
|
let entityEditorDirty = false;
|
|
692
|
+
let entityEditorReadOnly = false;
|
|
693
|
+
let characterListPage = 1;
|
|
531
694
|
let settingEditorItem = null;
|
|
532
695
|
let characterEditorItem = null;
|
|
533
696
|
let knowledgeEditorItem = null;
|
|
@@ -547,9 +710,10 @@ let knowledgeSectionVditor = null;
|
|
|
547
710
|
let characterSectionVditor = null;
|
|
548
711
|
let entityHistoryContext = null;
|
|
549
712
|
|
|
550
|
-
function showEntityEditorPage(type) {
|
|
713
|
+
function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
551
714
|
entityEditorType = type;
|
|
552
715
|
entityEditorDirty = false;
|
|
716
|
+
entityEditorReadOnly = readOnly;
|
|
553
717
|
characterSectionEditorDirty = false;
|
|
554
718
|
knowledgeSectionEditorDirty = false;
|
|
555
719
|
$("#entity-editor-view").classList.remove("hidden");
|
|
@@ -565,7 +729,7 @@ function showEntityEditorPage(type) {
|
|
|
565
729
|
|
|
566
730
|
function markEntityEditorDirty() {
|
|
567
731
|
const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
|
|
568
|
-
if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
|
|
732
|
+
if (entityEditorType && !entityEditorReadOnly && canEditModule(module)) entityEditorDirty = true;
|
|
569
733
|
}
|
|
570
734
|
|
|
571
735
|
async function confirmEntityEditorDiscard(message) {
|
|
@@ -588,6 +752,7 @@ async function closeEntityEditor({ force = false } = {}) {
|
|
|
588
752
|
const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
|
|
589
753
|
entityEditorType = null;
|
|
590
754
|
entityEditorDirty = false;
|
|
755
|
+
entityEditorReadOnly = false;
|
|
591
756
|
settingEditorItem = null;
|
|
592
757
|
characterEditorItem = null;
|
|
593
758
|
knowledgeEditorItem = null;
|
|
@@ -1587,7 +1752,7 @@ function attachOptimisticVersion(path, method, body) {
|
|
|
1587
1752
|
|
|
1588
1753
|
async function api(path, options = {}) {
|
|
1589
1754
|
const method = String(options.method ?? "GET").toUpperCase();
|
|
1590
|
-
const body = attachOptimisticVersion(path, method, options.body);
|
|
1755
|
+
const body = options.skipOptimisticVersion ? options.body : attachOptimisticVersion(path, method, options.body);
|
|
1591
1756
|
const headers = { ...(options.headers ?? {}) };
|
|
1592
1757
|
if (state.csrfToken && !["GET", "HEAD", "OPTIONS"].includes(method)) headers["X-CSRF-Token"] = state.csrfToken;
|
|
1593
1758
|
if (!(body instanceof FormData)) headers["Content-Type"] = "application/json";
|
|
@@ -1624,6 +1789,23 @@ async function apiAllPages(path, limit = 100) {
|
|
|
1624
1789
|
}
|
|
1625
1790
|
}
|
|
1626
1791
|
|
|
1792
|
+
async function initializeProductFooters() {
|
|
1793
|
+
const year = String(new Date().getFullYear());
|
|
1794
|
+
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
1795
|
+
try {
|
|
1796
|
+
const health = await api("/api/health");
|
|
1797
|
+
const version = String(health.version ?? "").trim();
|
|
1798
|
+
document.querySelectorAll("[data-product-footer-version]").forEach((element) => {
|
|
1799
|
+
element.textContent = version ? `v${version}` : "v—";
|
|
1800
|
+
});
|
|
1801
|
+
document.querySelectorAll("[data-product-footer-development]").forEach((element) => {
|
|
1802
|
+
element.classList.toggle("hidden", health.development !== true);
|
|
1803
|
+
});
|
|
1804
|
+
} catch {
|
|
1805
|
+
document.querySelectorAll("[data-product-footer-version]").forEach((element) => { element.textContent = "v—"; });
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1627
1809
|
function selectAuthMode(mode) {
|
|
1628
1810
|
const registerTab = $("#auth-register-tab");
|
|
1629
1811
|
const login = mode === "login" || registerTab.disabled;
|
|
@@ -1685,7 +1867,8 @@ function applyAuthenticatedUser(session) {
|
|
|
1685
1867
|
state.csrfToken = session.csrfToken;
|
|
1686
1868
|
$("#account-name").textContent = session.user.displayName;
|
|
1687
1869
|
renderUserAvatar($("#account-avatar"), session.user);
|
|
1688
|
-
$("#account-menu-name").textContent =
|
|
1870
|
+
$("#account-menu-display-name").textContent = session.user.displayName;
|
|
1871
|
+
$("#account-menu-username").textContent = `@${session.user.username}`;
|
|
1689
1872
|
$("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
|
|
1690
1873
|
$("#auth-view").classList.add("hidden");
|
|
1691
1874
|
document.documentElement.classList.remove("login-route");
|
|
@@ -1787,6 +1970,60 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
1787
1970
|
});
|
|
1788
1971
|
}
|
|
1789
1972
|
|
|
1973
|
+
function inputToast(message, { title = "请输入", inputLabel = title, placeholder = "", confirmLabel = "确认", cancelLabel = "取消", maxLength = 500 } = {}) {
|
|
1974
|
+
const region = $("#toast-region");
|
|
1975
|
+
const element = document.createElement("section");
|
|
1976
|
+
element.className = "toast toast-confirmation toast-input-dialog";
|
|
1977
|
+
element.setAttribute("role", "alertdialog");
|
|
1978
|
+
element.setAttribute("aria-label", title);
|
|
1979
|
+
const heading = document.createElement("strong");
|
|
1980
|
+
heading.textContent = title;
|
|
1981
|
+
const description = document.createElement("p");
|
|
1982
|
+
description.textContent = message;
|
|
1983
|
+
const input = document.createElement("input");
|
|
1984
|
+
input.className = "toast-input";
|
|
1985
|
+
input.type = "text";
|
|
1986
|
+
input.maxLength = maxLength;
|
|
1987
|
+
input.placeholder = placeholder;
|
|
1988
|
+
input.setAttribute("aria-label", inputLabel);
|
|
1989
|
+
const actions = document.createElement("div");
|
|
1990
|
+
actions.className = "toast-confirmation-actions";
|
|
1991
|
+
const cancel = document.createElement("button");
|
|
1992
|
+
cancel.className = "ghost-button";
|
|
1993
|
+
cancel.type = "button";
|
|
1994
|
+
cancel.textContent = cancelLabel;
|
|
1995
|
+
const confirm = document.createElement("button");
|
|
1996
|
+
confirm.className = "primary-button";
|
|
1997
|
+
confirm.type = "button";
|
|
1998
|
+
confirm.textContent = confirmLabel;
|
|
1999
|
+
actions.append(cancel, confirm);
|
|
2000
|
+
element.append(heading, description, input, actions);
|
|
2001
|
+
region.append(element);
|
|
2002
|
+
raiseToastRegion();
|
|
2003
|
+
input.focus();
|
|
2004
|
+
return new Promise((resolve) => {
|
|
2005
|
+
let settled = false;
|
|
2006
|
+
const finish = (value) => {
|
|
2007
|
+
if (settled) return;
|
|
2008
|
+
settled = true;
|
|
2009
|
+
element.remove();
|
|
2010
|
+
if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
|
|
2011
|
+
resolve(value);
|
|
2012
|
+
};
|
|
2013
|
+
cancel.addEventListener("click", () => finish(null), { once: true });
|
|
2014
|
+
confirm.addEventListener("click", () => finish(input.value.trim()), { once: true });
|
|
2015
|
+
element.addEventListener("keydown", (event) => {
|
|
2016
|
+
if (event.key === "Escape") {
|
|
2017
|
+
event.preventDefault();
|
|
2018
|
+
finish(null);
|
|
2019
|
+
} else if (event.key === "Enter") {
|
|
2020
|
+
event.preventDefault();
|
|
2021
|
+
finish(input.value.trim());
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1790
2027
|
document.addEventListener("toggle", (event) => {
|
|
1791
2028
|
const target = event.target;
|
|
1792
2029
|
if (target instanceof HTMLDialogElement && target.open && $("#toast-region").childElementCount) {
|
|
@@ -1821,6 +2058,10 @@ function cancelChapterAutoSave() {
|
|
|
1821
2058
|
function scheduleChapterAutoSave(delay = chapterAutoSaveDelay) {
|
|
1822
2059
|
if (!state.chapter || !canEditProse()) return;
|
|
1823
2060
|
cancelChapterAutoSave();
|
|
2061
|
+
if (collaborationAutoSaveDisabled) {
|
|
2062
|
+
setSaveState("多人协作,自动保存已关闭", true);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
1824
2065
|
setSaveState("等待自动保存", true);
|
|
1825
2066
|
chapterAutoSaveTimer = setTimeout(() => {
|
|
1826
2067
|
chapterAutoSaveTimer = null;
|
|
@@ -1835,12 +2076,23 @@ async function persistChapter({ automatic = false } = {}) {
|
|
|
1835
2076
|
return null;
|
|
1836
2077
|
}
|
|
1837
2078
|
cancelChapterAutoSave();
|
|
2079
|
+
if (automatic) {
|
|
2080
|
+
await refreshPresence();
|
|
2081
|
+
if (collaborationAutoSaveDisabled) {
|
|
2082
|
+
setSaveState("多人协作,自动保存已关闭", true);
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1838
2086
|
if (chapterSaveInFlight) {
|
|
1839
2087
|
await chapterSaveInFlight;
|
|
1840
2088
|
const pendingDraft = chapterDraftSnapshot();
|
|
1841
2089
|
if (!sameChapterSnapshot(pendingDraft, lastSavedChapterSnapshot)) return persistChapter({ automatic });
|
|
1842
2090
|
return state.chapter;
|
|
1843
2091
|
}
|
|
2092
|
+
if (chapterSaveGuardInFlight) {
|
|
2093
|
+
await chapterSaveGuardInFlight;
|
|
2094
|
+
return persistChapter({ automatic });
|
|
2095
|
+
}
|
|
1844
2096
|
const draft = chapterDraftSnapshot();
|
|
1845
2097
|
if (!draft?.title) {
|
|
1846
2098
|
setSaveState("标题不能为空", true);
|
|
@@ -1853,9 +2105,17 @@ async function persistChapter({ automatic = false } = {}) {
|
|
|
1853
2105
|
scheduleChapterLineNumbers();
|
|
1854
2106
|
}
|
|
1855
2107
|
if (sameChapterSnapshot(draft, lastSavedChapterSnapshot)) {
|
|
1856
|
-
setSaveState(automatic ? "已自动保存" : "已保存");
|
|
2108
|
+
setSaveState(automatic ? "已自动保存" : collaborationAutoSaveDisabled ? "已保存 · 自动保存已关闭" : "已保存");
|
|
1857
2109
|
return state.chapter;
|
|
1858
2110
|
}
|
|
2111
|
+
const saveGuard = confirmConcurrentSave();
|
|
2112
|
+
chapterSaveGuardInFlight = saveGuard;
|
|
2113
|
+
const confirmed = await saveGuard;
|
|
2114
|
+
if (chapterSaveGuardInFlight === saveGuard) chapterSaveGuardInFlight = null;
|
|
2115
|
+
if (!confirmed) {
|
|
2116
|
+
setSaveState("检测到同页协作,未保存", true);
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
1859
2119
|
setSaveState(automatic ? "自动保存中" : "保存中", true);
|
|
1860
2120
|
const workId = state.work.id;
|
|
1861
2121
|
const request = (async () => {
|
|
@@ -1877,7 +2137,7 @@ async function persistChapter({ automatic = false } = {}) {
|
|
|
1877
2137
|
updateChapterStats();
|
|
1878
2138
|
const currentDraft = chapterDraftSnapshot();
|
|
1879
2139
|
if (sameChapterSnapshot(currentDraft, draft)) {
|
|
1880
|
-
setSaveState(automatic ? "已自动保存" : "已保存");
|
|
2140
|
+
setSaveState(automatic ? "已自动保存" : collaborationAutoSaveDisabled ? "已保存 · 自动保存已关闭" : "已保存");
|
|
1881
2141
|
if (!automatic) toast(`正文已保存为 v${state.chapter.versionNo}`);
|
|
1882
2142
|
} else {
|
|
1883
2143
|
scheduleChapterAutoSave(250);
|
|
@@ -1949,7 +2209,8 @@ function restoredSettingsReturnContext(route) {
|
|
|
1949
2209
|
}
|
|
1950
2210
|
|
|
1951
2211
|
async function initializePage() {
|
|
1952
|
-
|
|
2212
|
+
const [authenticated] = await Promise.all([initializeAuthentication(), initializeProductFooters()]);
|
|
2213
|
+
if (!authenticated) {
|
|
1953
2214
|
restoringPageRoute = false;
|
|
1954
2215
|
return;
|
|
1955
2216
|
}
|
|
@@ -1983,15 +2244,20 @@ async function initializePage() {
|
|
|
1983
2244
|
if (route.view === "module") return;
|
|
1984
2245
|
if (route.view === "entity-editor") {
|
|
1985
2246
|
const records = route.entity === "setting" ? state.settings : route.entity === "character" ? state.characters : route.entity === "race" ? state.races : state.organizations;
|
|
1986
|
-
const item = route.entityId
|
|
2247
|
+
const item = route.entityId
|
|
2248
|
+
? route.entity === "character"
|
|
2249
|
+
? await api(`/api/characters/${encodeURIComponent(route.entityId)}`)
|
|
2250
|
+
: records.find((record) => record.id === route.entityId)
|
|
2251
|
+
: null;
|
|
1987
2252
|
if (route.entityId && !item) {
|
|
1988
2253
|
toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
|
|
1989
2254
|
return;
|
|
1990
2255
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
else if (route.entity === "
|
|
1994
|
-
else if (route.entity === "
|
|
2256
|
+
const options = { readOnly: route.entityMode === "read" };
|
|
2257
|
+
if (route.entity === "setting") openSettingEditor(item, options);
|
|
2258
|
+
else if (route.entity === "character") await openCharacterEditor(item, options);
|
|
2259
|
+
else if (route.entity === "race") await openRaceDialog(item, options);
|
|
2260
|
+
else if (route.entity === "organization") await openOrganizationDialog(item, options);
|
|
1995
2261
|
return;
|
|
1996
2262
|
}
|
|
1997
2263
|
if (route.view === "welcome") {
|
|
@@ -2211,14 +2477,6 @@ async function openMembersDialog(targetWork = state.work) {
|
|
|
2211
2477
|
} catch (error) { $("#members-dialog").close(); toast(error.message, "error"); }
|
|
2212
2478
|
}
|
|
2213
2479
|
|
|
2214
|
-
const searchResultTypeLabels = {
|
|
2215
|
-
chapter: "章节",
|
|
2216
|
-
setting: "设定",
|
|
2217
|
-
character: "角色",
|
|
2218
|
-
race: "种族",
|
|
2219
|
-
organization: "组织"
|
|
2220
|
-
};
|
|
2221
|
-
|
|
2222
2480
|
async function openSearchDialog() {
|
|
2223
2481
|
if (!state.work) {
|
|
2224
2482
|
toast("请先打开一部作品", "error");
|
|
@@ -2243,7 +2501,7 @@ function renderSearchResults(results) {
|
|
|
2243
2501
|
}
|
|
2244
2502
|
$("#search-results").innerHTML = results.map((item) => `
|
|
2245
2503
|
<button type="button" class="search-result" data-search-type="${esc(item.type)}" data-search-id="${esc(item.id)}">
|
|
2246
|
-
<div class="search-result-meta"><span>${esc(
|
|
2504
|
+
<div class="search-result-meta"><span>${esc(searchResultTypeLabel(item.type))}</span><strong>${esc(item.title)}</strong></div>
|
|
2247
2505
|
<p>${esc(item.snippet || "无摘要")}</p>
|
|
2248
2506
|
</button>`).join("");
|
|
2249
2507
|
$("#search-results").querySelectorAll(".search-result").forEach((button) => {
|
|
@@ -2393,6 +2651,7 @@ function resetWorkScopedUiCaches() {
|
|
|
2393
2651
|
state.models = [];
|
|
2394
2652
|
state.characters = [];
|
|
2395
2653
|
state.settings = [];
|
|
2654
|
+
characterListPage = 1;
|
|
2396
2655
|
state.collapsedVolumeIds.clear();
|
|
2397
2656
|
lastSavedChapterSnapshot = null;
|
|
2398
2657
|
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
@@ -2581,7 +2840,7 @@ const moduleMeta = {
|
|
|
2581
2840
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
2582
2841
|
organizations: ["世界阵营", "组织与成员", "维护组织简介、设定清单,并将角色绑定到所属组织。", "新建组织"],
|
|
2583
2842
|
timeline: ["剧情脉络", "大事件时间轴", "候选事件经作者确认后,才进入正式时间线。", "新建事件"],
|
|
2584
|
-
outlines: ["创作规划", "
|
|
2843
|
+
outlines: ["创作规划", "大纲/伏笔", "为每章维护目标、冲突与转折,并持续提醒尚未回收的伏笔。", "新建伏笔"],
|
|
2585
2844
|
relationships: ["跨章证据", "人物关系", "记录关系方向、阶段、置信度与原文依据。", "新建关系"],
|
|
2586
2845
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
2587
2846
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
@@ -2625,7 +2884,7 @@ async function showModule(module) {
|
|
|
2625
2884
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
2626
2885
|
try {
|
|
2627
2886
|
if (module === "settings") await renderSettings();
|
|
2628
|
-
if (module === "characters") await renderCharacters();
|
|
2887
|
+
if (module === "characters") await renderCharacters(characterListPage);
|
|
2629
2888
|
if (module === "races") await renderRaces();
|
|
2630
2889
|
if (module === "organizations") await renderOrganizations();
|
|
2631
2890
|
if (module === "timeline") await renderTimeline();
|
|
@@ -2818,13 +3077,19 @@ function moduleRowPreview(text, max = 180) {
|
|
|
2818
3077
|
return preview.length > max ? `${preview.slice(0, max)}…` : preview;
|
|
2819
3078
|
}
|
|
2820
3079
|
|
|
3080
|
+
function moduleLayoutIconMarkup(layout) {
|
|
3081
|
+
if (layout === "rows") {
|
|
3082
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 6h13M8 12h13M8 18h13"></path><path d="M3 6h.01M3 12h.01M3 18h.01"></path></svg>';
|
|
3083
|
+
}
|
|
3084
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg>';
|
|
3085
|
+
}
|
|
3086
|
+
|
|
2821
3087
|
function renderModuleLayoutToggle(layout, ariaLabel = "列表样式") {
|
|
2822
3088
|
return `<div class="module-layout-toolbar" data-module-header-action="layout-toggle">
|
|
2823
3089
|
<div class="module-layout-toggle" role="group" aria-label="${esc(ariaLabel)}">
|
|
2824
|
-
<button type="button" data-module-layout="cards" aria-pressed="${layout === "cards"}"
|
|
2825
|
-
<button type="button" data-module-layout="rows" aria-pressed="${layout === "rows"}"
|
|
3090
|
+
<button type="button" data-module-layout="cards" aria-label="卡片视图" title="卡片视图" aria-pressed="${layout === "cards"}">${moduleLayoutIconMarkup("cards")}</button>
|
|
3091
|
+
<button type="button" data-module-layout="rows" aria-label="列表视图" title="列表视图" aria-pressed="${layout === "rows"}">${moduleLayoutIconMarkup("rows")}</button>
|
|
2826
3092
|
</div>
|
|
2827
|
-
<span class="module-layout-hint">当前:${esc(moduleLayoutLabel(layout))}</span>
|
|
2828
3093
|
</div>`;
|
|
2829
3094
|
}
|
|
2830
3095
|
|
|
@@ -2840,6 +3105,56 @@ function bindModuleLayoutToggle(refresh) {
|
|
|
2840
3105
|
}));
|
|
2841
3106
|
}
|
|
2842
3107
|
|
|
3108
|
+
function bindRecordPreview(selector, open) {
|
|
3109
|
+
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3110
|
+
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openReview;
|
|
3111
|
+
card.addEventListener("click", (event) => {
|
|
3112
|
+
if (!event.target.closest("button, a, summary")) void open(id);
|
|
3113
|
+
});
|
|
3114
|
+
card.addEventListener("keydown", (event) => {
|
|
3115
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
3116
|
+
if (event.target.closest("button, a, summary")) return;
|
|
3117
|
+
event.preventDefault();
|
|
3118
|
+
void open(id);
|
|
3119
|
+
});
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function openReviewDetailDialog(item) {
|
|
3124
|
+
if (!item) return;
|
|
3125
|
+
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
|
|
3126
|
+
const entityRefs = Array.isArray(item.entityRefs) ? item.entityRefs : [];
|
|
3127
|
+
const evidenceHtml = evidence.length
|
|
3128
|
+
? `<ul>${evidence.map((entry) => {
|
|
3129
|
+
if (!entry || typeof entry !== "object") return `<li>${esc(String(entry))}</li>`;
|
|
3130
|
+
const source = entry.chapterTitle || entry.chapterId || "相关证据";
|
|
3131
|
+
const quote = entry.quote ? `<blockquote>${esc(entry.quote)}</blockquote>` : "";
|
|
3132
|
+
const supports = entry.supports ? `<small>${esc(entry.supports)}</small>` : "";
|
|
3133
|
+
return `<li><strong>${esc(source)}</strong>${quote}${supports}</li>`;
|
|
3134
|
+
}).join("")}</ul>`
|
|
3135
|
+
: "<p>暂无证据</p>";
|
|
3136
|
+
const entityRefsHtml = entityRefs.length
|
|
3137
|
+
? `<ul>${entityRefs.map((reference) => `<li><code>${esc(JSON.stringify(reference))}</code></li>`).join("")}</ul>`
|
|
3138
|
+
: "<p>未关联资料</p>";
|
|
3139
|
+
openDialog("审核详情",
|
|
3140
|
+
`<div class="task-detail review-detail">
|
|
3141
|
+
<p><strong>问题类型</strong> ${esc(reviewItemTypeLabel(item.itemType))} · ${esc(reviewSeverityLabel(item.severity))} · ${esc(reviewStatusLabel(item.status))}</p>
|
|
3142
|
+
<div><strong>问题说明</strong><pre class="task-detail-result">${esc(item.description || "暂无说明")}</pre></div>
|
|
3143
|
+
<div><strong>处理建议</strong><pre class="task-detail-result">${esc(item.suggestion || "暂无建议")}</pre></div>
|
|
3144
|
+
<div><strong>相关证据</strong>${evidenceHtml}</div>
|
|
3145
|
+
<div><strong>关联资料</strong>${entityRefsHtml}</div>
|
|
3146
|
+
${item.resolutionNote ? `<div><strong>处理结果</strong><pre class="task-detail-result">${esc(item.resolutionNote)}</pre></div>` : ""}
|
|
3147
|
+
</div>`,
|
|
3148
|
+
async () => undefined,
|
|
3149
|
+
"审核建议",
|
|
3150
|
+
{
|
|
3151
|
+
submitLabel: "关闭",
|
|
3152
|
+
wide: true,
|
|
3153
|
+
hideCancel: true,
|
|
3154
|
+
meta: `创建于 ${formatDateTime(item.createdAt)} · 更新于 ${formatDateTime(item.updatedAt)}`
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
|
|
2843
3158
|
function settingRecordActions(item) {
|
|
2844
3159
|
return canEditModule("settings")
|
|
2845
3160
|
? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
|
|
@@ -2848,7 +3163,7 @@ function settingRecordActions(item) {
|
|
|
2848
3163
|
|
|
2849
3164
|
function renderSettingCards(records) {
|
|
2850
3165
|
return `<div class="card-grid">${records.map((item) => `
|
|
2851
|
-
<article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
|
|
3166
|
+
<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>
|
|
2852
3167
|
<h3>${esc(item.title)}</h3><div class="record-markdown-preview message-body">${renderMarkdown(item.content) || '<p class="markdown-editor-empty">暂无正文</p>'}</div>
|
|
2853
3168
|
<div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
|
|
2854
3169
|
}
|
|
@@ -2857,7 +3172,7 @@ function renderSettingRows(records) {
|
|
|
2857
3172
|
return `<div class="module-row-list">${records.map((item) => {
|
|
2858
3173
|
const preview = moduleRowPreview(item.content);
|
|
2859
3174
|
return `
|
|
2860
|
-
<article class="record-card module-row"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
|
|
3175
|
+
<article class="record-card module-row 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>
|
|
2861
3176
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
2862
3177
|
<div class="card-actions">${settingRecordActions(item)}</div></article>`;
|
|
2863
3178
|
}).join("")}</div>`;
|
|
@@ -2872,22 +3187,26 @@ async function renderSettings() {
|
|
|
2872
3187
|
? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
|
|
2873
3188
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
2874
3189
|
bindModuleLayoutToggle(renderSettings);
|
|
3190
|
+
bindRecordPreview("[data-open-setting]", (id) => openSettingEditor(records.find((item) => item.id === id), { readOnly: true }));
|
|
2875
3191
|
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => openSettingEditor(records.find((item) => item.id === button.dataset.editSetting))));
|
|
2876
3192
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
2877
3193
|
}
|
|
2878
3194
|
|
|
2879
|
-
async function renderCharacters() {
|
|
2880
|
-
[
|
|
2881
|
-
apiPage(`/api/works/${state.work.id}/characters
|
|
3195
|
+
async function renderCharacters(page = characterListPage) {
|
|
3196
|
+
const [characterPage, races, organizations] = await Promise.all([
|
|
3197
|
+
apiPage(`/api/works/${state.work.id}/characters`, page),
|
|
2882
3198
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
2883
3199
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
2884
3200
|
]);
|
|
3201
|
+
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3202
|
+
characterListPage = characterPage.page;
|
|
3203
|
+
[state.characters, state.races, state.organizations] = [characterPage.items, races, organizations];
|
|
2885
3204
|
const layout = readModuleLayout();
|
|
2886
3205
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
2887
3206
|
const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
|
|
2888
3207
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
2889
3208
|
return `
|
|
2890
|
-
<article class="record-card character-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
|
|
3209
|
+
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(characterVisibilityLabel(item.visibility))}</small>
|
|
2891
3210
|
<h3>${esc(item.name)}</h3>
|
|
2892
3211
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
2893
3212
|
${item.aliases.length ? `<div class="character-aliases">${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
|
|
@@ -2909,19 +3228,32 @@ async function renderCharacters() {
|
|
|
2909
3228
|
].filter(Boolean).join(" · ");
|
|
2910
3229
|
const line = meta ? `${meta} · ${preview}` : preview;
|
|
2911
3230
|
return `
|
|
2912
|
-
<article class="record-card module-row character-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
|
|
2913
|
-
<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
|
|
3231
|
+
<article class="record-card module-row character-card preview-record-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
|
|
3232
|
+
<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(characterVisibilityLabel(item.visibility))}</small>
|
|
2914
3233
|
<h3>${esc(item.name)}</h3>
|
|
2915
3234
|
<p class="module-row-preview" title="${esc(line)}">${esc(line)}</p>
|
|
2916
3235
|
<div class="card-actions">${characterActions(item)}</div>
|
|
2917
3236
|
</article>`;
|
|
2918
3237
|
}).join("")}</div>`;
|
|
2919
|
-
const
|
|
3238
|
+
const hasMultipleCharacters = characterPage.page > 1 || characterPage.hasMore || state.characters.length > 1;
|
|
3239
|
+
const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${hasMultipleCharacters ? "" : "disabled"}>AI 角色查重</button></section>` : "";
|
|
3240
|
+
const pagination = state.characters.length && (characterPage.page > 1 || characterPage.hasMore)
|
|
3241
|
+
? `<nav class="module-pagination" aria-label="角色列表分页">
|
|
3242
|
+
<button type="button" data-character-page="${characterPage.page - 1}" ${characterPage.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
3243
|
+
<span>第 ${characterPage.page} 页 · 本页 ${state.characters.length} 个角色</span>
|
|
3244
|
+
<button type="button" data-character-page="${characterPage.nextPage ?? characterPage.page + 1}" ${characterPage.hasMore ? "" : "disabled"}>下一页</button>
|
|
3245
|
+
</nav>`
|
|
3246
|
+
: "";
|
|
2920
3247
|
if (state.characters.length) mountModuleLayoutToggle(layout, "角色列表样式");
|
|
2921
3248
|
$("#module-content").innerHTML = auditPanel + (state.characters.length
|
|
2922
|
-
? `${layout === "rows" ? characterRows() : characterCards()}`
|
|
3249
|
+
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
2923
3250
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
2924
3251
|
bindModuleLayoutToggle(renderCharacters);
|
|
3252
|
+
$("#module-content").querySelectorAll("[data-character-page]").forEach((button) => button.addEventListener("click", async () => {
|
|
3253
|
+
if (button.disabled) return;
|
|
3254
|
+
$("#module-content").querySelectorAll("[data-character-page]").forEach((control) => { control.disabled = true; });
|
|
3255
|
+
await renderCharacters(Number(button.dataset.characterPage));
|
|
3256
|
+
}));
|
|
2925
3257
|
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
2926
3258
|
const button = $("#create-character-audit-task");
|
|
2927
3259
|
button.disabled = true;
|
|
@@ -2934,15 +3266,7 @@ async function renderCharacters() {
|
|
|
2934
3266
|
button.disabled = false;
|
|
2935
3267
|
}
|
|
2936
3268
|
});
|
|
2937
|
-
|
|
2938
|
-
const open = () => openCharacterEditor(state.characters.find((item) => item.id === card.dataset.openCharacter));
|
|
2939
|
-
card.addEventListener("click", (event) => { if (!event.target.closest("button")) open(); });
|
|
2940
|
-
card.addEventListener("keydown", (event) => {
|
|
2941
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2942
|
-
event.preventDefault();
|
|
2943
|
-
open();
|
|
2944
|
-
});
|
|
2945
|
-
});
|
|
3269
|
+
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(state.characters.find((item) => item.id === id), { readOnly: true }));
|
|
2946
3270
|
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
|
|
2947
3271
|
}
|
|
2948
3272
|
|
|
@@ -2962,7 +3286,7 @@ async function renderRaces() {
|
|
|
2962
3286
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
2963
3287
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
2964
3288
|
<div class="race-tree-branch">
|
|
2965
|
-
<article class="record-card race-card${canEditRaces ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
|
|
3289
|
+
<article class="record-card race-card preview-record-card${canEditRaces ? " has-card-edit" : ""}" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
|
|
2966
3290
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
2967
3291
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
2968
3292
|
<div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<section class="knowledge-markdown-block${setting.inherited ? " inherited" : ""}"><div class="knowledge-markdown-block-heading"><h4>${esc(setting.title || "未命名章节")}</h4><small>${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}</small></div><div class="message-body">${renderMarkdown(setting.value) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></section>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
|
|
@@ -2976,7 +3300,7 @@ async function renderRaces() {
|
|
|
2976
3300
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
2977
3301
|
const meta = `${item.memberIds.length} 位直接角色 · ${item.settings.length ? "已填写共同设定" : "暂无共同设定"}`;
|
|
2978
3302
|
return `
|
|
2979
|
-
<article class="record-card module-row race-card">
|
|
3303
|
+
<article class="record-card module-row race-card preview-record-card" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}">
|
|
2980
3304
|
<small>${esc(meta)}</small>
|
|
2981
3305
|
<h3>${esc(item.name)}<span class="module-row-path">${esc(racePathLabel(item))}</span></h3>
|
|
2982
3306
|
<p class="module-row-preview" title="${esc(preview)}">${esc(preview)}${item.members.length ? ` · ${esc(item.members.map((member) => member.name).join("、"))}` : ""}</p>
|
|
@@ -2988,6 +3312,7 @@ async function renderRaces() {
|
|
|
2988
3312
|
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
|
|
2989
3313
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
2990
3314
|
bindModuleLayoutToggle(renderRaces);
|
|
3315
|
+
bindRecordPreview("[data-open-race]", (id) => openRaceDialog(state.races.find((item) => item.id === id), { readOnly: true }));
|
|
2991
3316
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
|
|
2992
3317
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
2993
3318
|
}
|
|
@@ -3064,7 +3389,7 @@ async function renderTimeline() {
|
|
|
3064
3389
|
$("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
|
|
3065
3390
|
state.timelineTracks = tracks;
|
|
3066
3391
|
const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
|
|
3067
|
-
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(item.status)}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3392
|
+
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(timelineStatusLabel(item.status))}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3068
3393
|
$("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
|
|
3069
3394
|
const laneEvents = events.filter((item) => (item.trackId ?? "") === track.id);
|
|
3070
3395
|
return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
|
|
@@ -3102,24 +3427,22 @@ async function renderOutlines() {
|
|
|
3102
3427
|
const layout = readModuleLayout();
|
|
3103
3428
|
const unresolved = foreshadows.filter((item) => item.unresolved);
|
|
3104
3429
|
const overdue = unresolved.filter((item) => item.overdue);
|
|
3105
|
-
const navButton = $("#module-nav [data-module=outlines] .nav-label");
|
|
3106
|
-
if (navButton) navButton.textContent = unresolved.length ? `大纲与伏笔 · ${unresolved.length}` : "大纲与伏笔";
|
|
3107
3430
|
const foreshadowActions = (item) => `<button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
|
|
3108
3431
|
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
|
|
3109
3432
|
<article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
|
|
3110
|
-
<small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3433
|
+
<small>${esc(levelLabel(item.importance))} · ${esc(foreshadowStatusLabel(item.status))}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3111
3434
|
<h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
|
|
3112
|
-
<div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc(
|
|
3435
|
+
<div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc(occurrenceRoleLabel(link.role))} · ${esc(link.volumeTitle)} / ${esc(link.chapterTitle)}</span>`).join("") : '<span class="pill">尚未关联章节</span>'}</div>
|
|
3113
3436
|
<div class="card-actions">${foreshadowActions(item)}</div>
|
|
3114
3437
|
</article>`).join("")}</div>`;
|
|
3115
3438
|
const foreshadowRows = () => `<div class="module-row-list">${foreshadows.map((item) => {
|
|
3116
3439
|
const preview = moduleRowPreview(item.description || "暂无说明");
|
|
3117
3440
|
const links = item.occurrences.length
|
|
3118
|
-
? item.occurrences.map((link) => `${(
|
|
3441
|
+
? item.occurrences.map((link) => `${occurrenceRoleLabel(link.role)} · ${link.volumeTitle} / ${link.chapterTitle}`).join(";")
|
|
3119
3442
|
: "尚未关联章节";
|
|
3120
3443
|
return `
|
|
3121
3444
|
<article class="record-card module-row foreshadow-card ${item.overdue ? "is-overdue" : ""}">
|
|
3122
|
-
<small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3445
|
+
<small>${esc(levelLabel(item.importance))} · ${esc(foreshadowStatusLabel(item.status))}${item.overdue ? " · 已逾期" : ""}</small>
|
|
3123
3446
|
<h3>${esc(item.title)}</h3>
|
|
3124
3447
|
<p class="module-row-preview" title="${esc(`${preview} · ${links}`)}">${esc(preview)} · ${esc(links)}</p>
|
|
3125
3448
|
<div class="card-actions">${foreshadowActions(item)}</div>
|
|
@@ -3131,7 +3454,7 @@ async function renderOutlines() {
|
|
|
3131
3454
|
if (foreshadows.length) mountModuleLayoutToggle(layout, "伏笔列表样式");
|
|
3132
3455
|
const outlineHtml = outlines.length ? `<div class="outline-list">${outlines.map((item) => `
|
|
3133
3456
|
<article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
|
|
3134
|
-
<div><small>${esc(item.volumeTitle)} · ${esc(item.status)}</small><h3>${esc(item.chapterTitle)}</h3></div>
|
|
3457
|
+
<div><small>${esc(item.volumeTitle)} · ${esc(outlineStatusLabel(item.status))}</small><h3>${esc(item.chapterTitle)}</h3></div>
|
|
3135
3458
|
<div><b>目标</b><p>${esc(item.goal || "未填写")}</p></div>
|
|
3136
3459
|
<div><b>冲突</b><p>${esc(item.conflict || "未填写")}</p></div>
|
|
3137
3460
|
<div><b>转折</b><p>${esc(item.turningPoint || "未填写")}</p></div>
|
|
@@ -3155,7 +3478,7 @@ async function renderRelationships() {
|
|
|
3155
3478
|
state.relationshipGraph = graph;
|
|
3156
3479
|
$("#module-content").innerHTML = `<div id="relationship-map-host"></div>${relationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${relationships.map((item) => `
|
|
3157
3480
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3158
|
-
<td>${esc(item.category)} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(item.confirmationStatus)}</td><td class="relationship-actions"><button data-edit-relationship="${esc(item.id)}">编辑</button><button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'}`;
|
|
3481
|
+
<td>${esc(relationshipCategoryLabel(item.category))} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(relationshipConfirmationLabel(item.confirmationStatus))}</td><td class="relationship-actions"><button data-edit-relationship="${esc(item.id)}">编辑</button><button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'}`;
|
|
3159
3482
|
const openGalaxy = () => {
|
|
3160
3483
|
state.galaxy?.destroy();
|
|
3161
3484
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3199,11 +3522,11 @@ async function renderReviews() {
|
|
|
3199
3522
|
const actions = mergeActions || keepSeparateAction
|
|
3200
3523
|
? `<div class="card-actions character-duplicate-actions">${mergeActions}${keepSeparateAction}</div>`
|
|
3201
3524
|
: "";
|
|
3202
|
-
return `<article class="record-card character-duplicate-review"><small>角色查重 · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3><div class="character-duplicate-pair">${sideHtml}</div><p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>${evidenceHtml ? `<ul class="character-duplicate-evidence">${evidenceHtml}</ul>` : ""}${actions}${item.resolutionNote ? `<p class="review-resolution-note">处理结果:${esc(item.resolutionNote)}</p>` : ""}</article>`;
|
|
3525
|
+
return `<article class="record-card character-duplicate-review preview-record-card" data-open-review="${esc(item.id)}" role="button" tabindex="0" aria-label="查看审核建议 ${esc(item.title)}"><small>角色查重 · ${esc(reviewSeverityLabel(item.severity))} · ${esc(reviewStatusLabel(item.status))}</small><h3>${esc(item.title)}</h3><div class="character-duplicate-pair">${sideHtml}</div><p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>${evidenceHtml ? `<ul class="character-duplicate-evidence">${evidenceHtml}</ul>` : ""}${actions}${item.resolutionNote ? `<p class="review-resolution-note">处理结果:${esc(item.resolutionNote)}</p>` : ""}</article>`;
|
|
3203
3526
|
};
|
|
3204
3527
|
const layout = readModuleLayout();
|
|
3205
3528
|
const reviewCard = (item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
|
|
3206
|
-
<article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
|
|
3529
|
+
<article class="record-card preview-record-card" data-open-review="${esc(item.id)}" role="button" tabindex="0" aria-label="查看审核建议 ${esc(item.title)}"><small>${esc(reviewItemTypeLabel(item.itemType))} · ${esc(reviewSeverityLabel(item.severity))} · ${esc(reviewStatusLabel(item.status))}</small><h3>${esc(item.title)}</h3>
|
|
3207
3530
|
<p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>
|
|
3208
3531
|
${item.status === "pending" && canResolveReview ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`;
|
|
3209
3532
|
const reviewRow = (item) => {
|
|
@@ -3212,8 +3535,8 @@ async function renderReviews() {
|
|
|
3212
3535
|
}
|
|
3213
3536
|
const preview = moduleRowPreview(`${item.description || ""}${item.suggestion ? ` 建议:${item.suggestion}` : ""}`);
|
|
3214
3537
|
return `
|
|
3215
|
-
<article class="record-card module-row">
|
|
3216
|
-
<small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small>
|
|
3538
|
+
<article class="record-card module-row preview-record-card" data-open-review="${esc(item.id)}" role="button" tabindex="0" aria-label="查看审核建议 ${esc(item.title)}">
|
|
3539
|
+
<small>${esc(reviewItemTypeLabel(item.itemType))} · ${esc(reviewSeverityLabel(item.severity))} · ${esc(reviewStatusLabel(item.status))}</small>
|
|
3217
3540
|
<h3>${esc(item.title)}</h3>
|
|
3218
3541
|
<p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
3219
3542
|
<div class="card-actions">${item.status === "pending" && canResolveReview ? `<button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button>` : ""}</div>
|
|
@@ -3224,6 +3547,7 @@ async function renderReviews() {
|
|
|
3224
3547
|
? `${layout === "rows" ? `<div class="module-row-list">${reviews.map(reviewRow).join("")}</div>` : `<div class="card-grid">${reviews.map(reviewCard).join("")}</div>`}`
|
|
3225
3548
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
3226
3549
|
bindModuleLayoutToggle(renderReviews);
|
|
3550
|
+
bindRecordPreview("[data-open-review]", (id) => openReviewDetailDialog(reviews.find((item) => item.id === id)));
|
|
3227
3551
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
3228
3552
|
await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
|
|
3229
3553
|
await renderReviews();
|
|
@@ -3293,8 +3617,8 @@ async function renderTasks() {
|
|
|
3293
3617
|
</section>
|
|
3294
3618
|
${tasks.length ? `<table class="table-list task-table"><thead><tr><th>分析类型</th><th>范围</th><th>状态</th><th>进度</th><th>操作</th></tr></thead><tbody>${tasks.map((item) => `
|
|
3295
3619
|
<tr>
|
|
3296
|
-
<td>${esc(analysisTaskTypeLabel(item.taskType))}
|
|
3297
|
-
<td>${esc(item.scopeSummary || item.scope?.type || "book")}</td>
|
|
3620
|
+
<td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
|
|
3621
|
+
<td>${esc(item.scopeSummary || taskScopeLabel(item.scope?.type || "book"))}</td>
|
|
3298
3622
|
<td>${esc(analysisTaskStatusLabel(item.status))}</td>
|
|
3299
3623
|
<td>${Number(item.progress ?? 0)}%</td>
|
|
3300
3624
|
<td class="task-row-actions">
|
|
@@ -3402,7 +3726,7 @@ function openTaskDetailDialog(task) {
|
|
|
3402
3726
|
openDialog("任务详情",
|
|
3403
3727
|
`<div class="task-detail">
|
|
3404
3728
|
<p><strong>任务 ID</strong><br><code>${esc(task.id)}</code></p>
|
|
3405
|
-
<p><strong>类型</strong> ${esc(analysisTaskTypeLabel(task.taskType))}
|
|
3729
|
+
<p><strong>类型</strong> ${esc(analysisTaskTypeLabel(task.taskType))}</p>
|
|
3406
3730
|
<p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
|
|
3407
3731
|
<p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
|
|
3408
3732
|
<div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
|
|
@@ -3417,11 +3741,11 @@ function openTaskDetailDialog(task) {
|
|
|
3417
3741
|
|
|
3418
3742
|
function renderProviderCards(providers, models) {
|
|
3419
3743
|
return providers.length ? `<div class="card-grid provider-card-grid">${providers.map((provider) => `
|
|
3420
|
-
<article class="record-card provider-card"><small>平台级 · ${esc(provider.status)} · ${esc(provider.connectionStatus)}</small><h3>${esc(provider.name)}</h3>
|
|
3421
|
-
<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n并发:${provider.concurrencyLimit} ·
|
|
3422
|
-
<div class="provider-models">${models.filter((model) => model.providerId === provider.id).map((model) => `<button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} ·
|
|
3744
|
+
<article class="record-card provider-card"><small>平台级 · ${esc(providerStatusLabel(provider.status))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><h3>${esc(provider.name)}</h3>
|
|
3745
|
+
<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit} · 最大输出:${provider.maxTokens ?? 32000}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
3746
|
+
<div class="provider-models">${models.filter((model) => model.providerId === provider.id).map((model) => `<button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>`).join("")}</div>
|
|
3423
3747
|
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button><button data-test-provider="${esc(provider.id)}">测试连接</button><button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`).join("")}</div>`
|
|
3424
|
-
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI
|
|
3748
|
+
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
3425
3749
|
}
|
|
3426
3750
|
|
|
3427
3751
|
function bindPlatformProviderActions(host, providers, models) {
|
|
@@ -3442,10 +3766,10 @@ function renderTaskDefaults(models, providers, taskDefaults) {
|
|
|
3442
3766
|
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
3443
3767
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
3444
3768
|
return models.length ? `<section class="config-section">
|
|
3445
|
-
<div class="config-section-header"><div><h2>本书任务默认模型</h2><p
|
|
3769
|
+
<div class="config-section-header"><div><h2>本书任务默认模型</h2><p>选择平台模型作为当前作品的默认模型;所有请求都会携带最大输出令牌数,默认值为 32000。</p></div></div>
|
|
3446
3770
|
<table class="table-list"><thead><tr><th>任务能力</th><th>默认模型</th></tr></thead><tbody>${taskTypeLabels.map(([taskType, label]) => {
|
|
3447
3771
|
const currentModelId = defaultModelByTask.get(taskType) ?? "";
|
|
3448
|
-
return `<tr><td>${esc(label)}
|
|
3772
|
+
return `<tr><td>${esc(label)}</td><td><select class="default-model-select" data-task-default="${esc(taskType)}">
|
|
3449
3773
|
<option value="" disabled ${currentModelId ? "" : "selected"}>请选择模型</option>
|
|
3450
3774
|
${models.map((model) => {
|
|
3451
3775
|
const provider = providerById.get(model.providerId);
|
|
@@ -3813,7 +4137,7 @@ function renderKnowledgeMarkdownSections() {
|
|
|
3813
4137
|
const host = $("#knowledge-markdown-sections");
|
|
3814
4138
|
if (!host) return;
|
|
3815
4139
|
const label = knowledgeEditorKind === "race" ? "种族" : "组织";
|
|
3816
|
-
const canEdit = canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations");
|
|
4140
|
+
const canEdit = !entityEditorReadOnly && canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations");
|
|
3817
4141
|
const sections = Array.isArray(knowledgeEditorSections) ? knowledgeEditorSections : [];
|
|
3818
4142
|
host.innerHTML = `<div class="knowledge-markdown-list-toolbar"><div><b>${label} Markdown 设定</b><span>将每条设定单独保存为章节,需要编辑时打开大编辑器。</span></div>${canEdit ? '<button type="button" class="ghost-button" data-knowledge-section-create>新建设定</button>' : ""}</div>${sections.length ? `<div class="knowledge-markdown-list">${sections.map((section, index) => `<article class="knowledge-markdown-section" data-knowledge-section-index="${index}"><header><div><span>设定 ${index + 1}</span><h4>${esc(section.title || `未命名设定 ${index + 1}`)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${canEdit ? `<button type="button" data-knowledge-section-edit="${index}">编辑</button><button type="button" data-knowledge-section-delete="${index}">删除</button>` : ""}</div></header><p class="knowledge-section-card-preview">${esc(knowledgeSectionPreviewText(section))}</p></article>`).join("")}</div>` : '<p class="knowledge-markdown-empty">还没有 Markdown 设定,点击“新建设定”开始记录。</p>'}`;
|
|
3819
4143
|
host.querySelector("[data-knowledge-section-create]")?.addEventListener("click", () => void openKnowledgeSectionEditor());
|
|
@@ -3917,8 +4241,11 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
3917
4241
|
void discardPendingMarkdownAttachments();
|
|
3918
4242
|
$("#dialog-title").textContent = title;
|
|
3919
4243
|
$("#dialog-eyebrow").textContent = eyebrow;
|
|
4244
|
+
$("#dialog-meta").textContent = options.meta ?? "";
|
|
4245
|
+
$("#dialog-meta").classList.toggle("hidden", !options.meta);
|
|
3920
4246
|
$("#dialog-fields").innerHTML = fields;
|
|
3921
4247
|
$("#dialog-submit").textContent = options.submitLabel ?? "保存";
|
|
4248
|
+
$("#dynamic-form .dialog-actions [value='cancel']").classList.toggle("hidden", Boolean(options.hideCancel));
|
|
3922
4249
|
$("#form-dialog").classList.toggle("wide-dialog", Boolean(options.wide));
|
|
3923
4250
|
bindDynamicListControls($("#dialog-fields"));
|
|
3924
4251
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
@@ -4094,27 +4421,32 @@ function openVolumeDialog(item) {
|
|
|
4094
4421
|
}, "分卷设置");
|
|
4095
4422
|
}
|
|
4096
4423
|
|
|
4097
|
-
function openSettingEditor(item = null) {
|
|
4424
|
+
function openSettingEditor(item = null, { readOnly = false } = {}) {
|
|
4425
|
+
entityEditorReadOnly = readOnly;
|
|
4098
4426
|
destroyVditorEditor(settingEditorVditor);
|
|
4099
4427
|
settingEditorVditor = null;
|
|
4100
4428
|
settingEditorItem = item;
|
|
4101
|
-
$("#setting-editor-eyebrow").textContent = item ? "
|
|
4429
|
+
$("#setting-editor-eyebrow").textContent = item ? "编辑设定" : "新建设定";
|
|
4102
4430
|
$("#setting-editor-name").value = item?.title ?? "";
|
|
4103
4431
|
$("#setting-editor-category").value = item?.category ?? "世界规则";
|
|
4104
4432
|
$("#setting-editor-locked").checked = Boolean(item?.locked);
|
|
4105
4433
|
$("#setting-editor-body").value = item?.content ?? "";
|
|
4106
|
-
$("#setting-change-note").value = "";
|
|
4107
|
-
$("#setting-change-note-field").classList.toggle("hidden", !item);
|
|
4108
4434
|
$("#setting-editor-submit").textContent = item ? "保存新版本" : "创建设定";
|
|
4109
|
-
const viewOnly = !canEditModule("settings");
|
|
4435
|
+
const viewOnly = readOnly || !canEditModule("settings");
|
|
4436
|
+
$("#setting-editor-form").classList.toggle("is-read-only", viewOnly);
|
|
4437
|
+
$("#setting-editor-eyebrow").textContent = readOnly ? "阅读设定" : item ? "编辑设定" : "新建设定";
|
|
4110
4438
|
$("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
|
|
4111
4439
|
$("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
|
|
4112
4440
|
$("#setting-editor-submit").classList.toggle("hidden", viewOnly);
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4441
|
+
const editButton = $("#setting-editor-edit");
|
|
4442
|
+
editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
|
|
4443
|
+
editButton.onclick = () => openSettingEditor(item);
|
|
4444
|
+
const showManagementActions = Boolean(item && !viewOnly);
|
|
4115
4445
|
const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
|
|
4116
|
-
$("#setting-editor-confirm").classList.toggle("hidden", item?.status !== "pending");
|
|
4117
|
-
$("#setting-editor-deprecate").classList.toggle("hidden", item?.status !== "pending");
|
|
4446
|
+
$("#setting-editor-confirm").classList.toggle("hidden", !showManagementActions || item?.status !== "pending");
|
|
4447
|
+
$("#setting-editor-deprecate").classList.toggle("hidden", !showManagementActions || item?.status !== "pending");
|
|
4448
|
+
$("#setting-editor-history").classList.toggle("hidden", !showManagementActions);
|
|
4449
|
+
$("#setting-editor-delete").classList.toggle("hidden", !showManagementActions);
|
|
4118
4450
|
$("#setting-editor-history").onclick = async () => {
|
|
4119
4451
|
if (!item) return;
|
|
4120
4452
|
if (!(await closeEntityEditor())) return;
|
|
@@ -4149,7 +4481,7 @@ function openSettingEditor(item = null) {
|
|
|
4149
4481
|
});
|
|
4150
4482
|
$("#setting-editor-form").onsubmit = async (event) => {
|
|
4151
4483
|
event.preventDefault();
|
|
4152
|
-
if (!canEditModule("settings")) return;
|
|
4484
|
+
if (readOnly || !canEditModule("settings")) return;
|
|
4153
4485
|
const form = new FormData(event.currentTarget);
|
|
4154
4486
|
const submit = $("#setting-editor-submit");
|
|
4155
4487
|
submit.disabled = true;
|
|
@@ -4166,6 +4498,13 @@ function openSettingEditor(item = null) {
|
|
|
4166
4498
|
$("#setting-editor-body").focus();
|
|
4167
4499
|
return;
|
|
4168
4500
|
}
|
|
4501
|
+
const changeNote = item ? await inputToast("简要说明这次修改,便于以后查看版本历史。可以留空。", {
|
|
4502
|
+
title: "填写版本说明",
|
|
4503
|
+
inputLabel: "版本说明",
|
|
4504
|
+
placeholder: "例如:补充纪元历法限制",
|
|
4505
|
+
confirmLabel: "保存新版本"
|
|
4506
|
+
}) : "";
|
|
4507
|
+
if (changeNote === null) return;
|
|
4169
4508
|
const locked = form.get("locked") === "on";
|
|
4170
4509
|
const body = {
|
|
4171
4510
|
title,
|
|
@@ -4173,8 +4512,9 @@ function openSettingEditor(item = null) {
|
|
|
4173
4512
|
content,
|
|
4174
4513
|
locked,
|
|
4175
4514
|
status: locked ? "confirmed" : (item?.status ?? "draft"),
|
|
4176
|
-
...(item ? { changeNote
|
|
4515
|
+
...(item ? { changeNote } : {})
|
|
4177
4516
|
};
|
|
4517
|
+
if (!(await confirmConcurrentSave())) return;
|
|
4178
4518
|
await api(item ? `/api/settings/${item.id}` : `/api/works/${state.work.id}/settings`, { method: item ? "PATCH" : "POST", body });
|
|
4179
4519
|
await cleanupPendingMarkdownAttachments(body.content);
|
|
4180
4520
|
entityEditorDirty = false;
|
|
@@ -4187,12 +4527,12 @@ function openSettingEditor(item = null) {
|
|
|
4187
4527
|
submit.disabled = false;
|
|
4188
4528
|
}
|
|
4189
4529
|
};
|
|
4190
|
-
showEntityEditorPage("setting");
|
|
4530
|
+
showEntityEditorPage("setting", { readOnly });
|
|
4191
4531
|
settingEditorVditor = createVditorEditor($("#setting-editor-markdown"), item?.content ?? "", {
|
|
4192
4532
|
onInput: (markdown) => { $("#setting-editor-body").value = markdown; markEntityEditorDirty(); },
|
|
4193
4533
|
readOnly: viewOnly
|
|
4194
4534
|
});
|
|
4195
|
-
$("#setting-editor-name").focus();
|
|
4535
|
+
(readOnly ? $("#setting-editor-back") : $("#setting-editor-name")).focus();
|
|
4196
4536
|
}
|
|
4197
4537
|
|
|
4198
4538
|
function characterEditorSection(key, title, description, content) {
|
|
@@ -4219,14 +4559,6 @@ function setCharacterHistoryVisible(visible) {
|
|
|
4219
4559
|
$("#character-history-button").setAttribute("aria-expanded", String(visible));
|
|
4220
4560
|
}
|
|
4221
4561
|
|
|
4222
|
-
const relationshipCategoryLabels = {
|
|
4223
|
-
family: "亲属",
|
|
4224
|
-
social: "社交",
|
|
4225
|
-
emotional: "情感",
|
|
4226
|
-
conflict: "冲突",
|
|
4227
|
-
uncertain: "未确定"
|
|
4228
|
-
};
|
|
4229
|
-
|
|
4230
4562
|
function renderCharacterEditorRelationships() {
|
|
4231
4563
|
const host = $("#character-editor-relationships");
|
|
4232
4564
|
if (!host) return;
|
|
@@ -4244,15 +4576,15 @@ function renderCharacterEditorRelationships() {
|
|
|
4244
4576
|
const isSource = relationship.fromCharacterId === characterId;
|
|
4245
4577
|
const otherCharacterId = isSource ? relationship.toCharacterId : relationship.fromCharacterId;
|
|
4246
4578
|
const direction = relationship.directed ? (isSource ? "→" : "←") : "↔";
|
|
4247
|
-
const category =
|
|
4579
|
+
const category = relationshipCategoryLabel(relationship.category);
|
|
4248
4580
|
const relationLabel = [category, relationship.subtype].filter(Boolean).join(" · ") || "未细分";
|
|
4249
4581
|
const keywords = Array.isArray(relationship.keywords) ? relationship.keywords : [];
|
|
4250
4582
|
return `<article class="character-relationship-row">
|
|
4251
|
-
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${canEditModule("relationships") ? `<button type="button" data-character-relationship-edit="${esc(relationship.id)}">编辑关系</button>` : ""}</div>
|
|
4583
|
+
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${!entityEditorReadOnly && canEditModule("relationships") ? `<button type="button" data-character-relationship-edit="${esc(relationship.id)}">编辑关系</button>` : ""}</div>
|
|
4252
4584
|
<div class="character-relationship-keywords"><small>关系关键词</small><div>${keywords.map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || '<span class="character-relationship-empty-keywords">未填写关键词</span>'}</div></div>
|
|
4253
4585
|
</article>`;
|
|
4254
4586
|
}).join("");
|
|
4255
|
-
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${canEditModule("relationships") ? '<button type="button" class="ghost-button" data-character-relationship-create>新建关系</button>' : ""}</div>${rows || '<p class="character-relationship-status">暂未记录与其他人物的关系。</p>'}`;
|
|
4587
|
+
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${!entityEditorReadOnly && canEditModule("relationships") ? '<button type="button" class="ghost-button" data-character-relationship-create>新建关系</button>' : ""}</div>${rows || '<p class="character-relationship-status">暂未记录与其他人物的关系。</p>'}`;
|
|
4256
4588
|
host.querySelectorAll("[data-character-relationship-edit]").forEach((button) => button.addEventListener("click", () => {
|
|
4257
4589
|
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipEdit);
|
|
4258
4590
|
if (relationship) void openRelationshipDialog(relationship, { characterId });
|
|
@@ -4368,7 +4700,7 @@ function createVditorUploadHandler(uploadAttachment, getEditor) {
|
|
|
4368
4700
|
};
|
|
4369
4701
|
}
|
|
4370
4702
|
|
|
4371
|
-
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false } = {}) {
|
|
4703
|
+
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false, width = "auto" } = {}) {
|
|
4372
4704
|
if (!window.Vditor) {
|
|
4373
4705
|
toast("Markdown 编辑器资源加载失败,请刷新页面后重试", "error");
|
|
4374
4706
|
return null;
|
|
@@ -4382,6 +4714,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4382
4714
|
mode: "ir",
|
|
4383
4715
|
value: String(value ?? ""),
|
|
4384
4716
|
height: "100%",
|
|
4717
|
+
width,
|
|
4385
4718
|
minHeight: 260,
|
|
4386
4719
|
placeholder,
|
|
4387
4720
|
preview: { transform: transformVditorPreview },
|
|
@@ -4406,7 +4739,6 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4406
4739
|
attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
|
|
4407
4740
|
editor.__attachmentObserver = attachmentObserver;
|
|
4408
4741
|
host.__vditor = editor;
|
|
4409
|
-
if (readOnly) editor.disabled();
|
|
4410
4742
|
return editor;
|
|
4411
4743
|
}
|
|
4412
4744
|
|
|
@@ -4536,7 +4868,9 @@ async function openKnowledgeSectionEditor(index = null) {
|
|
|
4536
4868
|
const titleInput = $("#knowledge-section-title");
|
|
4537
4869
|
host.querySelectorAll("input, textarea").forEach((control) => control.addEventListener("input", () => { knowledgeSectionEditorDirty = true; }));
|
|
4538
4870
|
knowledgeSectionVditor = createVditorEditor($("#knowledge-section-markdown"), section?.contentMarkdown ?? "", {
|
|
4539
|
-
onInput: () => { knowledgeSectionEditorDirty = true; }
|
|
4871
|
+
onInput: () => { knowledgeSectionEditorDirty = true; },
|
|
4872
|
+
placeholder: "从这里开始写 Markdown 设定…",
|
|
4873
|
+
width: "100%"
|
|
4540
4874
|
});
|
|
4541
4875
|
host.querySelector("[data-knowledge-section-edit-close]").addEventListener("click", () => void closeKnowledgeSectionEditor());
|
|
4542
4876
|
host.querySelector("[data-knowledge-section-edit-cancel]").addEventListener("click", () => void closeKnowledgeSectionEditor());
|
|
@@ -4595,7 +4929,9 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
4595
4929
|
host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
|
|
4596
4930
|
characterSectionVditor = createVditorEditor($("#character-section-markdown"), section?.contentMarkdown ?? "", {
|
|
4597
4931
|
uploadAttachment: uploadCharacterSectionAttachment,
|
|
4598
|
-
onInput: () => { characterSectionEditorDirty = true; }
|
|
4932
|
+
onInput: () => { characterSectionEditorDirty = true; },
|
|
4933
|
+
placeholder: "从这里开始写人物章节…",
|
|
4934
|
+
width: "100%"
|
|
4599
4935
|
});
|
|
4600
4936
|
host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
4601
4937
|
host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
@@ -4610,6 +4946,10 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
4610
4946
|
button.disabled = true;
|
|
4611
4947
|
const contentMarkdown = characterSectionVditor?.getValue() ?? "";
|
|
4612
4948
|
try {
|
|
4949
|
+
if (!(await confirmConcurrentSave())) {
|
|
4950
|
+
button.disabled = false;
|
|
4951
|
+
return;
|
|
4952
|
+
}
|
|
4613
4953
|
const saved = await api(section ? `/api/character-sections/${section.id}` : `/api/characters/${characterEditorItem.id}/sections`, {
|
|
4614
4954
|
method: section ? "PATCH" : "POST",
|
|
4615
4955
|
body: {
|
|
@@ -4669,9 +5009,10 @@ function renderCharacterMarkdownSections() {
|
|
|
4669
5009
|
host.innerHTML = '<div class="character-editor-empty-field" role="note" aria-label="保存角色提示"><b>请先保存当前角色</b><span>完成基础资料后,请点击页面底部的“创建人物档案”。保存成功后即可新建 Markdown 档案章节。</span></div>';
|
|
4670
5010
|
return;
|
|
4671
5011
|
}
|
|
4672
|
-
const
|
|
5012
|
+
const canEdit = !entityEditorReadOnly && canEditModule("characters");
|
|
5013
|
+
const toolbar = `<div class="character-markdown-list-toolbar"><div><b>Markdown 档案章节</b><span>长篇内容独立保存、渲染、检索和版本管理。</span></div>${canEdit ? '<button type="button" class="primary-button" data-character-section-create>新建章节</button>' : ""}</div>`;
|
|
4673
5014
|
const sections = characterEditorSections.map((section) => `<article class="character-markdown-section">
|
|
4674
|
-
<header><div><span>${esc(characterSectionTypeLabels[section.sectionType] ?? section.sectionType)}</span><h4>${esc(section.title)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${
|
|
5015
|
+
<header><div><span>${esc(characterSectionTypeLabels[section.sectionType] ?? section.sectionType)}</span><h4>${esc(section.title)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${canEdit ? `<button type="button" data-character-section-edit="${esc(section.id)}">编辑</button>` : ""}<button type="button" data-character-section-versions="${esc(section.id)}">版本</button>${canEdit ? `<button type="button" data-character-section-delete="${esc(section.id)}">删除</button>` : ""}</div></header>
|
|
4675
5016
|
<div class="character-markdown-document message-body">${renderMarkdown(section.contentMarkdown) || '<p class="character-markdown-empty">本章节暂无正文。</p>'}</div>
|
|
4676
5017
|
<div data-character-section-versions-host="${esc(section.id)}"></div>
|
|
4677
5018
|
</article>`).join("");
|
|
@@ -4818,7 +5159,7 @@ function renderCharacterHistory() {
|
|
|
4818
5159
|
<div class="character-version-card-heading"><div><strong>v${version.versionNo}</strong><span>${esc(characterVersionSourceLabel(version.source))}</span></div><time>${esc(formatDateTime(version.createdAt))} · ${esc(version.actor || "历史数据")}</time></div>
|
|
4819
5160
|
<p>${esc(version.changeNote || "未填写版本说明")}</p>
|
|
4820
5161
|
<div class="character-version-changes">${changes.map((change) => `<span>${esc(change)}</span>`).join("")}</div>
|
|
4821
|
-
${isCurrent ? '<button type="button" disabled>当前版本</button>' : `<button type="button" data-character-restore="${version.versionNo}">回滚到此版本</button>`}
|
|
5162
|
+
${isCurrent ? '<button type="button" disabled>当前版本</button>' : entityEditorReadOnly ? '<button type="button" disabled>历史版本</button>' : `<button type="button" data-character-restore="${version.versionNo}">回滚到此版本</button>`}
|
|
4822
5163
|
</article>`;
|
|
4823
5164
|
}).join("");
|
|
4824
5165
|
host.querySelectorAll("[data-character-restore]").forEach((button) => button.addEventListener("click", async () => {
|
|
@@ -4871,7 +5212,8 @@ async function showCharacterHistory() {
|
|
|
4871
5212
|
}
|
|
4872
5213
|
}
|
|
4873
5214
|
|
|
4874
|
-
async function openCharacterEditor(item = null) {
|
|
5215
|
+
async function openCharacterEditor(item = null, { readOnly = false } = {}) {
|
|
5216
|
+
entityEditorReadOnly = readOnly;
|
|
4875
5217
|
[state.races, state.organizations] = await Promise.all([
|
|
4876
5218
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4877
5219
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
@@ -4890,7 +5232,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4890
5232
|
$("#character-history-button").title = item ? "查看、比较和回滚历史版本" : "创建人物档案后即可查看版本历史";
|
|
4891
5233
|
const characterMergeButton = $("#character-merge-button");
|
|
4892
5234
|
const characterDeleteButton = $("#character-delete-button");
|
|
4893
|
-
const canManageCharacter = Boolean(item && canEditModule("characters"));
|
|
5235
|
+
const canManageCharacter = Boolean(item && !readOnly && canEditModule("characters"));
|
|
4894
5236
|
characterMergeButton.classList.toggle("hidden", !canManageCharacter || state.characters.length < 2);
|
|
4895
5237
|
characterDeleteButton.classList.toggle("hidden", !canManageCharacter);
|
|
4896
5238
|
characterMergeButton.onclick = async () => {
|
|
@@ -4918,14 +5260,18 @@ async function openCharacterEditor(item = null) {
|
|
|
4918
5260
|
};
|
|
4919
5261
|
setCharacterHistoryVisible(false);
|
|
4920
5262
|
renderCharacterEditorFields(item);
|
|
4921
|
-
const viewOnly = !canEditModule("characters");
|
|
5263
|
+
const viewOnly = readOnly || !canEditModule("characters");
|
|
4922
5264
|
if (viewOnly) {
|
|
4923
|
-
$("#character-editor-eyebrow").textContent = "人物档案";
|
|
5265
|
+
$("#character-editor-eyebrow").textContent = readOnly ? "阅读人物档案" : "人物档案";
|
|
4924
5266
|
$("#character-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
|
|
4925
5267
|
$("#character-editor-fields").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = true; });
|
|
5268
|
+
$("#character-editor-fields").querySelectorAll("button").forEach((button) => { button.disabled = true; });
|
|
4926
5269
|
}
|
|
4927
5270
|
$("#character-change-note").readOnly = viewOnly;
|
|
4928
5271
|
$("#character-editor-submit").classList.toggle("hidden", viewOnly);
|
|
5272
|
+
const editButton = $("#character-editor-edit");
|
|
5273
|
+
editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
|
|
5274
|
+
editButton.onclick = () => void openCharacterEditor(item);
|
|
4929
5275
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
4930
5276
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
4931
5277
|
});
|
|
@@ -4935,7 +5281,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4935
5281
|
const form = $("#character-editor-form");
|
|
4936
5282
|
form.onsubmit = async (event) => {
|
|
4937
5283
|
event.preventDefault();
|
|
4938
|
-
if (!canEditModule("characters")) return;
|
|
5284
|
+
if (readOnly || !canEditModule("characters")) return;
|
|
4939
5285
|
const submit = $("#character-editor-submit");
|
|
4940
5286
|
submit.disabled = true;
|
|
4941
5287
|
try {
|
|
@@ -4944,6 +5290,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4944
5290
|
const wasEditing = Boolean(characterEditorItem);
|
|
4945
5291
|
const previousVersion = characterEditorItem?.versionNo;
|
|
4946
5292
|
if (!wasEditing) delete body.changeNote;
|
|
5293
|
+
if (!(await confirmConcurrentSave())) return;
|
|
4947
5294
|
const saved = await api(wasEditing ? `/api/characters/${characterEditorItem.id}` : `/api/works/${state.work.id}/characters`, { method: wasEditing ? "PATCH" : "POST", body });
|
|
4948
5295
|
entityEditorDirty = false;
|
|
4949
5296
|
await loadAiReferences();
|
|
@@ -4955,7 +5302,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4955
5302
|
submit.disabled = false;
|
|
4956
5303
|
}
|
|
4957
5304
|
};
|
|
4958
|
-
showEntityEditorPage("character");
|
|
5305
|
+
showEntityEditorPage("character", { readOnly });
|
|
4959
5306
|
if (item) {
|
|
4960
5307
|
if (canReadModule("relationships")) void loadCharacterEditorRelationships(item.id);
|
|
4961
5308
|
void loadCharacterMarkdownSections(item.id);
|
|
@@ -4997,7 +5344,8 @@ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
|
|
|
4997
5344
|
activateKnowledgeEditorTab("basic");
|
|
4998
5345
|
}
|
|
4999
5346
|
|
|
5000
|
-
async function openKnowledgeEditor(kind, item) {
|
|
5347
|
+
async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
|
|
5348
|
+
entityEditorReadOnly = readOnly;
|
|
5001
5349
|
await discardPendingMarkdownAttachments();
|
|
5002
5350
|
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
5003
5351
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
@@ -5027,8 +5375,8 @@ async function openKnowledgeEditor(kind, item) {
|
|
|
5027
5375
|
const candidates = isRace ? state.races : state.organizations;
|
|
5028
5376
|
const typeLabel = label;
|
|
5029
5377
|
historyButton.classList.toggle("hidden", !item);
|
|
5030
|
-
mergeButton.classList.toggle("hidden", !item || !canEditModule(module) || candidates.length < 2);
|
|
5031
|
-
deleteButton.classList.toggle("hidden", !item || !canEditModule(module));
|
|
5378
|
+
mergeButton.classList.toggle("hidden", !item || readOnly || !canEditModule(module) || candidates.length < 2);
|
|
5379
|
+
deleteButton.classList.toggle("hidden", !item || readOnly || !canEditModule(module));
|
|
5032
5380
|
historyButton.onclick = async () => {
|
|
5033
5381
|
if (!item) return;
|
|
5034
5382
|
if (!(await closeEntityEditor())) return;
|
|
@@ -5058,17 +5406,22 @@ async function openKnowledgeEditor(kind, item) {
|
|
|
5058
5406
|
});
|
|
5059
5407
|
};
|
|
5060
5408
|
renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions);
|
|
5061
|
-
const viewOnly = !canEditModule(module);
|
|
5409
|
+
const viewOnly = readOnly || !canEditModule(module);
|
|
5062
5410
|
if (viewOnly) {
|
|
5063
|
-
$("#knowledge-editor-eyebrow").textContent = `${label}档案`;
|
|
5411
|
+
$("#knowledge-editor-eyebrow").textContent = readOnly ? `阅读${label}档案` : `${label}档案`;
|
|
5064
5412
|
$("#knowledge-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
|
|
5065
5413
|
$("#knowledge-editor-fields").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = true; });
|
|
5414
|
+
$("#knowledge-editor-fields").querySelectorAll("button").forEach((button) => { button.disabled = true; });
|
|
5066
5415
|
}
|
|
5067
5416
|
$("#knowledge-editor-submit").classList.toggle("hidden", viewOnly);
|
|
5417
|
+
const editButton = $("#knowledge-editor-edit");
|
|
5418
|
+
editButton.textContent = `编辑${label}`;
|
|
5419
|
+
editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
|
|
5420
|
+
editButton.onclick = () => void openKnowledgeEditor(kind, item);
|
|
5068
5421
|
const form = $("#knowledge-editor-form");
|
|
5069
5422
|
form.onsubmit = async (event) => {
|
|
5070
5423
|
event.preventDefault();
|
|
5071
|
-
if (!canEditModule(module)) return;
|
|
5424
|
+
if (readOnly || !canEditModule(module)) return;
|
|
5072
5425
|
const submit = $("#knowledge-editor-submit");
|
|
5073
5426
|
submit.disabled = true;
|
|
5074
5427
|
try {
|
|
@@ -5086,6 +5439,7 @@ async function openKnowledgeEditor(kind, item) {
|
|
|
5086
5439
|
: { name, description: data.get("description"), settingsMarkdown, settingsSections };
|
|
5087
5440
|
if (canReadModule("characters")) body.memberIds = data.getAll("memberIds").map(String);
|
|
5088
5441
|
const wasEditing = Boolean(knowledgeEditorItem);
|
|
5442
|
+
if (!(await confirmConcurrentSave())) return;
|
|
5089
5443
|
await api(wasEditing ? `/api/${module}/${knowledgeEditorItem.id}` : `/api/works/${state.work.id}/${module}`, { method: wasEditing ? "PATCH" : "POST", body });
|
|
5090
5444
|
await cleanupPendingMarkdownAttachments(settingsMarkdown);
|
|
5091
5445
|
entityEditorDirty = false;
|
|
@@ -5098,16 +5452,16 @@ async function openKnowledgeEditor(kind, item) {
|
|
|
5098
5452
|
submit.disabled = false;
|
|
5099
5453
|
}
|
|
5100
5454
|
};
|
|
5101
|
-
showEntityEditorPage(kind);
|
|
5102
|
-
$("#knowledge-editor-fields").querySelector("input:not([type='checkbox']), textarea")?.focus();
|
|
5455
|
+
showEntityEditorPage(kind, { readOnly });
|
|
5456
|
+
(readOnly ? $("#knowledge-editor-close") : $("#knowledge-editor-fields").querySelector("input:not([type='checkbox']), textarea"))?.focus();
|
|
5103
5457
|
}
|
|
5104
5458
|
|
|
5105
|
-
async function openRaceDialog(item) {
|
|
5106
|
-
await openKnowledgeEditor("race", item);
|
|
5459
|
+
async function openRaceDialog(item, options) {
|
|
5460
|
+
await openKnowledgeEditor("race", item, options);
|
|
5107
5461
|
}
|
|
5108
5462
|
|
|
5109
|
-
async function openOrganizationDialog(item) {
|
|
5110
|
-
await openKnowledgeEditor("organization", item);
|
|
5463
|
+
async function openOrganizationDialog(item, options) {
|
|
5464
|
+
await openKnowledgeEditor("organization", item, options);
|
|
5111
5465
|
}
|
|
5112
5466
|
|
|
5113
5467
|
function openTimelineTrackDialog(item) {
|
|
@@ -5237,7 +5591,7 @@ function openTaskDialog() {
|
|
|
5237
5591
|
}
|
|
5238
5592
|
|
|
5239
5593
|
function openProviderDialog(item) {
|
|
5240
|
-
openDialog(item ? "编辑 AI 供应商" : "新建 AI 供应商", field("name", "显示名称", "text", item?.name) + field("baseUrl", "
|
|
5594
|
+
openDialog(item ? "编辑 AI 供应商" : "新建 AI 供应商", field("name", "显示名称", "text", item?.name) + field("baseUrl", "OpenAI 兼容接口地址", "url", item?.baseUrl ?? "https://api.openai.com/v1") + field("apiKey", item ? "替换 API 密钥(留空则不变)" : "API 密钥", "password") + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10) + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10) + field("maxTokens", "最大输出令牌数", "number", item?.maxTokens ?? 32000) + field("note", "用途备注", "textarea", item?.note) + field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true), async (form) => {
|
|
5241
5595
|
const body = { name: form.get("name"), baseUrl: form.get("baseUrl"), concurrencyLimit: Number(form.get("concurrencyLimit")), rpmLimit: Number(form.get("rpmLimit")), maxTokens: Number(form.get("maxTokens")), note: form.get("note"), status: form.get("enabled") === "on" ? "enabled" : "disabled" };
|
|
5242
5596
|
if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
|
|
5243
5597
|
await api(item ? `/api/providers/${item.id}` : "/api/platform/ai/providers", { method: item ? "PATCH" : "POST", body });
|
|
@@ -5249,7 +5603,7 @@ function openProviderDialog(item) {
|
|
|
5249
5603
|
function openModelDialog(providerId, item = null) {
|
|
5250
5604
|
const values = modelFormValues(item);
|
|
5251
5605
|
const temperatureField = `<div class="form-field model-temperature-field"><label for="model-temperature">默认温度<input id="model-temperature" name="temperature" type="number" value="${esc(values.temperature)}" step="any" aria-describedby="model-temperature-hint"></label><small id="model-temperature-hint" class="model-temperature-hint" hidden>Kimi 模型必须设置温度为 1。</small></div>`;
|
|
5252
|
-
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "
|
|
5606
|
+
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "模型上下文令牌总量", "number", values.contextWindow) + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled), async (form) => {
|
|
5253
5607
|
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
5254
5608
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
5255
5609
|
await renderPlatformAiConfig();
|
|
@@ -5491,7 +5845,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
5491
5845
|
message.className = "assistant-message";
|
|
5492
5846
|
const applicable = suggestion.action !== "note";
|
|
5493
5847
|
const guard = suggestion.guard;
|
|
5494
|
-
const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(issue.severity)} · ${esc(issue.type)}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
|
|
5848
|
+
const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
|
|
5495
5849
|
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
5496
5850
|
attachMessageHeading(message, "助手建议", createdAt ?? undefined);
|
|
5497
5851
|
attachAssistantCopyAction(message, suggestion.content);
|
|
@@ -5524,7 +5878,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
5524
5878
|
async function showVersions() {
|
|
5525
5879
|
if (!state.chapter) return;
|
|
5526
5880
|
const versions = await api(`/api/chapters/${state.chapter.id}/versions`);
|
|
5527
|
-
$("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(version.source)} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p>${canEditProse() ? `<button class="ghost-button" data-restore-version="${version.versionNo}">恢复</button>` : ""}</div>`).join("");
|
|
5881
|
+
$("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(chapterVersionSourceLabel(version.source))} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p>${canEditProse() ? `<button class="ghost-button" data-restore-version="${version.versionNo}">恢复</button>` : ""}</div>`).join("");
|
|
5528
5882
|
$("#versions-list").querySelectorAll("[data-restore-version]").forEach((button) => button.addEventListener("click", async () => {
|
|
5529
5883
|
if (!(await confirmToast(
|
|
5530
5884
|
`将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`,
|
|
@@ -5934,6 +6288,11 @@ $("#platform-ai-button").addEventListener("click", () => showPlatformAi().catch(
|
|
|
5934
6288
|
$("#user-management-button").addEventListener("click", openUsersDialog);
|
|
5935
6289
|
$("#platform-ui-settings-button").addEventListener("click", openPlatformUiSettingsDialog);
|
|
5936
6290
|
$("#collaboration-button").addEventListener("click", () => openMembersDialog());
|
|
6291
|
+
$("#presence-button").addEventListener("click", () => {
|
|
6292
|
+
const panel = $("#presence-panel");
|
|
6293
|
+
const open = panel.classList.toggle("hidden") === false;
|
|
6294
|
+
$("#presence-button").setAttribute("aria-expanded", String(open));
|
|
6295
|
+
});
|
|
5937
6296
|
$("#users-dialog-close").addEventListener("click", () => $("#users-dialog").close());
|
|
5938
6297
|
$("#platform-ui-settings-close").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
|
|
5939
6298
|
$("#platform-ui-settings-cancel").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
|
|
@@ -6249,6 +6608,10 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
6249
6608
|
$("#account-menu").classList.add("hidden");
|
|
6250
6609
|
$("#account-button").setAttribute("aria-expanded", "false");
|
|
6251
6610
|
}
|
|
6611
|
+
if (!event.target.closest("#presence-control")) {
|
|
6612
|
+
$("#presence-panel").classList.add("hidden");
|
|
6613
|
+
$("#presence-button").setAttribute("aria-expanded", "false");
|
|
6614
|
+
}
|
|
6252
6615
|
});
|
|
6253
6616
|
document.addEventListener("keydown", (event) => {
|
|
6254
6617
|
if (event.key === "Escape") {
|