@musnows/scriverse 0.7.11 → 0.7.13
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 +468 -90
- 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 +321 -29
- package/dist/public/index.html +4 -4
- package/dist/public/plain-text-paste.js +89 -0
- package/dist/public/stream-typewriter.d.ts +12 -1
- package/dist/public/stream-typewriter.js +65 -5
- package/dist/public/styles.css +85 -1
- package/dist/store.js +384 -97
- 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
|
@@ -19,13 +19,14 @@ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelCo
|
|
|
19
19
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260812-connectivity-cooldown-v1";
|
|
20
20
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
21
21
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
22
|
-
import { createStreamTypewriter } from "/stream-typewriter.js?v=
|
|
22
|
+
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260815-ai-stream-typewriter-v4";
|
|
23
23
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
24
24
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
25
25
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
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";
|
|
@@ -550,7 +551,15 @@ function cancelActiveAiRequest(reason) {
|
|
|
550
551
|
return cancelled;
|
|
551
552
|
}
|
|
552
553
|
|
|
553
|
-
function beginAiConversationNavigation(reason) {
|
|
554
|
+
function beginAiConversationNavigation(reason, action = "切换会话") {
|
|
555
|
+
if (aiRequestManager.hasActive()) {
|
|
556
|
+
const isNewConversation = action === "新建会话";
|
|
557
|
+
toast(
|
|
558
|
+
`当前 turn 尚未结束,${action}会中断生成;${isNewConversation ? "\n" : ""}已收到的内容会保留在历史记录中`,
|
|
559
|
+
"warning",
|
|
560
|
+
isNewConversation ? "ai-new-conversation-toast" : ""
|
|
561
|
+
);
|
|
562
|
+
}
|
|
554
563
|
aiConversationNavigationGeneration += 1;
|
|
555
564
|
aiConversationNavigationPending = aiConversationNavigationGeneration;
|
|
556
565
|
const navigation = Object.freeze({
|
|
@@ -573,7 +582,10 @@ function finishAiConversationNavigation(navigation) {
|
|
|
573
582
|
return true;
|
|
574
583
|
}
|
|
575
584
|
|
|
576
|
-
function invalidateAiConversationNavigation(reason) {
|
|
585
|
+
function invalidateAiConversationNavigation(reason, action = "切换作品") {
|
|
586
|
+
if (aiRequestManager.hasActive()) {
|
|
587
|
+
toast(`当前 turn 尚未结束,${action}会中断生成;已收到的内容会保留在历史记录中`, "warning");
|
|
588
|
+
}
|
|
577
589
|
aiConversationNavigationGeneration += 1;
|
|
578
590
|
aiConversationNavigationPending = null;
|
|
579
591
|
cancelActiveAiRequest(reason);
|
|
@@ -1870,7 +1882,7 @@ function renderMessageCardActions(message) {
|
|
|
1870
1882
|
});
|
|
1871
1883
|
actions.append(copy);
|
|
1872
1884
|
}
|
|
1873
|
-
if (message.dataset.messageId) {
|
|
1885
|
+
if (message.dataset.messageId && message.classList.contains("assistant-message")) {
|
|
1874
1886
|
const fork = document.createElement("button");
|
|
1875
1887
|
fork.type = "button";
|
|
1876
1888
|
fork.className = "message-fork-button";
|
|
@@ -2000,6 +2012,29 @@ function formatAiToolCallTime(value) {
|
|
|
2000
2012
|
}).format(date);
|
|
2001
2013
|
}
|
|
2002
2014
|
|
|
2015
|
+
function setAiToolCallCopyButtonState(button, copied) {
|
|
2016
|
+
const label = button.dataset.copyLabel ?? "代码块";
|
|
2017
|
+
button.dataset.copyState = copied ? "copied" : "idle";
|
|
2018
|
+
button.classList.toggle("is-copied", copied);
|
|
2019
|
+
button.setAttribute("aria-label", copied ? `${label}已复制` : `复制${label}`);
|
|
2020
|
+
button.title = copied ? "已复制" : `复制${label}`;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
async function copyAiToolCallCode(button) {
|
|
2024
|
+
const targetId = button.dataset.copyTarget;
|
|
2025
|
+
const target = targetId ? document.getElementById(targetId) : null;
|
|
2026
|
+
if (!target) return;
|
|
2027
|
+
try {
|
|
2028
|
+
await copyAiRawMarkdown(target.textContent);
|
|
2029
|
+
setAiToolCallCopyButtonState(button, true);
|
|
2030
|
+
window.setTimeout(() => {
|
|
2031
|
+
if (button.isConnected && button.dataset.copyState === "copied") setAiToolCallCopyButtonState(button, false);
|
|
2032
|
+
}, 1200);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
toast(error.message, "error");
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2003
2038
|
function openAiToolCallDetail(toolCall) {
|
|
2004
2039
|
const name = String(toolCall?.name ?? "unknown");
|
|
2005
2040
|
const status = toolCall?.status === "failed" ? "调用失败" : "调用成功";
|
|
@@ -2016,6 +2051,7 @@ function openAiToolCallDetail(toolCall) {
|
|
|
2016
2051
|
$("#ai-tool-call-arguments").textContent = JSON.stringify(toolCall?.arguments ?? {}, null, 2);
|
|
2017
2052
|
$("#ai-tool-call-result-length").textContent = `${resultDetails.characterCount.toLocaleString("zh-CN")} 字符`;
|
|
2018
2053
|
$("#ai-tool-call-result").textContent = resultDetails.text;
|
|
2054
|
+
document.querySelectorAll("[data-ai-tool-call-copy]").forEach((button) => setAiToolCallCopyButtonState(button, false));
|
|
2019
2055
|
$("#ai-tool-call-dialog").showModal();
|
|
2020
2056
|
}
|
|
2021
2057
|
|
|
@@ -2417,7 +2453,7 @@ async function ensureAiConversationsLoaded() {
|
|
|
2417
2453
|
|
|
2418
2454
|
async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
|
|
2419
2455
|
if (!state.work) return null;
|
|
2420
|
-
const navigation = beginAiConversationNavigation("已切换 AI 对话");
|
|
2456
|
+
const navigation = beginAiConversationNavigation("已切换 AI 对话", "切换会话");
|
|
2421
2457
|
try {
|
|
2422
2458
|
const parameters = new URLSearchParams({ page: "1", limit: "100" });
|
|
2423
2459
|
if (focusMessageId) parameters.set("messageId", String(focusMessageId));
|
|
@@ -2487,7 +2523,7 @@ function applyNewAiConversation(conversation) {
|
|
|
2487
2523
|
|
|
2488
2524
|
async function createNewAiConversation(taskType = "chat") {
|
|
2489
2525
|
if (!state.work) return null;
|
|
2490
|
-
const navigation = beginAiConversationNavigation("已新建 AI 对话");
|
|
2526
|
+
const navigation = beginAiConversationNavigation("已新建 AI 对话", "新建会话");
|
|
2491
2527
|
try {
|
|
2492
2528
|
const conversation = await api(`/api/works/${navigation.workId}/ai-conversations`, { method: "POST", body: { taskType } });
|
|
2493
2529
|
if (!isAiConversationNavigationCurrent(navigation)) return null;
|
|
@@ -3897,11 +3933,11 @@ function dismissChapterInsightToast() {
|
|
|
3897
3933
|
}
|
|
3898
3934
|
}
|
|
3899
3935
|
|
|
3900
|
-
function toast(message, type = "info") {
|
|
3936
|
+
function toast(message, type = "info", extraClass = "") {
|
|
3901
3937
|
if (systemRestartDetected || (!state.user && document.documentElement.classList.contains("login-route"))) return;
|
|
3902
3938
|
const region = $("#toast-region");
|
|
3903
3939
|
const element = document.createElement("div");
|
|
3904
|
-
element.className = `toast ${type}`;
|
|
3940
|
+
element.className = `toast ${type}${extraClass ? ` ${extraClass}` : ""}`;
|
|
3905
3941
|
element.setAttribute("role", type === "error" ? "alert" : "status");
|
|
3906
3942
|
element.setAttribute("aria-atomic", "true");
|
|
3907
3943
|
element.textContent = message;
|
|
@@ -9319,7 +9355,8 @@ function renderCharacterExtractionEditor(preview) {
|
|
|
9319
9355
|
if (!items.length) return '<p class="character-extraction-empty">没有可应用的角色候选。</p>';
|
|
9320
9356
|
return `<div class="character-extraction-editor" data-character-extraction-editor data-preview-token="${esc(preview.previewToken)}">
|
|
9321
9357
|
<div class="character-extraction-policy" role="note"><strong>合并规则</strong><span>优先使用任务保存的稳定角色引用,其次使用当前标准名和别名匹配。合并只追加无冲突别名、空缺身份、种族和首次登场;已有非空字段保持不变。</span></div>
|
|
9322
|
-
<div class="character-extraction-editor-toolbar"><span data-character-extraction-selected-count>已选择 0 项</span><button class="ghost-button" type="button" data-character-extraction-select-all>全选可处理项</button><button class="ghost-button" type="button" data-character-extraction-clear>全部跳过</button></div>
|
|
9358
|
+
<div class="character-extraction-editor-toolbar"><button class="primary-button character-extraction-apply-button" type="button" data-apply-character-extraction>确认并应用所选候选</button><span data-character-extraction-selected-count>已选择 0 项</span><button class="ghost-button" type="button" data-character-extraction-select-all>全选可处理项</button><button class="ghost-button" type="button" data-character-extraction-clear>全部跳过</button></div>
|
|
9359
|
+
<p class="character-extraction-apply-note">确认采用整批事务;任一新建项名称冲突时全部回滚。重复点击或网络重试不会重复创建。</p>
|
|
9323
9360
|
<div class="character-extraction-candidate-list">${items.map((item, index) => {
|
|
9324
9361
|
const matches = Array.isArray(item.matchCandidates) ? item.matchCandidates : [];
|
|
9325
9362
|
const suggestedAction = item.suggestedAction === "merge" && matches.length ? "merge" : item.suggestedAction === "skip" ? "skip" : "create";
|
|
@@ -9350,7 +9387,6 @@ function renderCharacterExtractionEditor(preview) {
|
|
|
9350
9387
|
</article>`;
|
|
9351
9388
|
}).join("")}</div>
|
|
9352
9389
|
<p class="character-extraction-apply-error hidden" data-character-extraction-error role="alert"></p>
|
|
9353
|
-
<div class="character-extraction-apply-actions"><button class="primary-button" type="button" data-apply-character-extraction>确认并应用所选候选</button><small>确认采用整批事务;任一新建项名称冲突时全部回滚。重复点击或网络重试不会重复创建。</small></div>
|
|
9354
9390
|
</div>`;
|
|
9355
9391
|
}
|
|
9356
9392
|
|
|
@@ -11821,6 +11857,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
11821
11857
|
const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
|
|
11822
11858
|
attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
|
|
11823
11859
|
editor.__attachmentObserver = attachmentObserver;
|
|
11860
|
+
editor.__plainTextPasteCleanup = bindPlainTextPaste(host);
|
|
11824
11861
|
host.__vditor = editor;
|
|
11825
11862
|
return editor;
|
|
11826
11863
|
}
|
|
@@ -12012,6 +12049,7 @@ function ensureVditorIconScript() {
|
|
|
12012
12049
|
function destroyVditorEditor(editor) {
|
|
12013
12050
|
if (!editor) return;
|
|
12014
12051
|
editor.__attachmentObserver?.disconnect();
|
|
12052
|
+
editor.__plainTextPasteCleanup?.();
|
|
12015
12053
|
editor.__vditorLineNumberObserver?.disconnect();
|
|
12016
12054
|
editor.__vditorLineNumberResizeObserver?.disconnect();
|
|
12017
12055
|
if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
|
|
@@ -13084,7 +13122,13 @@ function openReviewDialog() {
|
|
|
13084
13122
|
}
|
|
13085
13123
|
|
|
13086
13124
|
async function openTaskDialog() {
|
|
13087
|
-
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) =>
|
|
13125
|
+
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
13126
|
+
id: String(chapter.id),
|
|
13127
|
+
title: String(chapter.title),
|
|
13128
|
+
volumeId: String(volume.id),
|
|
13129
|
+
volumeTitle: String(volume.title),
|
|
13130
|
+
chapterType: String(chapter.chapterType || "正文")
|
|
13131
|
+
})));
|
|
13088
13132
|
let relationshipCharacters = [];
|
|
13089
13133
|
let taskModels = [];
|
|
13090
13134
|
let taskDefaults = [];
|
|
@@ -13100,6 +13144,67 @@ async function openTaskDialog() {
|
|
|
13100
13144
|
toast(`分析任务配置加载失败:${error.message}`, "error");
|
|
13101
13145
|
return;
|
|
13102
13146
|
}
|
|
13147
|
+
const taskScopePicker = (options, emptyLabel) => {
|
|
13148
|
+
const kind = "chapter";
|
|
13149
|
+
const label = "章节";
|
|
13150
|
+
const inputName = "chapterIds";
|
|
13151
|
+
const renderOption = (item) => `<label class="task-scope-option" data-task-scope-search-name="${esc(`${item.title} ${item.volumeTitle ?? ""}`.toLocaleLowerCase())}">
|
|
13152
|
+
<input type="checkbox" name="${inputName}" value="${esc(item.id)}" data-task-scope-input="${kind}" data-task-scope-title="${esc(item.title)}" data-task-scope-subtitle="${esc(item.volumeTitle ?? "")}">
|
|
13153
|
+
<span class="task-scope-checkbox" aria-hidden="true"></span>
|
|
13154
|
+
<span class="task-scope-option-copy"><strong>${esc(item.title)}</strong>${item.volumeTitle ? `<small>${esc(item.volumeTitle)}${item.chapterType && item.chapterType !== "正文" ? ` · ${esc(item.chapterType)}` : ""}</small>` : ""}</span>
|
|
13155
|
+
</label>`;
|
|
13156
|
+
const optionMarkup = [...options.reduce((groups, item) => {
|
|
13157
|
+
const groupId = String(item.volumeId || "");
|
|
13158
|
+
const groupTitle = String(item.volumeTitle || "未分卷章节");
|
|
13159
|
+
const groupKey = `${groupId}:${groupTitle}`;
|
|
13160
|
+
const group = groups.get(groupKey)?.options ?? [];
|
|
13161
|
+
group.push(item);
|
|
13162
|
+
groups.set(groupKey, { id: groupId, title: groupTitle, options: group });
|
|
13163
|
+
return groups;
|
|
13164
|
+
}, new Map())].map(([, group]) => `<section class="task-scope-volume-group" data-task-scope-group data-task-scope-volume-id="${esc(group.id)}">
|
|
13165
|
+
<header>
|
|
13166
|
+
${group.id ? `<label class="task-scope-volume-option">
|
|
13167
|
+
<input type="checkbox" data-task-scope-volume-input="${esc(group.id)}" data-task-scope-volume-title="${esc(group.title)}" aria-label="全选${esc(group.title)}章节">
|
|
13168
|
+
<span class="task-scope-checkbox" aria-hidden="true"></span>
|
|
13169
|
+
<span class="task-scope-volume-copy"><strong>${esc(group.title)}</strong><small>勾选以全选本卷章节</small></span>
|
|
13170
|
+
</label>` : `<strong>${esc(group.title)}</strong>`}
|
|
13171
|
+
<span>${group.options.length} 章</span>
|
|
13172
|
+
</header>
|
|
13173
|
+
${group.options.map(renderOption).join("")}
|
|
13174
|
+
</section>`).join("");
|
|
13175
|
+
return `<div class="form-field task-scope-field task-scope-${kind}-field is-disabled" data-task-scope-field="${kind}" aria-disabled="true">
|
|
13176
|
+
<div class="task-scope-field-heading"><span id="task-${kind}-label">${esc(label)}(可多选)</span><small>从左侧选择,右侧确认已选内容</small></div>
|
|
13177
|
+
<div class="task-scope-picker">
|
|
13178
|
+
<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>
|
|
13179
|
+
<span id="task-${kind}-summary">${esc(emptyLabel)}</span>
|
|
13180
|
+
<span class="task-scope-trigger-meta"><span data-task-scope-count="${kind}">未选择</span><span class="task-scope-chevron" aria-hidden="true">⌄</span></span>
|
|
13181
|
+
</button>
|
|
13182
|
+
<section id="task-${kind}-bubble" class="task-scope-panel hidden" data-task-scope-bubble="${kind}" aria-hidden="true" aria-labelledby="task-${kind}-label">
|
|
13183
|
+
<header class="task-scope-panel-header">
|
|
13184
|
+
<div><strong>选择${esc(label)}</strong><small>支持搜索和按分卷浏览,右侧会实时汇总。</small></div>
|
|
13185
|
+
<button type="button" class="task-scope-clear" data-task-scope-clear="${kind}" disabled>清空已选</button>
|
|
13186
|
+
</header>
|
|
13187
|
+
<div class="task-scope-author-note-banner" role="note">
|
|
13188
|
+
<span class="task-scope-author-note-banner-icon" aria-hidden="true">i</span>
|
|
13189
|
+
<div><strong>范围提示</strong><span>标记为“作者的话”的章节不会纳入 AI 分析输入;即使被选中,也只会保留在作品目录中。</span></div>
|
|
13190
|
+
</div>
|
|
13191
|
+
<div class="task-scope-panel-grid">
|
|
13192
|
+
<section class="task-scope-available" aria-label="待选${esc(label)}">
|
|
13193
|
+
<header class="task-scope-panel-section-header"><strong>待选章节</strong><span data-task-scope-available-count="${kind}">${options.length} 章</span></header>
|
|
13194
|
+
<label class="task-scope-search"><span>筛选${esc(label)}</span><input type="search" data-task-scope-search="${kind}" placeholder="输入名称" autocomplete="off"></label>
|
|
13195
|
+
<div class="task-scope-options" role="group" aria-labelledby="task-${kind}-label">${optionMarkup}</div>
|
|
13196
|
+
<p class="task-scope-empty hidden" data-task-scope-empty="${kind}">没有匹配的${esc(label)}</p>
|
|
13197
|
+
</section>
|
|
13198
|
+
<aside class="task-scope-selected" aria-label="已选章节汇总">
|
|
13199
|
+
<header class="task-scope-panel-section-header"><strong>已选汇总</strong><span data-task-scope-summary-count="${kind}">0 章</span></header>
|
|
13200
|
+
<p class="task-scope-selected-note">创建任务时会分析这里列出的章节;标记为“作者的话”的章节除外。</p>
|
|
13201
|
+
<div class="task-scope-selected-list" data-task-scope-selected-list="${kind}" role="list" aria-live="polite"><p class="task-scope-selected-empty">暂未选择章节</p></div>
|
|
13202
|
+
</aside>
|
|
13203
|
+
</div>
|
|
13204
|
+
</section>
|
|
13205
|
+
</div>
|
|
13206
|
+
</div>`;
|
|
13207
|
+
};
|
|
13103
13208
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
13104
13209
|
const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
|
|
13105
13210
|
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
@@ -13126,8 +13231,8 @@ async function openTaskDialog() {
|
|
|
13126
13231
|
const modelField = `<label>任务模型<select name="modelId" required aria-describedby="analysis-task-model-help">
|
|
13127
13232
|
<option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
|
|
13128
13233
|
${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 =
|
|
13234
|
+
</select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small><p class="analysis-context-warning hidden" data-analysis-context-warning role="alert"></p></label>`;
|
|
13235
|
+
const chapterField = taskScopePicker(chapterOptions, "选择需要分析的章节");
|
|
13131
13236
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
13132
13237
|
${relationshipCharacterPicker}
|
|
13133
13238
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
@@ -13164,25 +13269,34 @@ async function openTaskDialog() {
|
|
|
13164
13269
|
const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
|
|
13165
13270
|
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
13166
13271
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
13272
|
+
const chapterIds = form.getAll("chapterIds").map(String).filter(Boolean);
|
|
13273
|
+
const chapterScope = {
|
|
13274
|
+
type: "chapter",
|
|
13275
|
+
...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
|
|
13276
|
+
...(chapterIds.length ? { chapterIds } : {})
|
|
13277
|
+
};
|
|
13167
13278
|
const scope = settingsOnly
|
|
13168
13279
|
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13169
|
-
:
|
|
13280
|
+
: scopeType === "book" || includeAllSettings
|
|
13170
13281
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13171
|
-
: {
|
|
13282
|
+
: { ...chapterScope, ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
13172
13283
|
return scope;
|
|
13173
13284
|
};
|
|
13174
13285
|
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
13175
13286
|
type: scope.type,
|
|
13176
13287
|
chapterId: scope.chapterId ?? null,
|
|
13288
|
+
chapterIds: scope.chapterIds ?? [],
|
|
13177
13289
|
includeAllSettings: scope.includeAllSettings === true,
|
|
13178
13290
|
characterIds: scope.characterIds ?? [],
|
|
13179
13291
|
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
13180
13292
|
modelId
|
|
13181
13293
|
});
|
|
13182
13294
|
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
13295
|
+
const workId = state.work.id;
|
|
13183
13296
|
const taskType = String(form.get("taskType"));
|
|
13184
13297
|
const modelId = String(form.get("modelId"));
|
|
13185
13298
|
const scope = buildRelationshipScope(form);
|
|
13299
|
+
if (scope.type === "chapter" && !scope.chapterIds?.length) return toast("请先选择至少一个章节", "error");
|
|
13186
13300
|
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
13187
13301
|
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
13188
13302
|
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
@@ -13192,16 +13306,21 @@ async function openTaskDialog() {
|
|
|
13192
13306
|
sourceVersion: input.dataset.sourceVersion
|
|
13193
13307
|
}));
|
|
13194
13308
|
}
|
|
13195
|
-
|
|
13196
|
-
await refreshBackgroundTaskCenter({ announce: false });
|
|
13197
|
-
taskListPage = 1;
|
|
13309
|
+
clearContextWarning();
|
|
13198
13310
|
try {
|
|
13199
|
-
await
|
|
13200
|
-
toast("分析任务已创建,已进入任务队列");
|
|
13311
|
+
await api(`/api/works/${workId}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
|
|
13201
13312
|
} catch (error) {
|
|
13202
|
-
|
|
13313
|
+
if (error.code === "AI_CONTEXT_TOO_LARGE") {
|
|
13314
|
+
contextWarning.textContent = error.message || "当前分析范围超过模型上下文阈值,请切换到上下文更长的模型后重试。";
|
|
13315
|
+
contextWarning.classList.remove("hidden");
|
|
13316
|
+
}
|
|
13317
|
+
throw error;
|
|
13203
13318
|
}
|
|
13319
|
+
taskListPage = 1;
|
|
13320
|
+
toast("分析任务已创建,已进入任务队列");
|
|
13321
|
+
void refreshAnalysisTaskViewsAfterCreate(workId);
|
|
13204
13322
|
}, "AI 分析", {
|
|
13323
|
+
large: true,
|
|
13205
13324
|
submitLabel: "创建任务",
|
|
13206
13325
|
pendingLabel: "创建中…",
|
|
13207
13326
|
pendingMessage: "正在创建分析任务,请稍候",
|
|
@@ -13210,8 +13329,45 @@ async function openTaskDialog() {
|
|
|
13210
13329
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
13211
13330
|
const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
|
|
13212
13331
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
13213
|
-
const
|
|
13214
|
-
|
|
13332
|
+
const scopeFields = {
|
|
13333
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]')
|
|
13334
|
+
};
|
|
13335
|
+
const scopeTriggers = {
|
|
13336
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]')
|
|
13337
|
+
};
|
|
13338
|
+
const scopeBubbles = {
|
|
13339
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]')
|
|
13340
|
+
};
|
|
13341
|
+
const scopeSearches = {
|
|
13342
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]')
|
|
13343
|
+
};
|
|
13344
|
+
const scopeInputs = {
|
|
13345
|
+
chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')]
|
|
13346
|
+
};
|
|
13347
|
+
const scopeVolumeInputs = [
|
|
13348
|
+
...$("#dialog-fields").querySelectorAll("[data-task-scope-volume-input]")
|
|
13349
|
+
];
|
|
13350
|
+
const scopeSummaries = {
|
|
13351
|
+
chapter: $("#dialog-fields").querySelector("#task-chapter-summary")
|
|
13352
|
+
};
|
|
13353
|
+
const scopeCounts = {
|
|
13354
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]')
|
|
13355
|
+
};
|
|
13356
|
+
const scopeClearButtons = {
|
|
13357
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]')
|
|
13358
|
+
};
|
|
13359
|
+
const scopeEmptyMessages = {
|
|
13360
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-empty="chapter"]')
|
|
13361
|
+
};
|
|
13362
|
+
const scopeSelectedLists = {
|
|
13363
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-selected-list="chapter"]')
|
|
13364
|
+
};
|
|
13365
|
+
const scopeSummaryCounts = {
|
|
13366
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-summary-count="chapter"]')
|
|
13367
|
+
};
|
|
13368
|
+
const scopeAvailableCounts = {
|
|
13369
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-available-count="chapter"]')
|
|
13370
|
+
};
|
|
13215
13371
|
const description = $("#analysis-type-description");
|
|
13216
13372
|
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
13217
13373
|
const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
|
|
@@ -13229,17 +13385,82 @@ async function openTaskDialog() {
|
|
|
13229
13385
|
const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
|
|
13230
13386
|
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
13231
13387
|
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
|
|
13388
|
+
const contextWarning = $("#dialog-fields").querySelector("[data-analysis-context-warning]");
|
|
13389
|
+
const clearContextWarning = () => {
|
|
13390
|
+
contextWarning.textContent = "";
|
|
13391
|
+
contextWarning.classList.add("hidden");
|
|
13392
|
+
};
|
|
13232
13393
|
const allSettingsOption = document.createElement("option");
|
|
13233
13394
|
allSettingsOption.value = "book-with-settings";
|
|
13234
13395
|
allSettingsOption.textContent = "全书 + 设定集";
|
|
13235
13396
|
const settingsOnlyOption = document.createElement("option");
|
|
13236
13397
|
settingsOnlyOption.value = "settings";
|
|
13237
13398
|
settingsOnlyOption.textContent = "仅设定集";
|
|
13399
|
+
const setTaskScopeBubbleOpen = (kind, open) => {
|
|
13400
|
+
scopeBubbles[kind].classList.toggle("hidden", !open);
|
|
13401
|
+
scopeTriggers[kind].setAttribute("aria-expanded", String(open));
|
|
13402
|
+
scopeBubbles[kind].setAttribute("aria-hidden", String(!open));
|
|
13403
|
+
if (open) {
|
|
13404
|
+
scopeBubbles[kind].scrollIntoView({ block: "start" });
|
|
13405
|
+
scopeSearches[kind].focus({ preventScroll: true });
|
|
13406
|
+
}
|
|
13407
|
+
};
|
|
13408
|
+
const syncTaskScopeVolumeInputs = () => {
|
|
13409
|
+
for (const volumeInput of scopeVolumeInputs) {
|
|
13410
|
+
const group = volumeInput.closest("[data-task-scope-group]");
|
|
13411
|
+
const chapters = [...group.querySelectorAll('[data-task-scope-input="chapter"]')];
|
|
13412
|
+
const selectedCount = chapters.filter((input) => input.checked).length;
|
|
13413
|
+
volumeInput.checked = chapters.length > 0 && selectedCount === chapters.length;
|
|
13414
|
+
volumeInput.indeterminate = selectedCount > 0 && selectedCount < chapters.length;
|
|
13415
|
+
}
|
|
13416
|
+
};
|
|
13417
|
+
const syncTaskScopePicker = (kind) => {
|
|
13418
|
+
const selected = scopeInputs[kind].filter((input) => input.checked);
|
|
13419
|
+
syncTaskScopeVolumeInputs();
|
|
13420
|
+
const selectedNames = selected.map((input) => input.dataset.taskScopeTitle ?? "");
|
|
13421
|
+
scopeSummaries[kind].textContent = selectedNames.length
|
|
13422
|
+
? selectedNames.length === 1 ? selectedNames[0] : `已选择 ${selectedNames.length} 个章节`
|
|
13423
|
+
: "选择需要分析的章节";
|
|
13424
|
+
scopeCounts[kind].textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
13425
|
+
scopeSummaryCounts[kind].textContent = `${selected.length} 章`;
|
|
13426
|
+
scopeClearButtons[kind].disabled = selected.length === 0;
|
|
13427
|
+
scopeTriggers[kind].setAttribute("aria-label", `筛选章节,已选择 ${selected.length} 个`);
|
|
13428
|
+
scopeSelectedLists[kind].innerHTML = selected.length
|
|
13429
|
+
? selected.map((input) => `<div class="task-scope-selected-item" role="listitem">
|
|
13430
|
+
<span class="task-scope-selected-copy"><strong>${esc(input.dataset.taskScopeTitle ?? "")}</strong>${input.dataset.taskScopeSubtitle ? `<small>${esc(input.dataset.taskScopeSubtitle)}</small>` : ""}</span>
|
|
13431
|
+
<button type="button" data-task-scope-remove="${kind}" data-task-scope-remove-id="${esc(input.value)}" aria-label="移除${esc(input.dataset.taskScopeTitle ?? "")}">×</button>
|
|
13432
|
+
</div>`).join("")
|
|
13433
|
+
: `<p class="task-scope-selected-empty">暂未选择章节</p>`;
|
|
13434
|
+
};
|
|
13435
|
+
const filterTaskScopeOptions = (kind) => {
|
|
13436
|
+
const query = scopeSearches[kind].value.trim().toLocaleLowerCase();
|
|
13437
|
+
let visibleCount = 0;
|
|
13438
|
+
for (const input of scopeInputs[kind]) {
|
|
13439
|
+
const option = input.closest(".task-scope-option");
|
|
13440
|
+
const visible = !query || option.dataset.taskScopeSearchName.includes(query);
|
|
13441
|
+
option.classList.toggle("hidden", !visible);
|
|
13442
|
+
if (visible) visibleCount += 1;
|
|
13443
|
+
}
|
|
13444
|
+
scopeFields[kind].querySelectorAll("[data-task-scope-group]").forEach((group) => {
|
|
13445
|
+
group.classList.toggle("hidden", !group.querySelector(".task-scope-option:not(.hidden)"));
|
|
13446
|
+
});
|
|
13447
|
+
scopeAvailableCounts[kind].textContent = query
|
|
13448
|
+
? `${visibleCount} / ${scopeInputs[kind].length} 章`
|
|
13449
|
+
: `${scopeInputs[kind].length} 章`;
|
|
13450
|
+
scopeEmptyMessages[kind].classList.toggle("hidden", visibleCount > 0);
|
|
13451
|
+
};
|
|
13238
13452
|
const syncChapterField = () => {
|
|
13239
|
-
const
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13453
|
+
const kind = "chapter";
|
|
13454
|
+
const enabled = scopeTypeSelect.value === "chapter";
|
|
13455
|
+
scopeFields[kind].classList.toggle("is-disabled", !enabled);
|
|
13456
|
+
scopeFields[kind].classList.toggle("hidden", !enabled);
|
|
13457
|
+
scopeFields[kind].setAttribute("aria-disabled", String(!enabled));
|
|
13458
|
+
scopeTriggers[kind].disabled = !enabled;
|
|
13459
|
+
for (const input of scopeInputs[kind]) input.disabled = !enabled;
|
|
13460
|
+
for (const input of scopeVolumeInputs) input.disabled = !enabled;
|
|
13461
|
+
if (!enabled) setTaskScopeBubbleOpen(kind, false);
|
|
13462
|
+
syncTaskScopePicker(kind);
|
|
13463
|
+
filterTaskScopeOptions(kind);
|
|
13243
13464
|
};
|
|
13244
13465
|
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
13245
13466
|
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
@@ -13337,10 +13558,12 @@ async function openTaskDialog() {
|
|
|
13337
13558
|
taskTypeSelect.addEventListener("change", () => {
|
|
13338
13559
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
13339
13560
|
syncTaskModelDefault();
|
|
13561
|
+
clearContextWarning();
|
|
13340
13562
|
invalidateRelationshipSourcePreview();
|
|
13341
13563
|
syncRelationshipOptions();
|
|
13342
13564
|
});
|
|
13343
13565
|
taskModelSelect.addEventListener("change", () => {
|
|
13566
|
+
clearContextWarning();
|
|
13344
13567
|
invalidateRelationshipSourcePreview();
|
|
13345
13568
|
syncRelationshipOptions();
|
|
13346
13569
|
});
|
|
@@ -13387,11 +13610,56 @@ async function openTaskDialog() {
|
|
|
13387
13610
|
syncRelationshipOptions();
|
|
13388
13611
|
}
|
|
13389
13612
|
});
|
|
13613
|
+
for (const kind of ["chapter"]) {
|
|
13614
|
+
scopeTriggers[kind].addEventListener("click", () => {
|
|
13615
|
+
setTaskScopeBubbleOpen(kind, scopeTriggers[kind].getAttribute("aria-expanded") !== "true");
|
|
13616
|
+
});
|
|
13617
|
+
scopeSearches[kind].addEventListener("input", () => filterTaskScopeOptions(kind));
|
|
13618
|
+
for (const input of scopeInputs[kind]) input.addEventListener("change", () => {
|
|
13619
|
+
syncTaskScopePicker(kind);
|
|
13620
|
+
clearContextWarning();
|
|
13621
|
+
invalidateRelationshipSourcePreview();
|
|
13622
|
+
});
|
|
13623
|
+
scopeClearButtons[kind].addEventListener("click", () => {
|
|
13624
|
+
for (const input of scopeInputs[kind]) input.checked = false;
|
|
13625
|
+
syncTaskScopePicker(kind);
|
|
13626
|
+
clearContextWarning();
|
|
13627
|
+
invalidateRelationshipSourcePreview();
|
|
13628
|
+
});
|
|
13629
|
+
scopeSelectedLists[kind].addEventListener("click", (event) => {
|
|
13630
|
+
const removeButton = event.target.closest("[data-task-scope-remove]");
|
|
13631
|
+
if (!removeButton) return;
|
|
13632
|
+
const input = scopeInputs[kind].find((candidate) => candidate.value === removeButton.dataset.taskScopeRemoveId);
|
|
13633
|
+
if (!input) return;
|
|
13634
|
+
input.checked = false;
|
|
13635
|
+
syncTaskScopePicker(kind);
|
|
13636
|
+
clearContextWarning();
|
|
13637
|
+
invalidateRelationshipSourcePreview();
|
|
13638
|
+
});
|
|
13639
|
+
}
|
|
13640
|
+
for (const volumeInput of scopeVolumeInputs) volumeInput.addEventListener("change", () => {
|
|
13641
|
+
const group = volumeInput.closest("[data-task-scope-group]");
|
|
13642
|
+
for (const chapterInput of group.querySelectorAll('[data-task-scope-input="chapter"]')) chapterInput.checked = volumeInput.checked;
|
|
13643
|
+
syncTaskScopePicker("chapter");
|
|
13644
|
+
clearContextWarning();
|
|
13645
|
+
invalidateRelationshipSourcePreview();
|
|
13646
|
+
});
|
|
13390
13647
|
$("#dynamic-form").onclick = (event) => {
|
|
13391
13648
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
13649
|
+
for (const kind of ["chapter"]) {
|
|
13650
|
+
if (!scopeFields[kind].contains(event.target)) setTaskScopeBubbleOpen(kind, false);
|
|
13651
|
+
}
|
|
13392
13652
|
};
|
|
13393
13653
|
$("#dynamic-form").onkeydown = (event) => {
|
|
13394
|
-
if (event.key !== "Escape"
|
|
13654
|
+
if (event.key !== "Escape") return;
|
|
13655
|
+
const openScope = ["chapter"].find((kind) => !scopeBubbles[kind].classList.contains("hidden"));
|
|
13656
|
+
if (openScope) {
|
|
13657
|
+
event.preventDefault();
|
|
13658
|
+
setTaskScopeBubbleOpen(openScope, false);
|
|
13659
|
+
scopeTriggers[openScope].focus();
|
|
13660
|
+
return;
|
|
13661
|
+
}
|
|
13662
|
+
if (relationshipCharacterBubble.classList.contains("hidden")) return;
|
|
13395
13663
|
event.preventDefault();
|
|
13396
13664
|
setRelationshipCharacterBubbleOpen(false);
|
|
13397
13665
|
relationshipCharacterTrigger.focus();
|
|
@@ -13400,10 +13668,23 @@ async function openTaskDialog() {
|
|
|
13400
13668
|
invalidateRelationshipSourcePreview();
|
|
13401
13669
|
syncChapterField();
|
|
13402
13670
|
});
|
|
13403
|
-
chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
|
|
13404
13671
|
syncRelationshipOptions();
|
|
13405
13672
|
}
|
|
13406
13673
|
|
|
13674
|
+
async function refreshAnalysisTaskViewsAfterCreate(workId) {
|
|
13675
|
+
try {
|
|
13676
|
+
await Promise.all([
|
|
13677
|
+
refreshBackgroundTaskCenter({ announce: false }),
|
|
13678
|
+
state.module === "tasks" && state.work?.id === workId
|
|
13679
|
+
? renderTasks(1, { refresh: true })
|
|
13680
|
+
: Promise.resolve()
|
|
13681
|
+
]);
|
|
13682
|
+
} catch (error) {
|
|
13683
|
+
console.error("Failed to refresh analysis task views after creation", error);
|
|
13684
|
+
toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
|
|
13685
|
+
}
|
|
13686
|
+
}
|
|
13687
|
+
|
|
13407
13688
|
function openProviderDialog(item) {
|
|
13408
13689
|
const protocol = item?.protocol ?? "openai-chat-completions";
|
|
13409
13690
|
const providerProtocolOptions = [
|
|
@@ -13765,6 +14046,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13765
14046
|
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
|
|
13766
14047
|
const content = message.querySelector(".message-body");
|
|
13767
14048
|
const meta = message.querySelector(".message-meta");
|
|
14049
|
+
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
13768
14050
|
let messageMounted = false;
|
|
13769
14051
|
const mountAssistantMessage = () => {
|
|
13770
14052
|
if (messageMounted) return true;
|
|
@@ -13776,6 +14058,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13776
14058
|
return true;
|
|
13777
14059
|
};
|
|
13778
14060
|
const typewriter = createStreamTypewriter({
|
|
14061
|
+
speedController: streamSpeedController,
|
|
13779
14062
|
onRender: (text, progress) => {
|
|
13780
14063
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
|
|
13781
14064
|
content.innerHTML = renderMarkdown(text);
|
|
@@ -13811,6 +14094,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13811
14094
|
if (existing) return existing;
|
|
13812
14095
|
processStepVisibleContents.set(step, "");
|
|
13813
14096
|
const typewriter = createStreamTypewriter({
|
|
14097
|
+
speedController: streamSpeedController,
|
|
13814
14098
|
onRender: (text) => {
|
|
13815
14099
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
13816
14100
|
processStepVisibleContents.set(step, text);
|
|
@@ -15464,6 +15748,10 @@ $("#work-recycle-bin-close").addEventListener("click", () => $("#work-recycle-bi
|
|
|
15464
15748
|
$("#chapter-recycle-bin-close").addEventListener("click", () => $("#chapter-recycle-bin-dialog").close());
|
|
15465
15749
|
$("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
|
|
15466
15750
|
$("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
|
|
15751
|
+
document.querySelectorAll("[data-ai-tool-call-copy]").forEach((button) => {
|
|
15752
|
+
setAiToolCallCopyButtonState(button, false);
|
|
15753
|
+
button.addEventListener("click", () => void copyAiToolCallCode(button));
|
|
15754
|
+
});
|
|
15467
15755
|
$("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
|
|
15468
15756
|
$("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
|
|
15469
15757
|
$("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
|
|
@@ -15605,6 +15893,7 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
15605
15893
|
});
|
|
15606
15894
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
15607
15895
|
$("#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]?.());
|
|
15896
|
+
bindPlainTextPaste($("#ai-prompt"));
|
|
15608
15897
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
15609
15898
|
updateAiMentionMenu();
|
|
15610
15899
|
setAiContextMeter(null);
|
|
@@ -16179,6 +16468,9 @@ document.addEventListener("click", (event) => {
|
|
|
16179
16468
|
window.addEventListener("pagehide", dismissDeleteToasts);
|
|
16180
16469
|
window.addEventListener("beforeunload", (event) => {
|
|
16181
16470
|
if (hasUnsavedEditorChanges()) event.preventDefault();
|
|
16471
|
+
if (aiRequestManager.hasActive()) {
|
|
16472
|
+
toast("当前 turn 尚未结束,刷新会中断生成;已收到的内容会保留在历史记录中", "warning");
|
|
16473
|
+
}
|
|
16182
16474
|
});
|
|
16183
16475
|
window.addEventListener("online", () => {
|
|
16184
16476
|
updateSystemHealth({ status: "checking" });
|
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=20260815-ai-history-favorite-v1">
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260815-ai-history-favorite-v3&feature=ai-tool-call-copy-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -1099,8 +1099,8 @@
|
|
|
1099
1099
|
<div><dt>函数信息</dt><dd id="ai-tool-call-description"></dd></div>
|
|
1100
1100
|
<div><dt>返回字符数</dt><dd id="ai-tool-call-result-length"></dd></div>
|
|
1101
1101
|
</dl>
|
|
1102
|
-
<section><h3>参数</h3><pre id="ai-tool-call-arguments"></pre></section>
|
|
1103
|
-
<section><h3>返回值</h3><pre id="ai-tool-call-result"></pre></section>
|
|
1102
|
+
<section class="ai-tool-call-code-section"><h3>参数</h3><div class="ai-tool-call-code-block"><button class="ai-tool-call-copy-button" type="button" data-ai-tool-call-copy data-copy-target="ai-tool-call-arguments" data-copy-label="参数" aria-label="复制参数" title="复制参数"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="8" y="8" width="12" height="12" rx="2"></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path></svg></button><pre id="ai-tool-call-arguments"></pre></div></section>
|
|
1103
|
+
<section class="ai-tool-call-code-section"><h3>返回值</h3><div class="ai-tool-call-code-block"><button class="ai-tool-call-copy-button" type="button" data-ai-tool-call-copy data-copy-target="ai-tool-call-result" data-copy-label="返回值" aria-label="复制返回值" title="复制返回值"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="8" y="8" width="12" height="12" rx="2"></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path></svg></button><pre id="ai-tool-call-result"></pre></div></section>
|
|
1104
1104
|
</div>
|
|
1105
1105
|
</dialog>
|
|
1106
1106
|
|
|
@@ -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-ai-
|
|
1167
|
+
<script type="module" src="/app.js?v=20260815-ai-stream-persistence-v4&feature=ai-tool-call-copy-v1"></script>
|
|
1168
1168
|
</body>
|
|
1169
1169
|
</html>
|