@musnows/scriverse 0.6.13 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +11 -2
- package/dist/app.js.map +1 -1
- package/dist/database.js +19 -1
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +225 -16
- package/dist/public/display-labels.js +1 -1
- package/dist/public/entity-version.js +2 -1
- package/dist/public/index.html +32 -4
- package/dist/public/relationship-graph.js +469 -95
- package/dist/public/styles.css +74 -9
- package/dist/store.js +104 -4
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +3 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, renderRelationshipMindMap } from "/relationship-graph.js?v=20260809-relationship-search-galaxy-perf-v1";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
|
|
4
4
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-mentions.js?v=20260801-context-setting-mention-v1";
|
|
@@ -17,7 +17,7 @@ import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-ma
|
|
|
17
17
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
18
18
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
19
19
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
|
|
20
|
-
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=
|
|
20
|
+
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260809-global-replace-v1";
|
|
21
21
|
import {
|
|
22
22
|
chapterVersionSourceLabel,
|
|
23
23
|
foreshadowStatusLabel,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
taskScopeLabel,
|
|
37
37
|
timelineStatusLabel,
|
|
38
38
|
characterStateFieldLabel
|
|
39
|
-
} from "/display-labels.js?v=
|
|
39
|
+
} from "/display-labels.js?v=20260809-global-replace-v1";
|
|
40
40
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260731-work-comments-v2";
|
|
41
41
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
42
42
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
@@ -127,7 +127,7 @@ const state = {
|
|
|
127
127
|
dirty: false,
|
|
128
128
|
pendingImportMeta: null,
|
|
129
129
|
pendingCoverWorkId: null,
|
|
130
|
-
uiSettings: { toastPosition: "bottom-right", pageSizes: { ...defaultPageSizes } },
|
|
130
|
+
uiSettings: { toastPosition: "bottom-right", pageSizes: { ...defaultPageSizes }, galaxyFrameRate: 30 },
|
|
131
131
|
relationshipGraph: null,
|
|
132
132
|
galaxy: null,
|
|
133
133
|
relationshipMindMap: null,
|
|
@@ -331,6 +331,16 @@ function canReadAggregateContent(work = state.work) {
|
|
|
331
331
|
.every((module) => canReadModule(module, work));
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
function canGlobalReplaceScope(scope, work = state.work) {
|
|
335
|
+
if (scope === "settings") return canEditModule("settings", work);
|
|
336
|
+
if (scope === "prose-and-settings") return canEditProse(work) && canEditModule("settings", work);
|
|
337
|
+
return canEditProse(work);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function canGlobalReplaceAny(work = state.work) {
|
|
341
|
+
return Boolean(work) && (canGlobalReplaceScope("prose", work) || canGlobalReplaceScope("settings", work));
|
|
342
|
+
}
|
|
343
|
+
|
|
334
344
|
function applyWorkAccessMode() {
|
|
335
345
|
const viewOnly = Boolean(state.work) && !canEditWork();
|
|
336
346
|
const proseReadOnly = Boolean(state.work) && !canEditProse();
|
|
@@ -1648,7 +1658,7 @@ function resolveAiProcessDuration(metadata, steps, completedAt) {
|
|
|
1648
1658
|
return Math.max(0, completedTime - Math.min(...startedTimes));
|
|
1649
1659
|
}
|
|
1650
1660
|
|
|
1651
|
-
function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
1661
|
+
function renderAiProcessSteps(message, steps, completed, durationMs = null, visibleContents = null) {
|
|
1652
1662
|
message.querySelector(".ai-process-details")?.remove();
|
|
1653
1663
|
if (!Array.isArray(steps) || !steps.length) return;
|
|
1654
1664
|
const details = document.createElement("details");
|
|
@@ -1688,7 +1698,8 @@ function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
|
1688
1698
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · ${step.type === "thinking" ? "Thinking" : "中间输出"}`;
|
|
1689
1699
|
const body = document.createElement("div");
|
|
1690
1700
|
body.className = "message-body ai-process-step-body";
|
|
1691
|
-
|
|
1701
|
+
const content = visibleContents?.has(step) ? visibleContents.get(step) : step.content;
|
|
1702
|
+
body.innerHTML = renderMarkdown(content);
|
|
1692
1703
|
section.append(label, body);
|
|
1693
1704
|
list.append(section);
|
|
1694
1705
|
}
|
|
@@ -1942,7 +1953,7 @@ function applyAiRoleplayCharacter(character) {
|
|
|
1942
1953
|
if (active) $("#ai-scope").value = "none";
|
|
1943
1954
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
1944
1955
|
$("#ai-prompt").dataset.placeholder = active
|
|
1945
|
-
?
|
|
1956
|
+
? `与 ${String(state.aiRoleplayCharacter.name)} 角色开始对话……`
|
|
1946
1957
|
: "告诉 AI 你想讨论或修改什么……";
|
|
1947
1958
|
renderAiRoleplayCharacterSelect();
|
|
1948
1959
|
syncAiTaskOptions();
|
|
@@ -2673,6 +2684,10 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2673
2684
|
if (path.includes("/relationships")) affected.add("relationships");
|
|
2674
2685
|
if (path.includes("/chapter-annotations/") || /\/chapters\/[^/]+\/annotations(?:$|\?)/u.test(path)) affected.add("comments");
|
|
2675
2686
|
if (path.includes("/reviews")) affected.add("reviews");
|
|
2687
|
+
if (/\/api\/works\/[^/]+\/replace(?:$|\?)/u.test(path)) {
|
|
2688
|
+
affected.add("settings");
|
|
2689
|
+
if (state.work?.id) moduleRequestCache.invalidate(state.work.id, "settings");
|
|
2690
|
+
}
|
|
2676
2691
|
if (path.includes("/entity-versions/")) {
|
|
2677
2692
|
if (path.includes("/draft/")) affected.add("drafts");
|
|
2678
2693
|
if (path.includes("/setting/")) affected.add("settings");
|
|
@@ -2940,7 +2955,11 @@ function applyAuthenticatedUser(session) {
|
|
|
2940
2955
|
|
|
2941
2956
|
function applyPlatformUiSettings(settings) {
|
|
2942
2957
|
const position = settings?.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
2943
|
-
state.uiSettings = {
|
|
2958
|
+
state.uiSettings = {
|
|
2959
|
+
toastPosition: position,
|
|
2960
|
+
pageSizes: normalizePageSizes(settings?.pageSizes),
|
|
2961
|
+
galaxyFrameRate: normalizeGalaxyFrameRate(settings?.galaxyFrameRate)
|
|
2962
|
+
};
|
|
2944
2963
|
$("#toast-region").dataset.position = position;
|
|
2945
2964
|
}
|
|
2946
2965
|
|
|
@@ -3510,6 +3529,7 @@ function renderSettingsHub() {
|
|
|
3510
3529
|
$("#collaboration-button").disabled = !canManageWork;
|
|
3511
3530
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
3512
3531
|
$("#work-audit-button").disabled = !canManageWork;
|
|
3532
|
+
$("#global-replace-button").classList.toggle("hidden", !canGlobalReplaceAny());
|
|
3513
3533
|
$("#top-search-button").disabled = !canReadAggregate;
|
|
3514
3534
|
$("#export-button").disabled = !canExportManuscript;
|
|
3515
3535
|
$("#export-button").setAttribute("aria-expanded", "false");
|
|
@@ -3866,6 +3886,7 @@ async function openPlatformUiSettingsDialog() {
|
|
|
3866
3886
|
try {
|
|
3867
3887
|
const settings = await api("/api/platform/ui-settings");
|
|
3868
3888
|
$("#toast-position").value = settings.toastPosition === "top-right" ? "top-right" : "bottom-right";
|
|
3889
|
+
$("#galaxy-frame-rate").value = String(normalizeGalaxyFrameRate(settings.galaxyFrameRate));
|
|
3869
3890
|
const pageSizes = normalizePageSizes(settings.pageSizes);
|
|
3870
3891
|
$("#page-size-drafts").value = String(pageSizes.drafts);
|
|
3871
3892
|
$("#page-size-settings").value = String(pageSizes.settings);
|
|
@@ -4246,11 +4267,156 @@ async function openSearchDialog() {
|
|
|
4246
4267
|
$("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
|
|
4247
4268
|
$("#search-query").value = "";
|
|
4248
4269
|
$("#search-type").value = "";
|
|
4270
|
+
$("#search-to-replace").disabled = !canGlobalReplaceAny();
|
|
4249
4271
|
$("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
|
|
4250
4272
|
$("#search-dialog").showModal();
|
|
4251
4273
|
queueMicrotask(() => $("#search-query").focus());
|
|
4252
4274
|
}
|
|
4253
4275
|
|
|
4276
|
+
const globalReplaceScopeLabels = Object.freeze({
|
|
4277
|
+
prose: "正文",
|
|
4278
|
+
settings: "设定库",
|
|
4279
|
+
"prose-and-settings": "正文+设定库"
|
|
4280
|
+
});
|
|
4281
|
+
|
|
4282
|
+
function syncGlobalReplaceScopeOptions() {
|
|
4283
|
+
const dialog = $("#replace-dialog");
|
|
4284
|
+
const options = [...dialog.querySelectorAll('input[name="replaceScope"]')];
|
|
4285
|
+
for (const option of options) {
|
|
4286
|
+
const allowed = canGlobalReplaceScope(option.value);
|
|
4287
|
+
option.disabled = !allowed;
|
|
4288
|
+
option.closest(".replace-scope-option")?.classList.toggle("is-disabled", !allowed);
|
|
4289
|
+
}
|
|
4290
|
+
const selected = options.find((option) => option.checked && !option.disabled) ?? options.find((option) => !option.disabled);
|
|
4291
|
+
options.forEach((option) => { option.checked = option === selected; });
|
|
4292
|
+
const scope = selected?.value ?? "";
|
|
4293
|
+
$("#replace-submit").disabled = !scope || !canGlobalReplaceScope(scope);
|
|
4294
|
+
$("#replace-permission-note").textContent = scope
|
|
4295
|
+
? "替换完成后,命中的章节和设定会分别生成新的版本历史。"
|
|
4296
|
+
: "当前账户没有可写入的正文或设定库权限。";
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
function openGlobalReplaceDialog() {
|
|
4300
|
+
if (!state.work) {
|
|
4301
|
+
toast("请先打开一部作品", "error");
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
if (!canGlobalReplaceAny()) {
|
|
4305
|
+
toast("当前账户没有正文或设定库的编辑权限", "error");
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
if ($("#search-dialog").open) $("#search-dialog").close();
|
|
4309
|
+
$("#replace-form").reset();
|
|
4310
|
+
syncGlobalReplaceScopeOptions();
|
|
4311
|
+
$("#replace-dialog").showModal();
|
|
4312
|
+
queueMicrotask(() => $("#replace-find").focus());
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
async function refreshWorkAfterGlobalReplace(route, result) {
|
|
4316
|
+
const workId = state.work?.id;
|
|
4317
|
+
if (!workId) return;
|
|
4318
|
+
const nextWork = result?.work ?? await api(`/api/works/${encodeURIComponent(workId)}?directory=volumes`);
|
|
4319
|
+
if (!nextWork || nextWork.id !== workId) return;
|
|
4320
|
+
state.work = nextWork;
|
|
4321
|
+
state.work.volumes = state.work.volumes.map((volume) => ({ ...volume, chapters: Array.isArray(volume.chapters) ? volume.chapters : [] }));
|
|
4322
|
+
state.works = state.works.map((work) => work.id === workId ? { ...work, ...nextWork } : work);
|
|
4323
|
+
state.settings = [];
|
|
4324
|
+
loadedVolumeChapterIds.clear();
|
|
4325
|
+
volumeChapterLoadingIds.clear();
|
|
4326
|
+
volumeChapterRequests.clear();
|
|
4327
|
+
for (const volume of state.work.volumes) loadedVolumeChapterIds.add(volume.id);
|
|
4328
|
+
state.collapsedVolumeIds = new Set(state.work.volumes.map((volume) => volume.id));
|
|
4329
|
+
if (String(result?.scope) === "prose" || String(result?.scope) === "prose-and-settings") {
|
|
4330
|
+
state.chapter = null;
|
|
4331
|
+
lastSavedChapterSnapshot = null;
|
|
4332
|
+
}
|
|
4333
|
+
applyWorkAccessMode();
|
|
4334
|
+
showSystemStatus();
|
|
4335
|
+
updateDocumentTitle(state.work);
|
|
4336
|
+
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
4337
|
+
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
4338
|
+
renderTree();
|
|
4339
|
+
if (route.view === "editor" && route.chapterId && canReadModule("editor")) {
|
|
4340
|
+
await selectChapter(route.chapterId);
|
|
4341
|
+
} else if (route.view === "module") {
|
|
4342
|
+
await showModule(route.module);
|
|
4343
|
+
} else if (route.view === "settings") {
|
|
4344
|
+
renderSettingsHub();
|
|
4345
|
+
replacePageRoute({ view: "settings", workId: state.work.id, ...settingsRouteContext() });
|
|
4346
|
+
} else if (route.view === "welcome") {
|
|
4347
|
+
showWelcome(true);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
async function submitGlobalReplace(event) {
|
|
4352
|
+
event.preventDefault();
|
|
4353
|
+
if (!state.work) return;
|
|
4354
|
+
const find = $("#replace-find").value;
|
|
4355
|
+
const replacement = $("#replace-with").value;
|
|
4356
|
+
const scope = $("#replace-form").querySelector('input[name="replaceScope"]:checked')?.value ?? "prose";
|
|
4357
|
+
if (!find.trim()) {
|
|
4358
|
+
toast("请输入要查找的内容", "error");
|
|
4359
|
+
$("#replace-find").focus();
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
if (!canGlobalReplaceScope(scope)) {
|
|
4363
|
+
toast("当前账户没有所选范围的编辑权限", "error");
|
|
4364
|
+
syncGlobalReplaceScopeOptions();
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
const dialog = $("#replace-dialog");
|
|
4368
|
+
const reopenDialog = () => {
|
|
4369
|
+
dialog.showModal();
|
|
4370
|
+
syncGlobalReplaceScopeOptions();
|
|
4371
|
+
queueMicrotask(() => $("#replace-find").focus());
|
|
4372
|
+
};
|
|
4373
|
+
dialog.close();
|
|
4374
|
+
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,执行全局替换会放弃这些修改。是否继续?"))) {
|
|
4375
|
+
reopenDialog();
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
const scopeLabel = globalReplaceScopeLabels[scope] ?? "正文";
|
|
4379
|
+
const replacementLabel = replacement ? `“${replacement}”` : "空内容";
|
|
4380
|
+
const confirmed = await confirmToast(`将把《${state.work.title}》的${scopeLabel}中所有“${find}”替换为${replacementLabel}。每个命中对象都会生成新版本,确认继续吗?`, {
|
|
4381
|
+
title: "确认全局替换",
|
|
4382
|
+
confirmLabel: "确认替换"
|
|
4383
|
+
});
|
|
4384
|
+
if (!confirmed) {
|
|
4385
|
+
reopenDialog();
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
const button = $("#replace-submit");
|
|
4389
|
+
const route = currentPageRoute();
|
|
4390
|
+
const workId = state.work.id;
|
|
4391
|
+
button.disabled = true;
|
|
4392
|
+
button.textContent = "替换中…";
|
|
4393
|
+
cancelChapterAutoSave();
|
|
4394
|
+
state.dirty = false;
|
|
4395
|
+
try {
|
|
4396
|
+
const result = await api(`/api/works/${encodeURIComponent(workId)}/replace`, {
|
|
4397
|
+
method: "POST",
|
|
4398
|
+
body: { find, replacement, scope },
|
|
4399
|
+
skipOptimisticVersion: true
|
|
4400
|
+
});
|
|
4401
|
+
$("#replace-dialog").close();
|
|
4402
|
+
if (Number(result.totalMatches) > 0) await refreshWorkAfterGlobalReplace(route, result);
|
|
4403
|
+
if (Number(result.totalMatches) > 0) {
|
|
4404
|
+
const changedTargets = [];
|
|
4405
|
+
if (Number(result.chapterCount) > 0) changedTargets.push(`${result.chapterCount} 章`);
|
|
4406
|
+
if (Number(result.settingCount) > 0) changedTargets.push(`${result.settingCount} 条设定`);
|
|
4407
|
+
toast(`全局替换完成:${result.totalMatches} 处,已更新 ${changedTargets.join("、")}`);
|
|
4408
|
+
} else {
|
|
4409
|
+
toast("没有找到需要替换的内容");
|
|
4410
|
+
}
|
|
4411
|
+
} catch (error) {
|
|
4412
|
+
toast(error.message, "error");
|
|
4413
|
+
} finally {
|
|
4414
|
+
button.disabled = false;
|
|
4415
|
+
button.textContent = "开始替换";
|
|
4416
|
+
syncGlobalReplaceScopeOptions();
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4254
4420
|
function highlightedSearchText(value, query) {
|
|
4255
4421
|
return splitGlobalSearchHighlight(value, query)
|
|
4256
4422
|
.map((segment) => segment.match ? `<mark>${esc(segment.text)}</mark>` : esc(segment.text))
|
|
@@ -6226,6 +6392,7 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
6226
6392
|
</section>`;
|
|
6227
6393
|
mountRelationshipFilterToggle();
|
|
6228
6394
|
state.galaxy?.destroy();
|
|
6395
|
+
state.galaxy = null;
|
|
6229
6396
|
state.relationshipExpandedMap?.destroy?.();
|
|
6230
6397
|
if ($("#relationship-map-dialog").open) $("#relationship-map-dialog").close();
|
|
6231
6398
|
const graph = buildRelationshipGraph(state.characters, relationships);
|
|
@@ -6237,8 +6404,15 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
6237
6404
|
bindModulePagination("relationships", renderRelationships);
|
|
6238
6405
|
const openGalaxy = () => {
|
|
6239
6406
|
state.galaxy?.destroy();
|
|
6240
|
-
|
|
6241
|
-
|
|
6407
|
+
const galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, {
|
|
6408
|
+
workId: state.work.id,
|
|
6409
|
+
frameRate: state.uiSettings.galaxyFrameRate,
|
|
6410
|
+
onClose: () => {
|
|
6411
|
+
if (state.galaxy === galaxy) state.galaxy = null;
|
|
6412
|
+
}
|
|
6413
|
+
});
|
|
6414
|
+
state.galaxy = galaxy;
|
|
6415
|
+
galaxy.open();
|
|
6242
6416
|
};
|
|
6243
6417
|
const openExpanded = () => {
|
|
6244
6418
|
state.relationshipExpandedMap?.destroy?.();
|
|
@@ -10827,6 +11001,29 @@ async function streamChat(body) {
|
|
|
10827
11001
|
let finalAnswerStarted = false;
|
|
10828
11002
|
const processStartedAt = Date.now();
|
|
10829
11003
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
11004
|
+
const processStepTypewriters = new Map();
|
|
11005
|
+
const processStepVisibleContents = new Map();
|
|
11006
|
+
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
11007
|
+
renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
|
|
11008
|
+
};
|
|
11009
|
+
const processStepTypewriter = (step) => {
|
|
11010
|
+
const existing = processStepTypewriters.get(step);
|
|
11011
|
+
if (existing) return existing;
|
|
11012
|
+
processStepVisibleContents.set(step, "");
|
|
11013
|
+
const typewriter = createStreamTypewriter({
|
|
11014
|
+
onRender: (text) => {
|
|
11015
|
+
processStepVisibleContents.set(step, text);
|
|
11016
|
+
renderStreamingProcessSteps(finalAnswerStarted);
|
|
11017
|
+
scrollAiFeedToBottom();
|
|
11018
|
+
}
|
|
11019
|
+
});
|
|
11020
|
+
processStepTypewriters.set(step, typewriter);
|
|
11021
|
+
return typewriter;
|
|
11022
|
+
};
|
|
11023
|
+
const finishProcessStepTypewriters = () => Promise.all([...processStepTypewriters.values()].map((typewriter) => typewriter.finish()));
|
|
11024
|
+
const revealProcessStepTypewriters = () => {
|
|
11025
|
+
for (const typewriter of processStepTypewriters.values()) typewriter.reveal();
|
|
11026
|
+
};
|
|
10830
11027
|
try {
|
|
10831
11028
|
const response = await fetch(`/api/works/${state.work.id}/chat/stream`, {
|
|
10832
11029
|
method: "POST",
|
|
@@ -10886,7 +11083,7 @@ async function streamChat(body) {
|
|
|
10886
11083
|
streamedText += delta;
|
|
10887
11084
|
if (streamedText.length > 0) finalAnswerStarted = true;
|
|
10888
11085
|
typewriter.append(delta);
|
|
10889
|
-
if (firstFinalDelta && processSteps.length)
|
|
11086
|
+
if (firstFinalDelta && processSteps.length) renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
10890
11087
|
meta.textContent = "正在生成回复……";
|
|
10891
11088
|
} else if (eventName === "process_step") {
|
|
10892
11089
|
mountAssistantMessage();
|
|
@@ -10896,7 +11093,11 @@ async function streamChat(body) {
|
|
|
10896
11093
|
const existing = append ? processSteps.find((item) => item.id === step.id && item.type === step.type) : null;
|
|
10897
11094
|
if (existing && typeof step.content === "string") existing.content += step.content;
|
|
10898
11095
|
else processSteps.push(step);
|
|
10899
|
-
|
|
11096
|
+
const targetStep = existing ?? step;
|
|
11097
|
+
if (typeof step.content === "string" && step.content.length > 0 && step.type === "thinking") {
|
|
11098
|
+
processStepTypewriter(targetStep).append(step.content);
|
|
11099
|
+
}
|
|
11100
|
+
renderStreamingProcessSteps(finalAnswerStarted, elapsedProcessTime());
|
|
10900
11101
|
meta.textContent = step.type === "thinking"
|
|
10901
11102
|
? `正在思考 · 第 ${Number(step.round) || 1} 轮`
|
|
10902
11103
|
: step.type === "context_compaction"
|
|
@@ -10911,7 +11112,7 @@ async function streamChat(body) {
|
|
|
10911
11112
|
if (toolCall.status === "failed") setAiAssistantStatus("error");
|
|
10912
11113
|
toolCalls.push(toolCall);
|
|
10913
11114
|
processSteps.push(aiToolProcessStep(toolCall, round));
|
|
10914
|
-
|
|
11115
|
+
renderStreamingProcessSteps(finalAnswerStarted, elapsedProcessTime());
|
|
10915
11116
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
10916
11117
|
scrollAiFeedToBottom();
|
|
10917
11118
|
} else if (eventName === "context_compacted") {
|
|
@@ -10923,7 +11124,7 @@ async function streamChat(body) {
|
|
|
10923
11124
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
10924
11125
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
10925
11126
|
setAiContextMeter(payload.contextUsage);
|
|
10926
|
-
await typewriter.finish();
|
|
11127
|
+
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
10927
11128
|
message.classList.remove("is-streaming");
|
|
10928
11129
|
content.setAttribute("aria-busy", "false");
|
|
10929
11130
|
message.querySelector(".message-heading > span").textContent = "助手";
|
|
@@ -10949,16 +11150,17 @@ async function streamChat(body) {
|
|
|
10949
11150
|
if (chunk.done) break;
|
|
10950
11151
|
}
|
|
10951
11152
|
if (buffer.trim()) await consume(buffer);
|
|
10952
|
-
await typewriter.finish();
|
|
11153
|
+
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
10953
11154
|
if (streamError) throw streamError;
|
|
10954
11155
|
return { action: contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
10955
11156
|
} catch (error) {
|
|
10956
11157
|
mountAssistantMessage();
|
|
10957
11158
|
typewriter.reveal();
|
|
11159
|
+
revealProcessStepTypewriters();
|
|
10958
11160
|
message.classList.remove("is-streaming");
|
|
10959
11161
|
content.setAttribute("aria-busy", "false");
|
|
10960
11162
|
message.querySelector(".message-heading > span").textContent = aiAssistantLabel("生成中断");
|
|
10961
|
-
|
|
11163
|
+
renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
10962
11164
|
meta.textContent = "生成中断";
|
|
10963
11165
|
scrollAiFeedToBottom();
|
|
10964
11166
|
throw error;
|
|
@@ -11365,6 +11567,7 @@ $("#home-button").addEventListener("click", async () => {
|
|
|
11365
11567
|
$("#settings-button").addEventListener("click", () => {
|
|
11366
11568
|
void showSettingsHub();
|
|
11367
11569
|
});
|
|
11570
|
+
$("#global-replace-button").addEventListener("click", openGlobalReplaceDialog);
|
|
11368
11571
|
$("#account-button").addEventListener("click", () => {
|
|
11369
11572
|
const expanded = $("#account-menu").classList.toggle("hidden") === false;
|
|
11370
11573
|
$("#account-button").setAttribute("aria-expanded", String(expanded));
|
|
@@ -11999,6 +12202,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
11999
12202
|
method: "PATCH",
|
|
12000
12203
|
body: {
|
|
12001
12204
|
toastPosition: $("#toast-position").value,
|
|
12205
|
+
galaxyFrameRate: Number($("#galaxy-frame-rate").value),
|
|
12002
12206
|
pageSizes: {
|
|
12003
12207
|
drafts: Number($("#page-size-drafts").value),
|
|
12004
12208
|
settings: Number($("#page-size-settings").value),
|
|
@@ -12620,12 +12824,17 @@ $("#background-task-open-analysis").addEventListener("click", () => {
|
|
|
12620
12824
|
showModule("tasks").catch((error) => toast(error.message, "error"));
|
|
12621
12825
|
});
|
|
12622
12826
|
$("#search-dialog-close").addEventListener("click", () => $("#search-dialog").close());
|
|
12827
|
+
$("#search-to-replace").addEventListener("click", openGlobalReplaceDialog);
|
|
12623
12828
|
$("#search-form").addEventListener("submit", async (event) => {
|
|
12624
12829
|
event.preventDefault();
|
|
12625
12830
|
await runWorkSearch().catch((error) => {
|
|
12626
12831
|
$("#search-results").innerHTML = `<p class="search-results-status">${esc(error.message)}</p>`;
|
|
12627
12832
|
});
|
|
12628
12833
|
});
|
|
12834
|
+
$("#replace-dialog-close").addEventListener("click", () => $("#replace-dialog").close());
|
|
12835
|
+
$("#replace-cancel").addEventListener("click", () => $("#replace-dialog").close());
|
|
12836
|
+
$("#replace-form").querySelectorAll('input[name="replaceScope"]').forEach((input) => input.addEventListener("change", syncGlobalReplaceScopeOptions));
|
|
12837
|
+
$("#replace-form").addEventListener("submit", submitGlobalReplace);
|
|
12629
12838
|
$("#export-button").addEventListener("click", (event) => {
|
|
12630
12839
|
event.preventDefault();
|
|
12631
12840
|
event.stopPropagation();
|
|
@@ -74,7 +74,7 @@ export function providerProtocolLabel(value) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function chapterVersionSourceLabel(value) {
|
|
77
|
-
return enumLabel({ manual: "人工保存", auto: "自动保存", "ai-suggestion": "AI 建议", restore: "历史恢复", import: "文件导入", create: "初始版本" }, value, "其他来源");
|
|
77
|
+
return enumLabel({ manual: "人工保存", auto: "自动保存", "ai-suggestion": "AI 建议", restore: "历史恢复", import: "文件导入", create: "初始版本", "global-replace": "全局替换" }, value, "其他来源");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function occurrenceRoleLabel(value) {
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260809-relationship-search-galaxy-perf-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -209,11 +209,12 @@
|
|
|
209
209
|
<button id="platform-ai-button" class="settings-hub-card hidden" type="button" data-settings-action="ai"><span class="settings-card-mark">AI</span><strong>AI 管理</strong><small>供应商、模型、上下文与全局提示词</small></button>
|
|
210
210
|
<button id="platform-usage-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">T</span><strong>Token 用量</strong><small>总消耗、缓存命中率、每日网格与作品明细</small></button>
|
|
211
211
|
<button id="user-management-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">人</span><strong>用户管理</strong><small>管理员、普通用户与账户状态</small></button>
|
|
212
|
-
<button id="platform-ui-settings-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">界</span><strong>界面与分页</strong><small
|
|
212
|
+
<button id="platform-ui-settings-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">界</span><strong>界面与分页</strong><small>设置通知位置、银河图性能及各模块的单页数量</small></button>
|
|
213
213
|
<button id="s3-backup-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">S3</span><strong>S3 备份</strong><small>将整个系统的数据库和图片同步到多个目标</small></button>
|
|
214
214
|
<button id="collaboration-button" class="settings-hub-card" type="button"><span class="settings-card-mark">协</span><strong>作品协作</strong><small>邀请注册用户共同编辑当前作品</small></button>
|
|
215
215
|
<button id="writing-progress-button" class="settings-hub-card" type="button"><span class="settings-card-mark">字</span><strong>写作目标</strong><small>每日目标、总字数目标与近 30 天趋势</small></button>
|
|
216
216
|
<button id="work-audit-button" class="settings-hub-card" type="button"><span class="settings-card-mark">录</span><strong>操作记录</strong><small>查看作品修改、操作者、对象与时间</small></button>
|
|
217
|
+
<button id="global-replace-button" class="settings-hub-card" type="button"><span class="settings-card-mark">替</span><strong>全局替换</strong><small>替换已保存正文、设定库或两者内容</small></button>
|
|
217
218
|
<button id="appearance-button" class="settings-hub-card" type="button" data-settings-action="appearance"><span class="settings-card-mark">Aa</span><strong>显示设置</strong><small>中文字体、等宽英文字体、字号与行距</small></button>
|
|
218
219
|
<button id="export-button" class="settings-hub-card" type="button" data-settings-action="export" aria-haspopup="menu" aria-controls="manuscript-export-menu" aria-expanded="false"><span class="settings-card-mark">出</span><strong>导出正文</strong><small>选择导出 Markdown ZIP 或 DOCX;不包含角色和设定资料</small></button>
|
|
219
220
|
</div>
|
|
@@ -610,7 +611,7 @@
|
|
|
610
611
|
<dialog id="search-dialog" class="dialog wide-dialog" aria-labelledby="search-dialog-title">
|
|
611
612
|
<div class="dialog-header">
|
|
612
613
|
<div><span class="eyebrow">当前作品</span><h2 id="search-dialog-title">全文检索</h2></div>
|
|
613
|
-
<button id="search-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button>
|
|
614
|
+
<div class="settings-dialog-header-actions"><button id="search-to-replace" class="ghost-button" type="button">全局替换</button><button id="search-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
|
|
614
615
|
</div>
|
|
615
616
|
<div class="access-dialog-body">
|
|
616
617
|
<form id="search-form" class="search-form">
|
|
@@ -638,6 +639,31 @@
|
|
|
638
639
|
</div>
|
|
639
640
|
</dialog>
|
|
640
641
|
|
|
642
|
+
<dialog id="replace-dialog" class="dialog replace-dialog" aria-labelledby="replace-dialog-title" aria-describedby="replace-dialog-description">
|
|
643
|
+
<form id="replace-form">
|
|
644
|
+
<div class="dialog-header">
|
|
645
|
+
<div><span class="eyebrow">内容工具</span><h2 id="replace-dialog-title">全局替换</h2><p id="replace-dialog-description" class="dialog-header-meta">只处理已保存内容;每个被修改的章节或设定都会保留一个可恢复版本。</p></div>
|
|
646
|
+
<button id="replace-dialog-close" class="dialog-close" aria-label="关闭全局替换" type="button">×</button>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="replace-dialog-body">
|
|
649
|
+
<label class="replace-field">查找内容<input id="replace-find" name="find" type="text" maxlength="500" autocomplete="off" required placeholder="输入要查找的文字"></label>
|
|
650
|
+
<label class="replace-field">替换为<textarea id="replace-with" name="replacement" maxlength="200000" rows="4" placeholder="输入替换后的文字;留空表示删除"></textarea></label>
|
|
651
|
+
<fieldset class="replace-scope-fieldset" aria-describedby="replace-scope-description">
|
|
652
|
+
<legend>替换范围</legend>
|
|
653
|
+
<p id="replace-scope-description">默认只替换章节正文内容。</p>
|
|
654
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="prose" checked><span><strong>正文</strong><small>仅替换章节正文内容</small></span></label>
|
|
655
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="settings"><span><strong>设定库</strong><small>仅替换世界观设定内容</small></span></label>
|
|
656
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="prose-and-settings"><span><strong>正文+设定库</strong><small>同时替换章节正文和世界观设定内容</small></span></label>
|
|
657
|
+
</fieldset>
|
|
658
|
+
<p id="replace-permission-note" class="replace-permission-note" role="status"></p>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="dialog-actions">
|
|
661
|
+
<button id="replace-cancel" class="ghost-button" type="button">取消</button>
|
|
662
|
+
<button id="replace-submit" class="primary-button" type="submit">开始替换</button>
|
|
663
|
+
</div>
|
|
664
|
+
</form>
|
|
665
|
+
</dialog>
|
|
666
|
+
|
|
641
667
|
<dialog id="appearance-dialog" class="dialog">
|
|
642
668
|
<form id="appearance-form" method="dialog">
|
|
643
669
|
<div class="dialog-header">
|
|
@@ -780,6 +806,8 @@
|
|
|
780
806
|
<div class="dialog-fields">
|
|
781
807
|
<label>Toast 提示位置<select id="toast-position" name="toastPosition" aria-label="Toast 提示位置"><option value="bottom-right">右下角(默认)</option><option value="top-right">右上角</option></select></label>
|
|
782
808
|
<small>该设置对所有用户生效,保存后的提示会立即显示在新位置。</small>
|
|
809
|
+
<label>银河图动画帧率<select id="galaxy-frame-rate" name="galaxyFrameRate" aria-label="银河图动画帧率"><option value="24">24 FPS(较低负载)</option><option value="30">30 FPS(默认)</option><option value="60">60 FPS(更流畅)</option></select></label>
|
|
810
|
+
<small>该设置对所有作品和用户生效。更高帧率会让旋转与拖动更流畅,同时增加系统负载。</small>
|
|
783
811
|
<fieldset class="pagination-settings-fieldset">
|
|
784
812
|
<legend>可分页模块单页数量</legend>
|
|
785
813
|
<small>以下模块独立设置,允许 10–100 条;默认均为 30 条。种族层级始终完整展示。</small>
|
|
@@ -998,6 +1026,6 @@
|
|
|
998
1026
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
999
1027
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1000
1028
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1001
|
-
<script type="module" src="/app.js?v=
|
|
1029
|
+
<script type="module" src="/app.js?v=20260809-relationship-search-galaxy-perf-v1"></script>
|
|
1002
1030
|
</body>
|
|
1003
1031
|
</html>
|