@musnows/scriverse 0.7.11 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +388 -61
- package/dist/ai.js.map +1 -1
- package/dist/app.js +67 -2
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +186 -20
- package/dist/public/index.html +1 -1
- package/dist/public/plain-text-paste.js +89 -0
- package/dist/public/styles.css +28 -0
- package/dist/store.js +198 -67
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +1 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -26,6 +26,7 @@ import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-ti
|
|
|
26
26
|
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260812-context-usage-remaining-v2";
|
|
27
27
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
28
28
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
29
|
+
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
29
30
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
30
31
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
31
32
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
|
|
@@ -11821,6 +11822,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
11821
11822
|
const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
|
|
11822
11823
|
attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
|
|
11823
11824
|
editor.__attachmentObserver = attachmentObserver;
|
|
11825
|
+
editor.__plainTextPasteCleanup = bindPlainTextPaste(host);
|
|
11824
11826
|
host.__vditor = editor;
|
|
11825
11827
|
return editor;
|
|
11826
11828
|
}
|
|
@@ -12012,6 +12014,7 @@ function ensureVditorIconScript() {
|
|
|
12012
12014
|
function destroyVditorEditor(editor) {
|
|
12013
12015
|
if (!editor) return;
|
|
12014
12016
|
editor.__attachmentObserver?.disconnect();
|
|
12017
|
+
editor.__plainTextPasteCleanup?.();
|
|
12015
12018
|
editor.__vditorLineNumberObserver?.disconnect();
|
|
12016
12019
|
editor.__vditorLineNumberResizeObserver?.disconnect();
|
|
12017
12020
|
if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
|
|
@@ -13084,7 +13087,12 @@ function openReviewDialog() {
|
|
|
13084
13087
|
}
|
|
13085
13088
|
|
|
13086
13089
|
async function openTaskDialog() {
|
|
13087
|
-
const
|
|
13090
|
+
const volumeOptions = state.work.volumes.map((volume) => ({ id: String(volume.id), title: String(volume.title) }));
|
|
13091
|
+
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
13092
|
+
id: String(chapter.id),
|
|
13093
|
+
title: String(chapter.title),
|
|
13094
|
+
volumeTitle: String(volume.title)
|
|
13095
|
+
})));
|
|
13088
13096
|
let relationshipCharacters = [];
|
|
13089
13097
|
let taskModels = [];
|
|
13090
13098
|
let taskDefaults = [];
|
|
@@ -13100,6 +13108,23 @@ async function openTaskDialog() {
|
|
|
13100
13108
|
toast(`分析任务配置加载失败:${error.message}`, "error");
|
|
13101
13109
|
return;
|
|
13102
13110
|
}
|
|
13111
|
+
const taskScopePicker = (kind, label, options, emptyLabel) => `<div class="form-field task-scope-field task-scope-${kind}-field is-disabled" data-task-scope-field="${kind}" aria-disabled="true">
|
|
13112
|
+
<span id="task-${kind}-label">${esc(label)}(可多选)</span>
|
|
13113
|
+
<div class="task-scope-picker">
|
|
13114
|
+
<button class="task-scope-trigger" type="button" data-task-scope-trigger="${kind}" aria-expanded="false" aria-controls="task-${kind}-bubble" aria-labelledby="task-${kind}-label task-${kind}-summary" disabled>
|
|
13115
|
+
<span id="task-${kind}-summary">${esc(emptyLabel)}</span>
|
|
13116
|
+
<span class="task-scope-trigger-meta"><span data-task-scope-count="${kind}">未选择</span><span class="task-scope-chevron" aria-hidden="true">⌄</span></span>
|
|
13117
|
+
</button>
|
|
13118
|
+
<div id="task-${kind}-bubble" class="task-scope-bubble hidden" data-task-scope-bubble="${kind}">
|
|
13119
|
+
<label class="task-scope-search">筛选${esc(label)}<input type="search" data-task-scope-search="${kind}" placeholder="输入名称" autocomplete="off"></label>
|
|
13120
|
+
<div class="task-scope-bubble-meta"><span>共 ${options.length} ${kind === "chapter" ? "章" : "卷"}</span><button type="button" data-task-scope-clear="${kind}" disabled>清空选择</button></div>
|
|
13121
|
+
<div class="task-scope-options" role="group" aria-labelledby="task-${kind}-label">
|
|
13122
|
+
${options.map((item) => `<label class="task-scope-option" data-task-scope-search-name="${esc(`${item.title} ${item.volumeTitle ?? ""}`.toLocaleLowerCase())}"><input type="checkbox" name="${kind === "chapter" ? "chapterIds" : "volumeIds"}" value="${esc(item.id)}" data-task-scope-input="${kind}"><span><strong>${esc(item.title)}</strong>${item.volumeTitle ? `<small>${esc(item.volumeTitle)}</small>` : ""}</span></label>`).join("")}
|
|
13123
|
+
</div>
|
|
13124
|
+
<p class="task-scope-empty hidden" data-task-scope-empty="${kind}">没有匹配的${esc(label)}</p>
|
|
13125
|
+
</div>
|
|
13126
|
+
</div>
|
|
13127
|
+
</div>`;
|
|
13103
13128
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
13104
13129
|
const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
|
|
13105
13130
|
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
@@ -13126,8 +13151,9 @@ async function openTaskDialog() {
|
|
|
13126
13151
|
const modelField = `<label>任务模型<select name="modelId" required aria-describedby="analysis-task-model-help">
|
|
13127
13152
|
<option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
|
|
13128
13153
|
${availableTaskModels.map((model) => `<option value="${esc(model.id)}" ${model.id === defaultModelId ? "selected" : ""}>${esc(modelOptionLabel(model))}</option>`).join("")}
|
|
13129
|
-
</select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small></label>`;
|
|
13130
|
-
const chapterField =
|
|
13154
|
+
</select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small><p class="analysis-context-warning hidden" data-analysis-context-warning role="alert"></p></label>`;
|
|
13155
|
+
const chapterField = taskScopePicker("chapter", "章节", chapterOptions, "选择需要分析的章节");
|
|
13156
|
+
const volumeField = taskScopePicker("volume", "分卷", volumeOptions, "选择需要分析的分卷");
|
|
13131
13157
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
13132
13158
|
${relationshipCharacterPicker}
|
|
13133
13159
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
@@ -13164,25 +13190,43 @@ async function openTaskDialog() {
|
|
|
13164
13190
|
const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
|
|
13165
13191
|
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
13166
13192
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
13193
|
+
const chapterIds = form.getAll("chapterIds").map(String).filter(Boolean);
|
|
13194
|
+
const volumeIds = form.getAll("volumeIds").map(String).filter(Boolean);
|
|
13195
|
+
const chapterScope = {
|
|
13196
|
+
type: "chapter",
|
|
13197
|
+
...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
|
|
13198
|
+
...(chapterIds.length ? { chapterIds } : {})
|
|
13199
|
+
};
|
|
13200
|
+
const volumeScope = {
|
|
13201
|
+
type: "volume",
|
|
13202
|
+
...(volumeIds[0] ? { volumeId: volumeIds[0] } : {}),
|
|
13203
|
+
...(volumeIds.length ? { volumeIds } : {})
|
|
13204
|
+
};
|
|
13167
13205
|
const scope = settingsOnly
|
|
13168
13206
|
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13169
|
-
:
|
|
13207
|
+
: scopeType === "book" || includeAllSettings
|
|
13170
13208
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13171
|
-
: {
|
|
13209
|
+
: { ...(scopeType === "volume" ? volumeScope : chapterScope), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
13172
13210
|
return scope;
|
|
13173
13211
|
};
|
|
13174
13212
|
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
13175
13213
|
type: scope.type,
|
|
13176
13214
|
chapterId: scope.chapterId ?? null,
|
|
13215
|
+
chapterIds: scope.chapterIds ?? [],
|
|
13216
|
+
volumeId: scope.volumeId ?? null,
|
|
13217
|
+
volumeIds: scope.volumeIds ?? [],
|
|
13177
13218
|
includeAllSettings: scope.includeAllSettings === true,
|
|
13178
13219
|
characterIds: scope.characterIds ?? [],
|
|
13179
13220
|
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
13180
13221
|
modelId
|
|
13181
13222
|
});
|
|
13182
|
-
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
13223
|
+
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["volume", "指定分卷"], ["book", "全书"]]) + chapterField + volumeField + relationshipFields, async (form) => {
|
|
13224
|
+
const workId = state.work.id;
|
|
13183
13225
|
const taskType = String(form.get("taskType"));
|
|
13184
13226
|
const modelId = String(form.get("modelId"));
|
|
13185
13227
|
const scope = buildRelationshipScope(form);
|
|
13228
|
+
if (scope.type === "chapter" && !scope.chapterIds?.length) return toast("请先选择至少一个章节", "error");
|
|
13229
|
+
if (scope.type === "volume" && !scope.volumeIds?.length) return toast("请先选择至少一个分卷", "error");
|
|
13186
13230
|
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
13187
13231
|
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
13188
13232
|
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
@@ -13192,15 +13236,19 @@ async function openTaskDialog() {
|
|
|
13192
13236
|
sourceVersion: input.dataset.sourceVersion
|
|
13193
13237
|
}));
|
|
13194
13238
|
}
|
|
13195
|
-
|
|
13196
|
-
await refreshBackgroundTaskCenter({ announce: false });
|
|
13197
|
-
taskListPage = 1;
|
|
13239
|
+
clearContextWarning();
|
|
13198
13240
|
try {
|
|
13199
|
-
await
|
|
13200
|
-
toast("分析任务已创建,已进入任务队列");
|
|
13241
|
+
await api(`/api/works/${workId}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
|
|
13201
13242
|
} catch (error) {
|
|
13202
|
-
|
|
13243
|
+
if (error.code === "AI_CONTEXT_TOO_LARGE") {
|
|
13244
|
+
contextWarning.textContent = error.message || "当前分析范围超过模型上下文阈值,请切换到上下文更长的模型后重试。";
|
|
13245
|
+
contextWarning.classList.remove("hidden");
|
|
13246
|
+
}
|
|
13247
|
+
throw error;
|
|
13203
13248
|
}
|
|
13249
|
+
taskListPage = 1;
|
|
13250
|
+
toast("分析任务已创建,已进入任务队列");
|
|
13251
|
+
void refreshAnalysisTaskViewsAfterCreate(workId);
|
|
13204
13252
|
}, "AI 分析", {
|
|
13205
13253
|
submitLabel: "创建任务",
|
|
13206
13254
|
pendingLabel: "创建中…",
|
|
@@ -13210,8 +13258,42 @@ async function openTaskDialog() {
|
|
|
13210
13258
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
13211
13259
|
const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
|
|
13212
13260
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
13213
|
-
const
|
|
13214
|
-
|
|
13261
|
+
const scopeFields = {
|
|
13262
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]'),
|
|
13263
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-field="volume"]')
|
|
13264
|
+
};
|
|
13265
|
+
const scopeTriggers = {
|
|
13266
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]'),
|
|
13267
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-trigger="volume"]')
|
|
13268
|
+
};
|
|
13269
|
+
const scopeBubbles = {
|
|
13270
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]'),
|
|
13271
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-bubble="volume"]')
|
|
13272
|
+
};
|
|
13273
|
+
const scopeSearches = {
|
|
13274
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]'),
|
|
13275
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-search="volume"]')
|
|
13276
|
+
};
|
|
13277
|
+
const scopeInputs = {
|
|
13278
|
+
chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')],
|
|
13279
|
+
volume: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="volume"]')]
|
|
13280
|
+
};
|
|
13281
|
+
const scopeSummaries = {
|
|
13282
|
+
chapter: $("#dialog-fields").querySelector("#task-chapter-summary"),
|
|
13283
|
+
volume: $("#dialog-fields").querySelector("#task-volume-summary")
|
|
13284
|
+
};
|
|
13285
|
+
const scopeCounts = {
|
|
13286
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]'),
|
|
13287
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-count="volume"]')
|
|
13288
|
+
};
|
|
13289
|
+
const scopeClearButtons = {
|
|
13290
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]'),
|
|
13291
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-clear="volume"]')
|
|
13292
|
+
};
|
|
13293
|
+
const scopeEmptyMessages = {
|
|
13294
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-empty="chapter"]'),
|
|
13295
|
+
volume: $("#dialog-fields").querySelector('[data-task-scope-empty="volume"]')
|
|
13296
|
+
};
|
|
13215
13297
|
const description = $("#analysis-type-description");
|
|
13216
13298
|
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
13217
13299
|
const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
|
|
@@ -13229,17 +13311,57 @@ async function openTaskDialog() {
|
|
|
13229
13311
|
const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
|
|
13230
13312
|
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
13231
13313
|
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
|
|
13314
|
+
const contextWarning = $("#dialog-fields").querySelector("[data-analysis-context-warning]");
|
|
13315
|
+
const clearContextWarning = () => {
|
|
13316
|
+
contextWarning.textContent = "";
|
|
13317
|
+
contextWarning.classList.add("hidden");
|
|
13318
|
+
};
|
|
13232
13319
|
const allSettingsOption = document.createElement("option");
|
|
13233
13320
|
allSettingsOption.value = "book-with-settings";
|
|
13234
13321
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
13235
13322
|
const settingsOnlyOption = document.createElement("option");
|
|
13236
13323
|
settingsOnlyOption.value = "settings";
|
|
13237
13324
|
settingsOnlyOption.textContent = "仅设定集";
|
|
13325
|
+
const setTaskScopeBubbleOpen = (kind, open) => {
|
|
13326
|
+
scopeBubbles[kind].classList.toggle("hidden", !open);
|
|
13327
|
+
scopeTriggers[kind].setAttribute("aria-expanded", String(open));
|
|
13328
|
+
if (open) scopeSearches[kind].focus();
|
|
13329
|
+
};
|
|
13330
|
+
const syncTaskScopePicker = (kind) => {
|
|
13331
|
+
const selected = scopeInputs[kind].filter((input) => input.checked);
|
|
13332
|
+
const selectedNames = selected.map((input) => input.closest(".task-scope-option")?.querySelector("strong")?.textContent ?? "");
|
|
13333
|
+
const label = kind === "chapter" ? "选择需要分析的章节" : "选择需要分析的分卷";
|
|
13334
|
+
scopeSummaries[kind].textContent = selectedNames.length
|
|
13335
|
+
? `${selectedNames.slice(0, 2).join("、")}${selectedNames.length > 2 ? ` 等 ${selectedNames.length} 个` : ""}`
|
|
13336
|
+
: label;
|
|
13337
|
+
scopeCounts[kind].textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
13338
|
+
scopeClearButtons[kind].disabled = selected.length === 0;
|
|
13339
|
+
scopeTriggers[kind].setAttribute("aria-label", `筛选${kind === "chapter" ? "章节" : "分卷"},已选择 ${selected.length} 个`);
|
|
13340
|
+
};
|
|
13341
|
+
const filterTaskScopeOptions = (kind) => {
|
|
13342
|
+
const query = scopeSearches[kind].value.trim().toLocaleLowerCase();
|
|
13343
|
+
let visibleCount = 0;
|
|
13344
|
+
for (const input of scopeInputs[kind]) {
|
|
13345
|
+
const option = input.closest(".task-scope-option");
|
|
13346
|
+
const visible = !query || option.dataset.taskScopeSearchName.includes(query);
|
|
13347
|
+
option.classList.toggle("hidden", !visible);
|
|
13348
|
+
if (visible) visibleCount += 1;
|
|
13349
|
+
}
|
|
13350
|
+
scopeEmptyMessages[kind].classList.toggle("hidden", visibleCount > 0);
|
|
13351
|
+
};
|
|
13238
13352
|
const syncChapterField = () => {
|
|
13239
|
-
const
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13353
|
+
const activeKind = ["chapter", "volume"].includes(scopeTypeSelect.value) ? scopeTypeSelect.value : null;
|
|
13354
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13355
|
+
const enabled = kind === activeKind;
|
|
13356
|
+
scopeFields[kind].classList.toggle("is-disabled", !enabled);
|
|
13357
|
+
scopeFields[kind].classList.toggle("hidden", !enabled);
|
|
13358
|
+
scopeFields[kind].setAttribute("aria-disabled", String(!enabled));
|
|
13359
|
+
scopeTriggers[kind].disabled = !enabled;
|
|
13360
|
+
for (const input of scopeInputs[kind]) input.disabled = !enabled;
|
|
13361
|
+
if (!enabled) setTaskScopeBubbleOpen(kind, false);
|
|
13362
|
+
syncTaskScopePicker(kind);
|
|
13363
|
+
filterTaskScopeOptions(kind);
|
|
13364
|
+
}
|
|
13243
13365
|
};
|
|
13244
13366
|
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
13245
13367
|
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
@@ -13337,10 +13459,12 @@ async function openTaskDialog() {
|
|
|
13337
13459
|
taskTypeSelect.addEventListener("change", () => {
|
|
13338
13460
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
13339
13461
|
syncTaskModelDefault();
|
|
13462
|
+
clearContextWarning();
|
|
13340
13463
|
invalidateRelationshipSourcePreview();
|
|
13341
13464
|
syncRelationshipOptions();
|
|
13342
13465
|
});
|
|
13343
13466
|
taskModelSelect.addEventListener("change", () => {
|
|
13467
|
+
clearContextWarning();
|
|
13344
13468
|
invalidateRelationshipSourcePreview();
|
|
13345
13469
|
syncRelationshipOptions();
|
|
13346
13470
|
});
|
|
@@ -13387,11 +13511,39 @@ async function openTaskDialog() {
|
|
|
13387
13511
|
syncRelationshipOptions();
|
|
13388
13512
|
}
|
|
13389
13513
|
});
|
|
13514
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13515
|
+
scopeTriggers[kind].addEventListener("click", () => {
|
|
13516
|
+
setTaskScopeBubbleOpen(kind, scopeTriggers[kind].getAttribute("aria-expanded") !== "true");
|
|
13517
|
+
});
|
|
13518
|
+
scopeSearches[kind].addEventListener("input", () => filterTaskScopeOptions(kind));
|
|
13519
|
+
for (const input of scopeInputs[kind]) input.addEventListener("change", () => {
|
|
13520
|
+
syncTaskScopePicker(kind);
|
|
13521
|
+
clearContextWarning();
|
|
13522
|
+
invalidateRelationshipSourcePreview();
|
|
13523
|
+
});
|
|
13524
|
+
scopeClearButtons[kind].addEventListener("click", () => {
|
|
13525
|
+
for (const input of scopeInputs[kind]) input.checked = false;
|
|
13526
|
+
syncTaskScopePicker(kind);
|
|
13527
|
+
clearContextWarning();
|
|
13528
|
+
invalidateRelationshipSourcePreview();
|
|
13529
|
+
});
|
|
13530
|
+
}
|
|
13390
13531
|
$("#dynamic-form").onclick = (event) => {
|
|
13391
13532
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
13533
|
+
for (const kind of ["chapter", "volume"]) {
|
|
13534
|
+
if (!scopeFields[kind].contains(event.target)) setTaskScopeBubbleOpen(kind, false);
|
|
13535
|
+
}
|
|
13392
13536
|
};
|
|
13393
13537
|
$("#dynamic-form").onkeydown = (event) => {
|
|
13394
|
-
if (event.key !== "Escape"
|
|
13538
|
+
if (event.key !== "Escape") return;
|
|
13539
|
+
const openScope = ["chapter", "volume"].find((kind) => !scopeBubbles[kind].classList.contains("hidden"));
|
|
13540
|
+
if (openScope) {
|
|
13541
|
+
event.preventDefault();
|
|
13542
|
+
setTaskScopeBubbleOpen(openScope, false);
|
|
13543
|
+
scopeTriggers[openScope].focus();
|
|
13544
|
+
return;
|
|
13545
|
+
}
|
|
13546
|
+
if (relationshipCharacterBubble.classList.contains("hidden")) return;
|
|
13395
13547
|
event.preventDefault();
|
|
13396
13548
|
setRelationshipCharacterBubbleOpen(false);
|
|
13397
13549
|
relationshipCharacterTrigger.focus();
|
|
@@ -13400,10 +13552,23 @@ async function openTaskDialog() {
|
|
|
13400
13552
|
invalidateRelationshipSourcePreview();
|
|
13401
13553
|
syncChapterField();
|
|
13402
13554
|
});
|
|
13403
|
-
chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
13404
13555
|
syncRelationshipOptions();
|
|
13405
13556
|
}
|
|
13406
13557
|
|
|
13558
|
+
async function refreshAnalysisTaskViewsAfterCreate(workId) {
|
|
13559
|
+
try {
|
|
13560
|
+
await Promise.all([
|
|
13561
|
+
refreshBackgroundTaskCenter({ announce: false }),
|
|
13562
|
+
state.module === "tasks" && state.work?.id === workId
|
|
13563
|
+
? renderTasks(1, { refresh: true })
|
|
13564
|
+
: Promise.resolve()
|
|
13565
|
+
]);
|
|
13566
|
+
} catch (error) {
|
|
13567
|
+
console.error("Failed to refresh analysis task views after creation", error);
|
|
13568
|
+
toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
|
|
13569
|
+
}
|
|
13570
|
+
}
|
|
13571
|
+
|
|
13407
13572
|
function openProviderDialog(item) {
|
|
13408
13573
|
const protocol = item?.protocol ?? "openai-chat-completions";
|
|
13409
13574
|
const providerProtocolOptions = [
|
|
@@ -15605,6 +15770,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
15605
15770
|
});
|
|
15606
15771
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
15607
15772
|
$("#module-create-button").addEventListener("click", () => ({ drafts: openDraftDialog, settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
15773
|
+
bindPlainTextPaste($("#ai-prompt"));
|
|
15608
15774
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
15609
15775
|
updateAiMentionMenu();
|
|
15610
15776
|
setAiContextMeter(null);
|
package/dist/public/index.html
CHANGED
|
@@ -1164,6 +1164,6 @@
|
|
|
1164
1164
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1165
1165
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1166
1166
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1167
|
-
<script type="module" src="/app.js?v=20260815-
|
|
1167
|
+
<script type="module" src="/app.js?v=20260815-analysis-task-create-v1"></script>
|
|
1168
1168
|
</body>
|
|
1169
1169
|
</html>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function normalizePlainText(value) {
|
|
2
|
+
return String(value ?? "").replace(/\r\n?/gu, "\n");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function readClipboardPlainText(clipboardData) {
|
|
6
|
+
if (!clipboardData || typeof clipboardData.getData !== "function") return "";
|
|
7
|
+
try {
|
|
8
|
+
return normalizePlainText(clipboardData.getData("text/plain"));
|
|
9
|
+
} catch {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isInside(root, node) {
|
|
15
|
+
return Boolean(node) && (node === root || root.contains(node));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function insertIntoContentEditable(target, text, documentRef, windowRef) {
|
|
19
|
+
const selection = windowRef?.getSelection?.();
|
|
20
|
+
const currentRange = selection?.rangeCount ? selection.getRangeAt(0) : null;
|
|
21
|
+
const range = currentRange && isInside(target, currentRange.commonAncestorContainer)
|
|
22
|
+
? currentRange.cloneRange()
|
|
23
|
+
: documentRef.createRange();
|
|
24
|
+
if (!currentRange || !isInside(target, currentRange.commonAncestorContainer)) {
|
|
25
|
+
target.focus?.();
|
|
26
|
+
range.selectNodeContents(target);
|
|
27
|
+
range.collapse(false);
|
|
28
|
+
}
|
|
29
|
+
range.deleteContents();
|
|
30
|
+
const fragment = documentRef.createDocumentFragment();
|
|
31
|
+
normalizePlainText(text).split("\n").forEach((line, index) => {
|
|
32
|
+
if (index > 0) fragment.append(documentRef.createElement("br"));
|
|
33
|
+
if (line) fragment.append(documentRef.createTextNode(line));
|
|
34
|
+
});
|
|
35
|
+
range.insertNode(fragment);
|
|
36
|
+
range.collapse(false);
|
|
37
|
+
if (selection) {
|
|
38
|
+
selection.removeAllRanges();
|
|
39
|
+
selection.addRange(range);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function insertIntoTextArea(target, text) {
|
|
45
|
+
const value = String(target.value ?? "");
|
|
46
|
+
const start = Number.isInteger(target.selectionStart) ? target.selectionStart : value.length;
|
|
47
|
+
const end = Number.isInteger(target.selectionEnd) ? target.selectionEnd : start;
|
|
48
|
+
target.value = `${value.slice(0, start)}${normalizePlainText(text)}${value.slice(end)}`;
|
|
49
|
+
target.setSelectionRange?.(start + normalizePlainText(text).length, start + normalizePlainText(text).length);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function dispatchInput(target, text, windowRef) {
|
|
54
|
+
const InputEventConstructor = windowRef?.InputEvent;
|
|
55
|
+
const EventConstructor = windowRef?.Event;
|
|
56
|
+
if (typeof InputEventConstructor === "function") {
|
|
57
|
+
target.dispatchEvent(new InputEventConstructor("input", { bubbles: true, inputType: "insertFromPaste", data: text }));
|
|
58
|
+
} else if (typeof EventConstructor === "function") {
|
|
59
|
+
target.dispatchEvent(new EventConstructor("input", { bubbles: true }));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function insertClipboardPlainText(target, text, documentRef = globalThis.document, windowRef = globalThis.window) {
|
|
64
|
+
if (!target) return false;
|
|
65
|
+
if (target.tagName === "TEXTAREA") return insertIntoTextArea(target, text);
|
|
66
|
+
if (target.getAttribute?.("contenteditable") === "true") return insertIntoContentEditable(target, text, documentRef, windowRef);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pasteTarget(event, root) {
|
|
71
|
+
let target = event.target;
|
|
72
|
+
if (target?.nodeType !== 1) target = target?.parentElement;
|
|
73
|
+
const editable = target?.closest?.("textarea, [contenteditable=\"true\"]");
|
|
74
|
+
return editable && isInside(root, editable) ? editable : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function bindPlainTextPaste(root, documentRef = globalThis.document, windowRef = globalThis.window) {
|
|
78
|
+
if (!root) return () => {};
|
|
79
|
+
const handler = (event) => {
|
|
80
|
+
const target = pasteTarget(event, root);
|
|
81
|
+
if (!target) return;
|
|
82
|
+
const text = readClipboardPlainText(event.clipboardData);
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
event.stopImmediatePropagation();
|
|
85
|
+
if (insertClipboardPlainText(target, text, documentRef, windowRef)) dispatchInput(target, text, windowRef);
|
|
86
|
+
};
|
|
87
|
+
root.addEventListener("paste", handler, true);
|
|
88
|
+
return () => root.removeEventListener("paste", handler, true);
|
|
89
|
+
}
|
package/dist/public/styles.css
CHANGED
|
@@ -2878,6 +2878,33 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2878
2878
|
.task-chapter-field { transition: opacity .15s ease; }
|
|
2879
2879
|
.task-chapter-field.is-disabled { opacity: .48; }
|
|
2880
2880
|
.task-chapter-field select:disabled { cursor: not-allowed; }
|
|
2881
|
+
.task-scope-field { min-width: 0; transition: opacity .15s ease; }
|
|
2882
|
+
.task-scope-field.is-disabled { opacity: .48; }
|
|
2883
|
+
.task-scope-picker { display: grid; gap: 8px; min-width: 0; }
|
|
2884
|
+
.dialog-fields .task-scope-trigger { display: flex; width: 100%; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; text-align: left; }
|
|
2885
|
+
.task-scope-trigger:hover, .task-scope-trigger:focus-visible, .task-scope-trigger[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent); outline: none; }
|
|
2886
|
+
.task-scope-trigger:disabled { cursor: not-allowed; opacity: .48; }
|
|
2887
|
+
.task-scope-trigger > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2888
|
+
.task-scope-trigger-meta { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 10px; }
|
|
2889
|
+
.task-scope-chevron { font-size: 15px; line-height: 1; transition: transform .15s ease; }
|
|
2890
|
+
.task-scope-trigger[aria-expanded="true"] .task-scope-chevron { transform: rotate(180deg); }
|
|
2891
|
+
.task-scope-bubble { position: relative; display: grid; gap: 9px; padding: 12px; border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-radius: 8px; background: var(--surface-soft); box-shadow: 0 10px 30px rgba(48,39,31,.12); }
|
|
2892
|
+
.task-scope-bubble::before { position: absolute; top: -6px; right: 20px; width: 10px; height: 10px; border-top: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-left: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); background: var(--surface-soft); content: ""; transform: rotate(45deg); }
|
|
2893
|
+
.dialog-fields .task-scope-search { gap: 5px; }
|
|
2894
|
+
.dialog-fields .task-scope-search input { min-height: 36px; padding: 7px 9px; background: var(--surface); }
|
|
2895
|
+
.task-scope-bubble-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 9px; }
|
|
2896
|
+
.task-scope-bubble-meta button { padding: 2px 0; border: 0; background: transparent; color: var(--accent-dark); font-size: 9px; }
|
|
2897
|
+
.task-scope-bubble-meta button:disabled { color: var(--muted); cursor: default; opacity: .45; }
|
|
2898
|
+
.task-scope-options { display: grid; gap: 6px; max-height: 220px; overflow-y: auto; padding-right: 3px; }
|
|
2899
|
+
.dialog-fields .task-scope-option { display: grid; grid-template-columns: 16px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); cursor: pointer; }
|
|
2900
|
+
.dialog-fields .task-scope-option.hidden { display: none; }
|
|
2901
|
+
.dialog-fields .task-scope-option input { width: 15px; height: 15px; margin: 0; }
|
|
2902
|
+
.task-scope-option > span { display: grid; gap: 2px; min-width: 0; }
|
|
2903
|
+
.task-scope-option strong { overflow: hidden; color: var(--ink); font-size: 10px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
|
2904
|
+
.task-scope-option small { overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2905
|
+
.task-scope-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
|
|
2906
|
+
.task-scope-option input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2907
|
+
.task-scope-empty { margin: 0; padding: 18px 8px; color: var(--muted); font-size: 10px; text-align: center; }
|
|
2881
2908
|
.relationship-analysis-options { display: grid; gap: 14px; }
|
|
2882
2909
|
.relationship-character-field { min-width: 0; }
|
|
2883
2910
|
.relationship-character-picker { display: grid; gap: 8px; min-width: 0; }
|
|
@@ -2927,6 +2954,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2927
2954
|
.relationship-source-preview-match { padding: 2px 6px; border-radius: 10px; background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent-dark); font-size: 8px; white-space: nowrap; }
|
|
2928
2955
|
.relationship-source-preview-match.is-fuzzy { background: color-mix(in srgb, var(--warning, #a56f19) 12%, transparent); color: var(--warning, #8b5c10); }
|
|
2929
2956
|
.relationship-source-preview-empty { margin: 0; padding: 9px; border: 1px dashed var(--line); border-radius: 4px; }
|
|
2957
|
+
.analysis-context-warning { margin: 7px 0 0; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--warning, #a56f19) 45%, var(--line)); border-radius: 4px; background: color-mix(in srgb, var(--warning, #a56f19) 9%, transparent); color: var(--warning, #8b5c10); font-size: 10px; line-height: 1.55; }
|
|
2930
2958
|
.model-temperature-hint, .model-context-window-hint { margin: 0; color: var(--accent-dark); font-size: 10px; line-height: 1.55; }
|
|
2931
2959
|
.item-list-rows { display: grid; gap: 7px; }
|
|
2932
2960
|
.item-list-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 7px; }
|