@musnows/scriverse 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +59 -0
- package/dist/ai.js.map +1 -1
- package/dist/app.js +136 -35
- package/dist/app.js.map +1 -1
- package/dist/http-logging.js +2 -2
- package/dist/http-logging.js.map +1 -1
- package/dist/logger.js +2 -2
- package/dist/logger.js.map +1 -1
- package/dist/pagination.js +42 -0
- package/dist/pagination.js.map +1 -0
- package/dist/public/analysis-types.d.ts +8 -0
- package/dist/public/analysis-types.js +53 -0
- package/dist/public/app.js +235 -77
- package/dist/public/index.html +37 -8
- package/dist/public/markdown.js +35 -1
- package/dist/public/page-route.js +12 -0
- package/dist/public/styles.css +65 -22
- package/dist/security.js +41 -4
- package/dist/security.js.map +1 -1
- package/dist/store.js +303 -0
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +30 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/dist/public/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260721-release-0.3.6";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
|
-
import { renderMarkdown } from "/markdown.js?v=
|
|
3
|
+
import { renderMarkdown } from "/markdown.js?v=20260722-inline-code";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
5
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
@@ -14,10 +14,11 @@ import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "
|
|
|
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
16
|
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260714-all-knowledge-history";
|
|
17
|
-
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=
|
|
17
|
+
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260722-entity-editor-page";
|
|
18
18
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
19
19
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
20
20
|
import { buildRaceForest, eligibleRaceParents, racePathLabel } from "/race-hierarchy.js?v=20260721-race-hierarchy";
|
|
21
|
+
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
21
22
|
|
|
22
23
|
const state = {
|
|
23
24
|
user: null,
|
|
@@ -351,6 +352,10 @@ function replacePageRoute(route) {
|
|
|
351
352
|
|
|
352
353
|
function currentPageRoute() {
|
|
353
354
|
const workId = state.work?.id ?? null;
|
|
355
|
+
if (!$("#entity-editor-view").classList.contains("hidden") && workId && entityEditorType) {
|
|
356
|
+
const entityId = entityEditorType === "setting" ? settingEditorItem?.id : characterEditorItem?.id;
|
|
357
|
+
return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null };
|
|
358
|
+
}
|
|
354
359
|
if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
|
|
355
360
|
if (!$("#platform-ai-view").classList.contains("hidden")) return { view: "platform-ai", workId, ...settingsRouteContext() };
|
|
356
361
|
if (!$("#shelf-view").classList.contains("hidden")) return { view: "shelf" };
|
|
@@ -460,6 +465,9 @@ const chapterAutoSaveDelay = 800;
|
|
|
460
465
|
let aiMentionMatch = null;
|
|
461
466
|
let aiMentionRange = null;
|
|
462
467
|
let settingsReturnContext = null;
|
|
468
|
+
let entityEditorType = null;
|
|
469
|
+
let entityEditorDirty = false;
|
|
470
|
+
let settingEditorItem = null;
|
|
463
471
|
let characterEditorItem = null;
|
|
464
472
|
let characterEditorVersions = [];
|
|
465
473
|
let characterEditorRelationships = [];
|
|
@@ -467,8 +475,49 @@ let characterEditorRelationshipsLoading = false;
|
|
|
467
475
|
let characterEditorSections = [];
|
|
468
476
|
let characterSectionPreviewTimer = null;
|
|
469
477
|
let characterSectionPendingAttachments = [];
|
|
478
|
+
let characterSectionEditorDirty = false;
|
|
470
479
|
let entityHistoryContext = null;
|
|
471
480
|
|
|
481
|
+
function showEntityEditorPage(type) {
|
|
482
|
+
entityEditorType = type;
|
|
483
|
+
entityEditorDirty = false;
|
|
484
|
+
characterSectionEditorDirty = false;
|
|
485
|
+
$("#entity-editor-view").classList.remove("hidden");
|
|
486
|
+
$("#setting-editor-form").classList.toggle("hidden", type !== "setting");
|
|
487
|
+
$("#character-editor-form").classList.toggle("hidden", type !== "character");
|
|
488
|
+
$("#character-section-editor-view").classList.add("hidden");
|
|
489
|
+
$("#app").inert = true;
|
|
490
|
+
document.body.classList.add("entity-editor-open");
|
|
491
|
+
replacePageRoute(currentPageRoute());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function markEntityEditorDirty() {
|
|
495
|
+
if (entityEditorType && canEditWork()) entityEditorDirty = true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function confirmEntityEditorDiscard(message) {
|
|
499
|
+
if (!entityEditorDirty) return true;
|
|
500
|
+
return window.confirm(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function closeEntityEditor({ force = false } = {}) {
|
|
504
|
+
if (!$("#character-section-editor-view").classList.contains("hidden")) return closeCharacterSectionEditor({ force });
|
|
505
|
+
if (!force && !confirmEntityEditorDiscard()) return false;
|
|
506
|
+
await discardPendingCharacterAttachments();
|
|
507
|
+
const module = entityEditorType === "setting" ? "settings" : "characters";
|
|
508
|
+
entityEditorType = null;
|
|
509
|
+
entityEditorDirty = false;
|
|
510
|
+
settingEditorItem = null;
|
|
511
|
+
characterEditorItem = null;
|
|
512
|
+
$("#entity-editor-view").classList.add("hidden");
|
|
513
|
+
$("#setting-editor-form").classList.add("hidden");
|
|
514
|
+
$("#character-editor-form").classList.add("hidden");
|
|
515
|
+
$("#app").inert = false;
|
|
516
|
+
document.body.classList.remove("entity-editor-open");
|
|
517
|
+
await showModule(module);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
472
521
|
function setModuleNavExpanded(expanded) {
|
|
473
522
|
moduleNavExpanded = expanded;
|
|
474
523
|
$("#module-more-button .nav-label").textContent = expanded ? "收起" : "更多";
|
|
@@ -1015,7 +1064,7 @@ function renderAiConversationHistory() {
|
|
|
1015
1064
|
async function loadAiConversations(openLatest = true) {
|
|
1016
1065
|
const workId = state.work?.id;
|
|
1017
1066
|
if (!workId) return;
|
|
1018
|
-
const conversations = await
|
|
1067
|
+
const conversations = (await apiPage(`/api/works/${workId}/ai-conversations`)).items;
|
|
1019
1068
|
if (state.work?.id !== workId) return;
|
|
1020
1069
|
state.aiConversations = conversations;
|
|
1021
1070
|
loadedAiConversationsWorkId = workId;
|
|
@@ -1044,7 +1093,7 @@ async function ensureAiConversationsLoaded() {
|
|
|
1044
1093
|
}
|
|
1045
1094
|
|
|
1046
1095
|
async function openAiConversation(conversationId, hideHistory = true) {
|
|
1047
|
-
const conversation = await api(`/api/ai-conversations/${conversationId}`);
|
|
1096
|
+
const conversation = await api(`/api/ai-conversations/${conversationId}?page=1&limit=100`);
|
|
1048
1097
|
state.aiConversationId = conversation.id;
|
|
1049
1098
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
1050
1099
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
@@ -1395,6 +1444,24 @@ async function api(path, options = {}) {
|
|
|
1395
1444
|
return payload.data;
|
|
1396
1445
|
}
|
|
1397
1446
|
|
|
1447
|
+
async function apiPage(path, page = 1, limit = 50) {
|
|
1448
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
1449
|
+
const result = await api(`${path}${separator}page=${page}&limit=${limit}`);
|
|
1450
|
+
if (Array.isArray(result)) return { items: result, page, limit, hasMore: false, nextPage: null };
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
async function apiAllPages(path, limit = 100) {
|
|
1455
|
+
const items = [];
|
|
1456
|
+
let page = 1;
|
|
1457
|
+
while (true) {
|
|
1458
|
+
const result = await apiPage(path, page, limit);
|
|
1459
|
+
items.push(...(result.items ?? []));
|
|
1460
|
+
if (!result.hasMore || !result.nextPage) return items;
|
|
1461
|
+
page = result.nextPage;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1398
1465
|
function selectAuthMode(mode) {
|
|
1399
1466
|
const registerTab = $("#auth-register-tab");
|
|
1400
1467
|
const login = mode === "login" || registerTab.disabled;
|
|
@@ -1633,7 +1700,7 @@ function updateDocumentTitle(work = null) {
|
|
|
1633
1700
|
}
|
|
1634
1701
|
|
|
1635
1702
|
async function loadWorks(preferredId) {
|
|
1636
|
-
state.works = await
|
|
1703
|
+
state.works = (await apiPage("/api/works")).items;
|
|
1637
1704
|
if (preferredId) {
|
|
1638
1705
|
await selectWork(preferredId);
|
|
1639
1706
|
return;
|
|
@@ -1657,7 +1724,7 @@ async function initializePage() {
|
|
|
1657
1724
|
return;
|
|
1658
1725
|
}
|
|
1659
1726
|
const route = parsePageRoute(window.location.hash);
|
|
1660
|
-
state.works = await
|
|
1727
|
+
state.works = (await apiPage("/api/works")).items;
|
|
1661
1728
|
try {
|
|
1662
1729
|
if (route.view === "shelf") {
|
|
1663
1730
|
showShelf();
|
|
@@ -1671,16 +1738,30 @@ async function initializePage() {
|
|
|
1671
1738
|
}
|
|
1672
1739
|
|
|
1673
1740
|
if (requestedWork) {
|
|
1674
|
-
state.module = route.view === "module"
|
|
1741
|
+
state.module = route.view === "module"
|
|
1742
|
+
? route.module
|
|
1743
|
+
: route.view === "entity-editor"
|
|
1744
|
+
? (route.entity === "setting" ? "settings" : "characters")
|
|
1745
|
+
: "editor";
|
|
1675
1746
|
await selectWork(requestedWork.id, route.view === "editor" ? route.chapterId : null);
|
|
1676
1747
|
}
|
|
1677
1748
|
|
|
1678
1749
|
if (route.view === "editor") {
|
|
1679
|
-
|
|
1680
|
-
if (route.chapterId && chapterExists && state.chapter?.id !== route.chapterId) await selectChapter(route.chapterId);
|
|
1750
|
+
if (route.chapterId && state.chapter?.id !== route.chapterId) await selectChapter(route.chapterId);
|
|
1681
1751
|
return;
|
|
1682
1752
|
}
|
|
1683
1753
|
if (route.view === "module") return;
|
|
1754
|
+
if (route.view === "entity-editor") {
|
|
1755
|
+
const records = route.entity === "setting" ? state.settings : state.characters;
|
|
1756
|
+
const item = route.entityId ? records.find((record) => record.id === route.entityId) : null;
|
|
1757
|
+
if (route.entityId && !item) {
|
|
1758
|
+
toast(route.entity === "setting" ? "未找到要编辑的设定" : "未找到要编辑的角色", "error");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (route.entity === "setting") openSettingEditor(item);
|
|
1762
|
+
else await openCharacterEditor(item);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1684
1765
|
if (route.view === "welcome") {
|
|
1685
1766
|
showWelcome(true);
|
|
1686
1767
|
return;
|
|
@@ -1783,7 +1864,7 @@ async function openUsersDialog() {
|
|
|
1783
1864
|
}
|
|
1784
1865
|
$("#users-list").innerHTML = '<p class="empty-state">正在读取用户……</p>';
|
|
1785
1866
|
$("#users-dialog").showModal();
|
|
1786
|
-
try { renderUsers(await
|
|
1867
|
+
try { renderUsers((await apiPage("/api/users")).items); }
|
|
1787
1868
|
catch (error) { $("#users-dialog").close(); toast(error.message, "error"); }
|
|
1788
1869
|
}
|
|
1789
1870
|
|
|
@@ -1920,13 +2001,13 @@ async function openSearchResult(result) {
|
|
|
1920
2001
|
if (result.type === "character") {
|
|
1921
2002
|
await showModule("characters");
|
|
1922
2003
|
const character = state.characters.find((item) => item.id === result.id);
|
|
1923
|
-
if (character)
|
|
2004
|
+
if (character) openCharacterEditor(character);
|
|
1924
2005
|
return;
|
|
1925
2006
|
}
|
|
1926
2007
|
if (result.type === "setting") {
|
|
1927
2008
|
await showModule("settings");
|
|
1928
2009
|
const setting = await api(`/api/settings/${encodeURIComponent(result.id)}`);
|
|
1929
|
-
|
|
2010
|
+
openSettingEditor(setting);
|
|
1930
2011
|
return;
|
|
1931
2012
|
}
|
|
1932
2013
|
if (result.type === "race") {
|
|
@@ -2025,7 +2106,7 @@ function renderShelf() {
|
|
|
2025
2106
|
async function selectWork(workId, preferredChapterId = null) {
|
|
2026
2107
|
const discarding = state.work?.id !== workId && state.dirty;
|
|
2027
2108
|
if (discarding && !confirmDiscardChanges()) return false;
|
|
2028
|
-
const nextWork = await api(`/api/works/${workId}`);
|
|
2109
|
+
const nextWork = await api(`/api/works/${workId}?page=1&limit=100`);
|
|
2029
2110
|
if (state.work?.id !== nextWork.id) {
|
|
2030
2111
|
loadedAiModelsWorkId = null;
|
|
2031
2112
|
loadedAiReferencesWorkId = null;
|
|
@@ -2066,7 +2147,8 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2066
2147
|
renderTree();
|
|
2067
2148
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters);
|
|
2068
2149
|
const targetChapter = chapters.find((chapter) => chapter.id === preferredChapterId) ?? chapters[0];
|
|
2069
|
-
if (state.module === "editor" &&
|
|
2150
|
+
if (state.module === "editor" && preferredChapterId) await selectChapter(preferredChapterId);
|
|
2151
|
+
else if (state.module === "editor" && targetChapter) await selectChapter(targetChapter.id);
|
|
2070
2152
|
else if (state.module === "editor") showWelcome(true);
|
|
2071
2153
|
else await showModule(state.module);
|
|
2072
2154
|
return true;
|
|
@@ -2323,7 +2405,7 @@ function bindEntityHistoryButtons(refresh) {
|
|
|
2323
2405
|
}
|
|
2324
2406
|
|
|
2325
2407
|
async function renderSettings() {
|
|
2326
|
-
const records = await
|
|
2408
|
+
const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
|
|
2327
2409
|
state.settings = records;
|
|
2328
2410
|
$("#module-content").innerHTML = records.length ? `<div class="card-grid">${records.map((item) => `
|
|
2329
2411
|
<article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
|
|
@@ -2335,15 +2417,15 @@ async function renderSettings() {
|
|
|
2335
2417
|
await renderSettings();
|
|
2336
2418
|
await loadAiReferences();
|
|
2337
2419
|
}));
|
|
2338
|
-
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () =>
|
|
2420
|
+
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => openSettingEditor(records.find((item) => item.id === button.dataset.editSetting))));
|
|
2339
2421
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
2340
2422
|
}
|
|
2341
2423
|
|
|
2342
2424
|
async function renderCharacters() {
|
|
2343
2425
|
[state.characters, state.races, state.organizations] = await Promise.all([
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2426
|
+
apiPage(`/api/works/${state.work.id}/characters`).then((result) => result.items),
|
|
2427
|
+
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
2428
|
+
apiAllPages(`/api/works/${state.work.id}/organizations`)
|
|
2347
2429
|
]);
|
|
2348
2430
|
const auditPanel = `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>`;
|
|
2349
2431
|
$("#module-content").innerHTML = auditPanel + (state.characters.length ? `<div class="card-grid">${state.characters.map((item) => {
|
|
@@ -2373,7 +2455,7 @@ async function renderCharacters() {
|
|
|
2373
2455
|
}
|
|
2374
2456
|
});
|
|
2375
2457
|
$("#module-content").querySelectorAll("[data-open-character]").forEach((card) => {
|
|
2376
|
-
const open = () =>
|
|
2458
|
+
const open = () => openCharacterEditor(state.characters.find((item) => item.id === card.dataset.openCharacter));
|
|
2377
2459
|
card.addEventListener("click", (event) => { if (!event.target.closest("button")) open(); });
|
|
2378
2460
|
card.addEventListener("keydown", (event) => {
|
|
2379
2461
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
@@ -2381,13 +2463,13 @@ async function renderCharacters() {
|
|
|
2381
2463
|
open();
|
|
2382
2464
|
});
|
|
2383
2465
|
});
|
|
2384
|
-
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () =>
|
|
2466
|
+
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
|
|
2385
2467
|
}
|
|
2386
2468
|
|
|
2387
2469
|
async function renderRaces() {
|
|
2388
2470
|
[state.races, state.characters] = await Promise.all([
|
|
2389
|
-
|
|
2390
|
-
|
|
2471
|
+
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
2472
|
+
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2391
2473
|
]);
|
|
2392
2474
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
2393
2475
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
@@ -2409,8 +2491,8 @@ async function renderRaces() {
|
|
|
2409
2491
|
|
|
2410
2492
|
async function renderOrganizations() {
|
|
2411
2493
|
[state.organizations, state.characters] = await Promise.all([
|
|
2412
|
-
|
|
2413
|
-
|
|
2494
|
+
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
2495
|
+
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2414
2496
|
]);
|
|
2415
2497
|
$("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
2416
2498
|
<article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length} 条设定</small>
|
|
@@ -2425,8 +2507,8 @@ async function renderOrganizations() {
|
|
|
2425
2507
|
|
|
2426
2508
|
async function renderTimeline() {
|
|
2427
2509
|
const [events, tracks] = await Promise.all([
|
|
2428
|
-
|
|
2429
|
-
|
|
2510
|
+
apiPage(`/api/works/${state.work.id}/timeline`).then((result) => result.items),
|
|
2511
|
+
apiAllPages(`/api/works/${state.work.id}/timeline-tracks`)
|
|
2430
2512
|
]);
|
|
2431
2513
|
state.timelineTracks = tracks;
|
|
2432
2514
|
const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
|
|
@@ -2454,8 +2536,8 @@ async function renderTimeline() {
|
|
|
2454
2536
|
async function renderOutlines() {
|
|
2455
2537
|
const currentChapterId = state.chapter?.id;
|
|
2456
2538
|
const [outlines, foreshadows] = await Promise.all([
|
|
2457
|
-
|
|
2458
|
-
|
|
2539
|
+
apiPage(`/api/works/${state.work.id}/outlines`).then((result) => result.items),
|
|
2540
|
+
apiPage(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`).then((result) => result.items)
|
|
2459
2541
|
]);
|
|
2460
2542
|
const unresolved = foreshadows.filter((item) => item.unresolved);
|
|
2461
2543
|
const overdue = unresolved.filter((item) => item.overdue);
|
|
@@ -2483,8 +2565,8 @@ async function renderOutlines() {
|
|
|
2483
2565
|
}
|
|
2484
2566
|
|
|
2485
2567
|
async function renderRelationships() {
|
|
2486
|
-
state.characters = await
|
|
2487
|
-
const relationships = await
|
|
2568
|
+
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
2569
|
+
const relationships = (await apiPage(`/api/works/${state.work.id}/relationships`)).items;
|
|
2488
2570
|
const nameOf = (id) => state.characters.find((item) => item.id === id)?.name ?? "未知角色";
|
|
2489
2571
|
state.galaxy?.destroy();
|
|
2490
2572
|
state.relationshipExpandedMap?.destroy?.();
|
|
@@ -2515,8 +2597,8 @@ async function renderRelationships() {
|
|
|
2515
2597
|
|
|
2516
2598
|
async function renderReviews() {
|
|
2517
2599
|
const [reviews, characters] = await Promise.all([
|
|
2518
|
-
|
|
2519
|
-
|
|
2600
|
+
apiPage(`/api/works/${state.work.id}/reviews`).then((result) => result.items),
|
|
2601
|
+
apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`)
|
|
2520
2602
|
]);
|
|
2521
2603
|
const characterById = new Map(characters.map((character) => [character.id, character]));
|
|
2522
2604
|
const duplicateCard = (item) => {
|
|
@@ -2576,7 +2658,7 @@ async function renderReviews() {
|
|
|
2576
2658
|
|
|
2577
2659
|
async function renderTasks() {
|
|
2578
2660
|
const [tasks, settings] = await Promise.all([
|
|
2579
|
-
|
|
2661
|
+
apiPage(`/api/works/${state.work.id}/tasks`).then((result) => result.items),
|
|
2580
2662
|
api(`/api/works/${state.work.id}/ai-settings`)
|
|
2581
2663
|
]);
|
|
2582
2664
|
const pendingCount = tasks.filter((item) => item.status === "pending").length;
|
|
@@ -3011,8 +3093,8 @@ async function loadAiReferences() {
|
|
|
3011
3093
|
const workId = state.work?.id;
|
|
3012
3094
|
if (!workId) return;
|
|
3013
3095
|
const [characters, settings] = await Promise.all([
|
|
3014
|
-
|
|
3015
|
-
|
|
3096
|
+
apiAllPages(`/api/works/${workId}/characters`),
|
|
3097
|
+
apiAllPages(`/api/works/${workId}/settings`)
|
|
3016
3098
|
]);
|
|
3017
3099
|
if (state.work?.id !== workId) return;
|
|
3018
3100
|
state.characters = characters;
|
|
@@ -3229,7 +3311,7 @@ function bindWorkCoverControls(work) {
|
|
|
3229
3311
|
$("#work-cover-remove")?.addEventListener("click", async () => {
|
|
3230
3312
|
try {
|
|
3231
3313
|
await api(`/api/works/${work.id}/cover`, { method: "DELETE" });
|
|
3232
|
-
state.works = await
|
|
3314
|
+
state.works = (await apiPage("/api/works")).items;
|
|
3233
3315
|
const updated = state.works.find((item) => item.id === work.id) ?? { ...work, coverUrl: null };
|
|
3234
3316
|
Object.assign(work, updated);
|
|
3235
3317
|
const coverField = $("#dialog-fields")?.querySelector(".work-cover-field");
|
|
@@ -3256,7 +3338,7 @@ function openWorkSettingsDialog(work) {
|
|
|
3256
3338
|
workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField,
|
|
3257
3339
|
async (form) => {
|
|
3258
3340
|
await api(`/api/works/${work.id}`, { method: "PATCH", body: { title: form.get("title"), author: form.get("author"), description: form.get("description") } });
|
|
3259
|
-
state.works = await
|
|
3341
|
+
state.works = (await apiPage("/api/works")).items;
|
|
3260
3342
|
const updated = state.works.find((item) => item.id === work.id);
|
|
3261
3343
|
if (updated) Object.assign(work, updated);
|
|
3262
3344
|
if (state.work?.id === work.id) {
|
|
@@ -3313,17 +3395,50 @@ function openVolumeDialog(item) {
|
|
|
3313
3395
|
}, "分卷设置");
|
|
3314
3396
|
}
|
|
3315
3397
|
|
|
3316
|
-
function
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3398
|
+
function openSettingEditor(item = null) {
|
|
3399
|
+
settingEditorItem = item;
|
|
3400
|
+
$("#setting-editor-eyebrow").textContent = item ? "人工修正" : "作者事实";
|
|
3401
|
+
$("#setting-editor-title").textContent = item ? `编辑“${item.title}”` : "新建设定";
|
|
3402
|
+
$("#setting-editor-name").value = item?.title ?? "";
|
|
3403
|
+
$("#setting-editor-category").value = item?.category ?? "世界规则";
|
|
3404
|
+
$("#setting-editor-locked").checked = Boolean(item?.locked);
|
|
3405
|
+
$("#setting-editor-body").value = item?.content ?? "";
|
|
3406
|
+
$("#setting-change-note").value = "";
|
|
3407
|
+
$("#setting-change-note-field").classList.toggle("hidden", !item);
|
|
3408
|
+
$("#setting-editor-submit").textContent = item ? "保存新版本" : "创建设定";
|
|
3409
|
+
const viewOnly = !canEditWork();
|
|
3410
|
+
$("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
|
|
3411
|
+
$("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
|
|
3412
|
+
$("#setting-editor-submit").classList.toggle("hidden", viewOnly);
|
|
3413
|
+
$("#setting-editor-form").onsubmit = async (event) => {
|
|
3414
|
+
event.preventDefault();
|
|
3415
|
+
if (!canEditWork()) return;
|
|
3416
|
+
const form = new FormData(event.currentTarget);
|
|
3417
|
+
const submit = $("#setting-editor-submit");
|
|
3418
|
+
submit.disabled = true;
|
|
3419
|
+
try {
|
|
3420
|
+
const locked = form.get("locked") === "on";
|
|
3421
|
+
const body = {
|
|
3422
|
+
title: String(form.get("title") ?? "").trim(),
|
|
3423
|
+
category: String(form.get("category") ?? "世界规则"),
|
|
3424
|
+
content: String(form.get("content") ?? ""),
|
|
3425
|
+
locked,
|
|
3426
|
+
status: locked ? "confirmed" : (item?.status ?? "draft"),
|
|
3427
|
+
...(item ? { changeNote: String(form.get("changeNote") ?? "").trim() } : {})
|
|
3428
|
+
};
|
|
3323
3429
|
await api(item ? `/api/settings/${item.id}` : `/api/works/${state.work.id}/settings`, { method: item ? "PATCH" : "POST", body });
|
|
3324
|
-
|
|
3430
|
+
entityEditorDirty = false;
|
|
3325
3431
|
await loadAiReferences();
|
|
3326
|
-
|
|
3432
|
+
await closeEntityEditor({ force: true });
|
|
3433
|
+
toast(item ? "设定新版本已保存" : "设定已创建");
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
toast(error.message, "error");
|
|
3436
|
+
} finally {
|
|
3437
|
+
submit.disabled = false;
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
showEntityEditorPage("setting");
|
|
3441
|
+
$("#setting-editor-name").focus();
|
|
3327
3442
|
}
|
|
3328
3443
|
|
|
3329
3444
|
function characterEditorSection(key, title, description, content) {
|
|
@@ -3399,8 +3514,8 @@ async function loadCharacterEditorRelationships(characterId) {
|
|
|
3399
3514
|
let loaded = false;
|
|
3400
3515
|
try {
|
|
3401
3516
|
const [characters, relationships] = await Promise.all([
|
|
3402
|
-
|
|
3403
|
-
|
|
3517
|
+
apiAllPages(`/api/works/${workId}/characters`),
|
|
3518
|
+
apiAllPages(`/api/works/${workId}/relationships`)
|
|
3404
3519
|
]);
|
|
3405
3520
|
if (state.work?.id !== workId || characterEditorItem?.id !== characterId) return;
|
|
3406
3521
|
state.characters = characters;
|
|
@@ -3421,7 +3536,7 @@ async function loadCharacterEditorRelationships(characterId) {
|
|
|
3421
3536
|
async function refreshRelationshipSurfaces(characterId = null) {
|
|
3422
3537
|
const tasks = [];
|
|
3423
3538
|
if (state.module === "relationships") tasks.push(renderRelationships());
|
|
3424
|
-
if (characterId &&
|
|
3539
|
+
if (characterId && entityEditorType === "character" && !$("#entity-editor-view").classList.contains("hidden") && characterEditorItem?.id === characterId) {
|
|
3425
3540
|
tasks.push(loadCharacterEditorRelationships(characterId));
|
|
3426
3541
|
}
|
|
3427
3542
|
await Promise.all(tasks);
|
|
@@ -3458,9 +3573,29 @@ function scheduleCharacterSectionPreview() {
|
|
|
3458
3573
|
}, 260);
|
|
3459
3574
|
}
|
|
3460
3575
|
|
|
3576
|
+
async function closeCharacterSectionEditor({ force = false } = {}) {
|
|
3577
|
+
if (!force && characterSectionEditorDirty && !window.confirm("当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?")) return false;
|
|
3578
|
+
if (characterSectionPreviewTimer !== null) {
|
|
3579
|
+
clearTimeout(characterSectionPreviewTimer);
|
|
3580
|
+
characterSectionPreviewTimer = null;
|
|
3581
|
+
}
|
|
3582
|
+
await discardPendingCharacterAttachments();
|
|
3583
|
+
characterSectionEditorDirty = false;
|
|
3584
|
+
$("#character-section-editor-view").classList.add("hidden");
|
|
3585
|
+
$("#character-editor-form").classList.remove("hidden");
|
|
3586
|
+
$("#character-section-editor-host").innerHTML = "";
|
|
3587
|
+
replacePageRoute(currentPageRoute());
|
|
3588
|
+
return true;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3461
3591
|
function characterSectionEditorHtml(section = null) {
|
|
3462
3592
|
const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
|
|
3463
|
-
return `<
|
|
3593
|
+
return `<div class="character-section-editor-shell">
|
|
3594
|
+
<header class="character-section-editor-header">
|
|
3595
|
+
<div><span class="eyebrow">人物 Markdown 档案</span><h2 id="character-section-editor-title">${section ? `编辑“${esc(section.title)}”` : "新建档案章节"}</h2></div>
|
|
3596
|
+
<button class="entity-editor-back" type="button" data-character-section-edit-close>返回人物档案</button>
|
|
3597
|
+
</header>
|
|
3598
|
+
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}人物 Markdown 章节">
|
|
3464
3599
|
<div class="character-markdown-editor-meta">
|
|
3465
3600
|
<label>章节类型<select id="character-section-type">${options}</select></label>
|
|
3466
3601
|
<label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
|
|
@@ -3475,17 +3610,23 @@ function characterSectionEditorHtml(section = null) {
|
|
|
3475
3610
|
<label>Markdown 原文<textarea id="character-section-markdown" maxlength="500000" spellcheck="true" placeholder="支持标题、列表、引用、表格、链接和图片">${esc(section?.contentMarkdown ?? "")}</textarea></label>
|
|
3476
3611
|
<div><span class="character-markdown-preview-label">安全预览</span><article id="character-section-preview" class="character-markdown-document message-body">${renderMarkdown(section?.contentMarkdown ?? "") || '<p class="character-markdown-empty">预览区域暂无内容。</p>'}</article></div>
|
|
3477
3612
|
</div>
|
|
3478
|
-
<
|
|
3479
|
-
|
|
3480
|
-
|
|
3613
|
+
<div class="character-markdown-editor-footer">
|
|
3614
|
+
<label class="character-markdown-change-note">版本说明<input id="character-section-change-note" maxlength="500" placeholder="可选,例如:补充远古时期经历"></label>
|
|
3615
|
+
<div class="character-markdown-editor-actions"><button type="button" data-character-section-edit-cancel>取消</button><button type="button" class="primary-button" data-character-section-edit-save>${section ? "保存章节版本" : "创建章节"}</button></div>
|
|
3616
|
+
</div>
|
|
3617
|
+
</section>
|
|
3618
|
+
</div>`;
|
|
3481
3619
|
}
|
|
3482
3620
|
|
|
3483
3621
|
async function openCharacterSectionEditor(section = null) {
|
|
3484
3622
|
await discardPendingCharacterAttachments();
|
|
3485
|
-
const host = $("#character-
|
|
3486
|
-
if (!host) return;
|
|
3623
|
+
const host = $("#character-section-editor-host");
|
|
3487
3624
|
host.innerHTML = characterSectionEditorHtml(section);
|
|
3625
|
+
characterSectionEditorDirty = false;
|
|
3626
|
+
$("#character-editor-form").classList.add("hidden");
|
|
3627
|
+
$("#character-section-editor-view").classList.remove("hidden");
|
|
3488
3628
|
const textarea = $("#character-section-markdown");
|
|
3629
|
+
host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
|
|
3489
3630
|
textarea.addEventListener("input", scheduleCharacterSectionPreview);
|
|
3490
3631
|
$("#character-section-attachment").addEventListener("change", async (event) => {
|
|
3491
3632
|
const file = event.target.files?.[0];
|
|
@@ -3505,6 +3646,7 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
3505
3646
|
const suffix = end < textarea.value.length && !textarea.value.slice(end).startsWith("\n") ? "\n\n" : "";
|
|
3506
3647
|
textarea.setRangeText(`${prefix}${insertion}${suffix}`, start, end, "end");
|
|
3507
3648
|
textarea.focus();
|
|
3649
|
+
characterSectionEditorDirty = true;
|
|
3508
3650
|
scheduleCharacterSectionPreview();
|
|
3509
3651
|
toast(attachment.storedMimeType === "image/webp" ? "图片已转换为无损 WebP 并插入" : "图片已插入;转换后未变小,因此保留原格式");
|
|
3510
3652
|
} catch (error) {
|
|
@@ -3514,10 +3656,8 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
3514
3656
|
input.value = "";
|
|
3515
3657
|
}
|
|
3516
3658
|
});
|
|
3517
|
-
host.querySelector("[data-character-section-edit-
|
|
3518
|
-
|
|
3519
|
-
renderCharacterMarkdownSections();
|
|
3520
|
-
});
|
|
3659
|
+
host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
3660
|
+
host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
3521
3661
|
host.querySelector("[data-character-section-edit-save]").addEventListener("click", async (event) => {
|
|
3522
3662
|
const button = event.currentTarget;
|
|
3523
3663
|
const title = $("#character-section-title").value.trim();
|
|
@@ -3546,6 +3686,8 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
3546
3686
|
characterEditorSections = await api(`/api/characters/${characterEditorItem.id}/sections`);
|
|
3547
3687
|
renderCharacterMarkdownSections();
|
|
3548
3688
|
await Promise.all([renderCharacters(), loadAiReferences()]);
|
|
3689
|
+
characterSectionEditorDirty = false;
|
|
3690
|
+
await closeCharacterSectionEditor({ force: true });
|
|
3549
3691
|
toast(section ? `“${saved.title}”已保存为 v${saved.versionNo}` : `已创建“${saved.title}”`);
|
|
3550
3692
|
} catch (error) {
|
|
3551
3693
|
toast(error.message, "error");
|
|
@@ -3779,10 +3921,10 @@ async function showCharacterHistory() {
|
|
|
3779
3921
|
}
|
|
3780
3922
|
}
|
|
3781
3923
|
|
|
3782
|
-
async function
|
|
3924
|
+
async function openCharacterEditor(item = null) {
|
|
3783
3925
|
[state.races, state.organizations] = await Promise.all([
|
|
3784
|
-
|
|
3785
|
-
|
|
3926
|
+
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
3927
|
+
apiAllPages(`/api/works/${state.work.id}/organizations`)
|
|
3786
3928
|
]);
|
|
3787
3929
|
characterEditorItem = item ?? null;
|
|
3788
3930
|
characterEditorVersions = [];
|
|
@@ -3804,13 +3946,14 @@ async function openCharacterDialog(item) {
|
|
|
3804
3946
|
$("#character-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
|
|
3805
3947
|
$("#character-editor-fields").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = true; });
|
|
3806
3948
|
}
|
|
3949
|
+
$("#character-change-note").readOnly = viewOnly;
|
|
3950
|
+
$("#character-editor-submit").classList.toggle("hidden", viewOnly);
|
|
3807
3951
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
3808
3952
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
3809
3953
|
});
|
|
3810
3954
|
const relationshipTab = document.querySelector("[data-character-editor-tab='relationships']");
|
|
3811
3955
|
relationshipTab.disabled = !item;
|
|
3812
3956
|
relationshipTab.title = item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
3813
|
-
const dialog = $("#character-editor-dialog");
|
|
3814
3957
|
const form = $("#character-editor-form");
|
|
3815
3958
|
form.onsubmit = async (event) => {
|
|
3816
3959
|
event.preventDefault();
|
|
@@ -3824,8 +3967,9 @@ async function openCharacterDialog(item) {
|
|
|
3824
3967
|
const previousVersion = characterEditorItem?.versionNo;
|
|
3825
3968
|
if (!wasEditing) delete body.changeNote;
|
|
3826
3969
|
const saved = await api(wasEditing ? `/api/characters/${characterEditorItem.id}` : `/api/works/${state.work.id}/characters`, { method: wasEditing ? "PATCH" : "POST", body });
|
|
3827
|
-
|
|
3828
|
-
await
|
|
3970
|
+
entityEditorDirty = false;
|
|
3971
|
+
await loadAiReferences();
|
|
3972
|
+
await closeEntityEditor({ force: true });
|
|
3829
3973
|
toast(!wasEditing ? "人物档案已创建" : saved.versionNo === previousVersion ? "没有检测到人物档案变更" : `人物档案已保存为 v${saved.versionNo}`);
|
|
3830
3974
|
} catch (error) {
|
|
3831
3975
|
toast(error.message, "error");
|
|
@@ -3833,7 +3977,7 @@ async function openCharacterDialog(item) {
|
|
|
3833
3977
|
submit.disabled = false;
|
|
3834
3978
|
}
|
|
3835
3979
|
};
|
|
3836
|
-
|
|
3980
|
+
showEntityEditorPage("character");
|
|
3837
3981
|
if (item) {
|
|
3838
3982
|
void loadCharacterEditorRelationships(item.id);
|
|
3839
3983
|
void loadCharacterMarkdownSections(item.id);
|
|
@@ -3841,7 +3985,7 @@ async function openCharacterDialog(item) {
|
|
|
3841
3985
|
}
|
|
3842
3986
|
|
|
3843
3987
|
async function openRaceDialog(item) {
|
|
3844
|
-
state.characters = await
|
|
3988
|
+
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
3845
3989
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
3846
3990
|
const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
|
|
3847
3991
|
.sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
|
|
@@ -3862,7 +4006,7 @@ async function openRaceDialog(item) {
|
|
|
3862
4006
|
}
|
|
3863
4007
|
|
|
3864
4008
|
async function openOrganizationDialog(item) {
|
|
3865
|
-
state.characters = await
|
|
4009
|
+
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
3866
4010
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
3867
4011
|
openDialog(item ? "编辑组织" : "新建组织",
|
|
3868
4012
|
field("name", "组织名称", "text", item?.name) +
|
|
@@ -3967,7 +4111,7 @@ function openTimelineSplitDialog(item) {
|
|
|
3967
4111
|
}
|
|
3968
4112
|
|
|
3969
4113
|
async function openRelationshipDialog(item, options = {}) {
|
|
3970
|
-
state.characters = await
|
|
4114
|
+
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
3971
4115
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
3972
4116
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
3973
4117
|
const defaultFrom = options.characterId && state.characters.some((character) => character.id === options.characterId) ? options.characterId : characterOptions[0][0];
|
|
@@ -3988,11 +4132,18 @@ function openReviewDialog() {
|
|
|
3988
4132
|
|
|
3989
4133
|
function openTaskDialog() {
|
|
3990
4134
|
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
|
|
3991
|
-
|
|
4135
|
+
const defaultTaskType = ANALYSIS_TYPES[0].value;
|
|
4136
|
+
const taskTypeField = `<div class="form-field analysis-type-field"><label>分析类型<select name="taskType" aria-describedby="analysis-type-description">${ANALYSIS_TYPES.map(({ value, label }) => `<option value="${esc(value)}" ${value === defaultTaskType ? "selected" : ""}>${esc(label)}</option>`).join("")}</select></label><p id="analysis-type-description" class="analysis-type-description" aria-live="polite">${esc(analysisTypeDescription(defaultTaskType))}</p></div>`;
|
|
4137
|
+
openDialog("开始 AI 分析", taskTypeField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + field("chapterId", "章节", "select", chapterOptions[0]?.[0] ?? "", chapterOptions), async (form) => {
|
|
3992
4138
|
const scope = form.get("taskType") === "character-identity-audit" || form.get("scopeType") === "book" ? { type: "book" } : { type: "chapter", chapterId: form.get("chapterId") };
|
|
3993
4139
|
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType: form.get("taskType"), scope } });
|
|
3994
4140
|
await renderTasks();
|
|
3995
4141
|
});
|
|
4142
|
+
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
4143
|
+
const description = $("#analysis-type-description");
|
|
4144
|
+
taskTypeSelect.addEventListener("change", () => {
|
|
4145
|
+
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
4146
|
+
});
|
|
3996
4147
|
}
|
|
3997
4148
|
|
|
3998
4149
|
function openProviderDialog(item) {
|
|
@@ -4588,9 +4739,16 @@ $("#versions-button").addEventListener("click", showVersions);
|
|
|
4588
4739
|
$("#versions-close").addEventListener("click", () => $("#versions-dialog").close());
|
|
4589
4740
|
$("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
|
|
4590
4741
|
$("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
|
|
4591
|
-
$("#
|
|
4592
|
-
$("#character-editor-
|
|
4593
|
-
$("#character-editor-
|
|
4742
|
+
$("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
|
|
4743
|
+
$("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
|
|
4744
|
+
$("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
|
|
4745
|
+
$("#setting-editor-form").addEventListener("input", markEntityEditorDirty);
|
|
4746
|
+
$("#setting-editor-form").addEventListener("change", markEntityEditorDirty);
|
|
4747
|
+
$("#character-editor-form").addEventListener("input", markEntityEditorDirty);
|
|
4748
|
+
$("#character-editor-form").addEventListener("change", markEntityEditorDirty);
|
|
4749
|
+
$("#character-editor-fields").addEventListener("click", (event) => {
|
|
4750
|
+
if (event.target.closest("[data-item-list-add], [data-structured-list-add], [data-item-list-remove], [data-structured-list-remove]")) markEntityEditorDirty();
|
|
4751
|
+
});
|
|
4594
4752
|
$("#character-history-button").addEventListener("click", () => {
|
|
4595
4753
|
if ($("#character-history-panel").classList.contains("hidden")) void showCharacterHistory();
|
|
4596
4754
|
else setCharacterHistoryVisible(false);
|
|
@@ -4689,7 +4847,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
4689
4847
|
if (button.dataset.module) showModule(button.dataset.module);
|
|
4690
4848
|
});
|
|
4691
4849
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
4692
|
-
$("#module-create-button").addEventListener("click", () => ({ settings:
|
|
4850
|
+
$("#module-create-button").addEventListener("click", () => ({ settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
4693
4851
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
4694
4852
|
updateAiMentionMenu();
|
|
4695
4853
|
scheduleAiContextUsage();
|
|
@@ -4761,7 +4919,7 @@ $("#cover-file").addEventListener("change", async (event) => {
|
|
|
4761
4919
|
body.append("file", file);
|
|
4762
4920
|
try {
|
|
4763
4921
|
await api(`/api/works/${workId}/cover`, { method: "PUT", body });
|
|
4764
|
-
state.works = await
|
|
4922
|
+
state.works = (await apiPage("/api/works")).items;
|
|
4765
4923
|
const updated = state.works.find((item) => item.id === workId);
|
|
4766
4924
|
const coverField = $("#dialog-fields")?.querySelector(".work-cover-field");
|
|
4767
4925
|
if (updated && coverField && $("#form-dialog")?.open) {
|
|
@@ -4900,7 +5058,7 @@ $("#search-form").addEventListener("submit", async (event) => {
|
|
|
4900
5058
|
$("#export-button").addEventListener("click", () => {
|
|
4901
5059
|
if (state.work) window.location.href = `/api/works/${state.work.id}/export?format=markdown`;
|
|
4902
5060
|
});
|
|
4903
|
-
window.addEventListener("beforeunload", (event) => { if (state.dirty) event.preventDefault(); });
|
|
5061
|
+
window.addEventListener("beforeunload", (event) => { if (state.dirty || entityEditorDirty || characterSectionEditorDirty) event.preventDefault(); });
|
|
4904
5062
|
|
|
4905
5063
|
initializePage().catch((error) => {
|
|
4906
5064
|
restoringPageRoute = false;
|