@musnows/scriverse 0.7.12 → 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 +86 -35
- package/dist/ai.js.map +1 -1
- package/dist/public/app.js +206 -80
- package/dist/public/index.html +4 -4
- package/dist/public/stream-typewriter.d.ts +12 -1
- package/dist/public/stream-typewriter.js +65 -5
- package/dist/public/styles.css +73 -17
- package/dist/store.js +187 -31
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -19,7 +19,7 @@ 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";
|
|
@@ -551,7 +551,15 @@ function cancelActiveAiRequest(reason) {
|
|
|
551
551
|
return cancelled;
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
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
|
+
}
|
|
555
563
|
aiConversationNavigationGeneration += 1;
|
|
556
564
|
aiConversationNavigationPending = aiConversationNavigationGeneration;
|
|
557
565
|
const navigation = Object.freeze({
|
|
@@ -574,7 +582,10 @@ function finishAiConversationNavigation(navigation) {
|
|
|
574
582
|
return true;
|
|
575
583
|
}
|
|
576
584
|
|
|
577
|
-
function invalidateAiConversationNavigation(reason) {
|
|
585
|
+
function invalidateAiConversationNavigation(reason, action = "切换作品") {
|
|
586
|
+
if (aiRequestManager.hasActive()) {
|
|
587
|
+
toast(`当前 turn 尚未结束,${action}会中断生成;已收到的内容会保留在历史记录中`, "warning");
|
|
588
|
+
}
|
|
578
589
|
aiConversationNavigationGeneration += 1;
|
|
579
590
|
aiConversationNavigationPending = null;
|
|
580
591
|
cancelActiveAiRequest(reason);
|
|
@@ -1871,7 +1882,7 @@ function renderMessageCardActions(message) {
|
|
|
1871
1882
|
});
|
|
1872
1883
|
actions.append(copy);
|
|
1873
1884
|
}
|
|
1874
|
-
if (message.dataset.messageId) {
|
|
1885
|
+
if (message.dataset.messageId && message.classList.contains("assistant-message")) {
|
|
1875
1886
|
const fork = document.createElement("button");
|
|
1876
1887
|
fork.type = "button";
|
|
1877
1888
|
fork.className = "message-fork-button";
|
|
@@ -2001,6 +2012,29 @@ function formatAiToolCallTime(value) {
|
|
|
2001
2012
|
}).format(date);
|
|
2002
2013
|
}
|
|
2003
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
|
+
|
|
2004
2038
|
function openAiToolCallDetail(toolCall) {
|
|
2005
2039
|
const name = String(toolCall?.name ?? "unknown");
|
|
2006
2040
|
const status = toolCall?.status === "failed" ? "调用失败" : "调用成功";
|
|
@@ -2017,6 +2051,7 @@ function openAiToolCallDetail(toolCall) {
|
|
|
2017
2051
|
$("#ai-tool-call-arguments").textContent = JSON.stringify(toolCall?.arguments ?? {}, null, 2);
|
|
2018
2052
|
$("#ai-tool-call-result-length").textContent = `${resultDetails.characterCount.toLocaleString("zh-CN")} 字符`;
|
|
2019
2053
|
$("#ai-tool-call-result").textContent = resultDetails.text;
|
|
2054
|
+
document.querySelectorAll("[data-ai-tool-call-copy]").forEach((button) => setAiToolCallCopyButtonState(button, false));
|
|
2020
2055
|
$("#ai-tool-call-dialog").showModal();
|
|
2021
2056
|
}
|
|
2022
2057
|
|
|
@@ -2418,7 +2453,7 @@ async function ensureAiConversationsLoaded() {
|
|
|
2418
2453
|
|
|
2419
2454
|
async function openAiConversation(conversationId, hideHistory = true, focusMessageId = null) {
|
|
2420
2455
|
if (!state.work) return null;
|
|
2421
|
-
const navigation = beginAiConversationNavigation("已切换 AI 对话");
|
|
2456
|
+
const navigation = beginAiConversationNavigation("已切换 AI 对话", "切换会话");
|
|
2422
2457
|
try {
|
|
2423
2458
|
const parameters = new URLSearchParams({ page: "1", limit: "100" });
|
|
2424
2459
|
if (focusMessageId) parameters.set("messageId", String(focusMessageId));
|
|
@@ -2488,7 +2523,7 @@ function applyNewAiConversation(conversation) {
|
|
|
2488
2523
|
|
|
2489
2524
|
async function createNewAiConversation(taskType = "chat") {
|
|
2490
2525
|
if (!state.work) return null;
|
|
2491
|
-
const navigation = beginAiConversationNavigation("已新建 AI 对话");
|
|
2526
|
+
const navigation = beginAiConversationNavigation("已新建 AI 对话", "新建会话");
|
|
2492
2527
|
try {
|
|
2493
2528
|
const conversation = await api(`/api/works/${navigation.workId}/ai-conversations`, { method: "POST", body: { taskType } });
|
|
2494
2529
|
if (!isAiConversationNavigationCurrent(navigation)) return null;
|
|
@@ -3898,11 +3933,11 @@ function dismissChapterInsightToast() {
|
|
|
3898
3933
|
}
|
|
3899
3934
|
}
|
|
3900
3935
|
|
|
3901
|
-
function toast(message, type = "info") {
|
|
3936
|
+
function toast(message, type = "info", extraClass = "") {
|
|
3902
3937
|
if (systemRestartDetected || (!state.user && document.documentElement.classList.contains("login-route"))) return;
|
|
3903
3938
|
const region = $("#toast-region");
|
|
3904
3939
|
const element = document.createElement("div");
|
|
3905
|
-
element.className = `toast ${type}`;
|
|
3940
|
+
element.className = `toast ${type}${extraClass ? ` ${extraClass}` : ""}`;
|
|
3906
3941
|
element.setAttribute("role", type === "error" ? "alert" : "status");
|
|
3907
3942
|
element.setAttribute("aria-atomic", "true");
|
|
3908
3943
|
element.textContent = message;
|
|
@@ -9320,7 +9355,8 @@ function renderCharacterExtractionEditor(preview) {
|
|
|
9320
9355
|
if (!items.length) return '<p class="character-extraction-empty">没有可应用的角色候选。</p>';
|
|
9321
9356
|
return `<div class="character-extraction-editor" data-character-extraction-editor data-preview-token="${esc(preview.previewToken)}">
|
|
9322
9357
|
<div class="character-extraction-policy" role="note"><strong>合并规则</strong><span>优先使用任务保存的稳定角色引用,其次使用当前标准名和别名匹配。合并只追加无冲突别名、空缺身份、种族和首次登场;已有非空字段保持不变。</span></div>
|
|
9323
|
-
<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>
|
|
9324
9360
|
<div class="character-extraction-candidate-list">${items.map((item, index) => {
|
|
9325
9361
|
const matches = Array.isArray(item.matchCandidates) ? item.matchCandidates : [];
|
|
9326
9362
|
const suggestedAction = item.suggestedAction === "merge" && matches.length ? "merge" : item.suggestedAction === "skip" ? "skip" : "create";
|
|
@@ -9351,7 +9387,6 @@ function renderCharacterExtractionEditor(preview) {
|
|
|
9351
9387
|
</article>`;
|
|
9352
9388
|
}).join("")}</div>
|
|
9353
9389
|
<p class="character-extraction-apply-error hidden" data-character-extraction-error role="alert"></p>
|
|
9354
|
-
<div class="character-extraction-apply-actions"><button class="primary-button" type="button" data-apply-character-extraction>确认并应用所选候选</button><small>确认采用整批事务;任一新建项名称冲突时全部回滚。重复点击或网络重试不会重复创建。</small></div>
|
|
9355
9390
|
</div>`;
|
|
9356
9391
|
}
|
|
9357
9392
|
|
|
@@ -13087,11 +13122,12 @@ function openReviewDialog() {
|
|
|
13087
13122
|
}
|
|
13088
13123
|
|
|
13089
13124
|
async function openTaskDialog() {
|
|
13090
|
-
const volumeOptions = state.work.volumes.map((volume) => ({ id: String(volume.id), title: String(volume.title) }));
|
|
13091
13125
|
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
|
|
13092
13126
|
id: String(chapter.id),
|
|
13093
13127
|
title: String(chapter.title),
|
|
13094
|
-
|
|
13128
|
+
volumeId: String(volume.id),
|
|
13129
|
+
volumeTitle: String(volume.title),
|
|
13130
|
+
chapterType: String(chapter.chapterType || "正文")
|
|
13095
13131
|
})));
|
|
13096
13132
|
let relationshipCharacters = [];
|
|
13097
13133
|
let taskModels = [];
|
|
@@ -13108,23 +13144,67 @@ async function openTaskDialog() {
|
|
|
13108
13144
|
toast(`分析任务配置加载失败:${error.message}`, "error");
|
|
13109
13145
|
return;
|
|
13110
13146
|
}
|
|
13111
|
-
const taskScopePicker = (
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
<
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
|
|
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>
|
|
13125
13205
|
</div>
|
|
13126
|
-
</div
|
|
13127
|
-
|
|
13206
|
+
</div>`;
|
|
13207
|
+
};
|
|
13128
13208
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
13129
13209
|
const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
|
|
13130
13210
|
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
@@ -13152,8 +13232,7 @@ async function openTaskDialog() {
|
|
|
13152
13232
|
<option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
|
|
13153
13233
|
${availableTaskModels.map((model) => `<option value="${esc(model.id)}" ${model.id === defaultModelId ? "selected" : ""}>${esc(modelOptionLabel(model))}</option>`).join("")}
|
|
13154
13234
|
</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(
|
|
13156
|
-
const volumeField = taskScopePicker("volume", "分卷", volumeOptions, "选择需要分析的分卷");
|
|
13235
|
+
const chapterField = taskScopePicker(chapterOptions, "选择需要分析的章节");
|
|
13157
13236
|
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
13158
13237
|
${relationshipCharacterPicker}
|
|
13159
13238
|
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
|
|
@@ -13191,42 +13270,33 @@ async function openTaskDialog() {
|
|
|
13191
13270
|
const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
|
|
13192
13271
|
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
13193
13272
|
const chapterIds = form.getAll("chapterIds").map(String).filter(Boolean);
|
|
13194
|
-
const volumeIds = form.getAll("volumeIds").map(String).filter(Boolean);
|
|
13195
13273
|
const chapterScope = {
|
|
13196
13274
|
type: "chapter",
|
|
13197
13275
|
...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
|
|
13198
13276
|
...(chapterIds.length ? { chapterIds } : {})
|
|
13199
13277
|
};
|
|
13200
|
-
const volumeScope = {
|
|
13201
|
-
type: "volume",
|
|
13202
|
-
...(volumeIds[0] ? { volumeId: volumeIds[0] } : {}),
|
|
13203
|
-
...(volumeIds.length ? { volumeIds } : {})
|
|
13204
|
-
};
|
|
13205
13278
|
const scope = settingsOnly
|
|
13206
13279
|
? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13207
13280
|
: scopeType === "book" || includeAllSettings
|
|
13208
13281
|
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
13209
|
-
: { ...
|
|
13282
|
+
: { ...chapterScope, ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
13210
13283
|
return scope;
|
|
13211
13284
|
};
|
|
13212
13285
|
const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
|
|
13213
13286
|
type: scope.type,
|
|
13214
13287
|
chapterId: scope.chapterId ?? null,
|
|
13215
13288
|
chapterIds: scope.chapterIds ?? [],
|
|
13216
|
-
volumeId: scope.volumeId ?? null,
|
|
13217
|
-
volumeIds: scope.volumeIds ?? [],
|
|
13218
13289
|
includeAllSettings: scope.includeAllSettings === true,
|
|
13219
13290
|
characterIds: scope.characterIds ?? [],
|
|
13220
13291
|
preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
|
|
13221
13292
|
modelId
|
|
13222
13293
|
});
|
|
13223
|
-
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["
|
|
13294
|
+
openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
13224
13295
|
const workId = state.work.id;
|
|
13225
13296
|
const taskType = String(form.get("taskType"));
|
|
13226
13297
|
const modelId = String(form.get("modelId"));
|
|
13227
13298
|
const scope = buildRelationshipScope(form);
|
|
13228
13299
|
if (scope.type === "chapter" && !scope.chapterIds?.length) return toast("请先选择至少一个章节", "error");
|
|
13229
|
-
if (scope.type === "volume" && !scope.volumeIds?.length) return toast("请先选择至少一个分卷", "error");
|
|
13230
13300
|
if (taskType === "relationship-analysis" && relationshipSourcePreview
|
|
13231
13301
|
&& relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
|
|
13232
13302
|
scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
|
|
@@ -13250,6 +13320,7 @@ async function openTaskDialog() {
|
|
|
13250
13320
|
toast("分析任务已创建,已进入任务队列");
|
|
13251
13321
|
void refreshAnalysisTaskViewsAfterCreate(workId);
|
|
13252
13322
|
}, "AI 分析", {
|
|
13323
|
+
large: true,
|
|
13253
13324
|
submitLabel: "创建任务",
|
|
13254
13325
|
pendingLabel: "创建中…",
|
|
13255
13326
|
pendingMessage: "正在创建分析任务,请稍候",
|
|
@@ -13259,40 +13330,43 @@ async function openTaskDialog() {
|
|
|
13259
13330
|
const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
|
|
13260
13331
|
const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
|
|
13261
13332
|
const scopeFields = {
|
|
13262
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]')
|
|
13263
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-field="volume"]')
|
|
13333
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]')
|
|
13264
13334
|
};
|
|
13265
13335
|
const scopeTriggers = {
|
|
13266
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]')
|
|
13267
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-trigger="volume"]')
|
|
13336
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]')
|
|
13268
13337
|
};
|
|
13269
13338
|
const scopeBubbles = {
|
|
13270
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]')
|
|
13271
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-bubble="volume"]')
|
|
13339
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]')
|
|
13272
13340
|
};
|
|
13273
13341
|
const scopeSearches = {
|
|
13274
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]')
|
|
13275
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-search="volume"]')
|
|
13342
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]')
|
|
13276
13343
|
};
|
|
13277
13344
|
const scopeInputs = {
|
|
13278
|
-
chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')]
|
|
13279
|
-
volume: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="volume"]')]
|
|
13345
|
+
chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')]
|
|
13280
13346
|
};
|
|
13347
|
+
const scopeVolumeInputs = [
|
|
13348
|
+
...$("#dialog-fields").querySelectorAll("[data-task-scope-volume-input]")
|
|
13349
|
+
];
|
|
13281
13350
|
const scopeSummaries = {
|
|
13282
|
-
chapter: $("#dialog-fields").querySelector("#task-chapter-summary")
|
|
13283
|
-
volume: $("#dialog-fields").querySelector("#task-volume-summary")
|
|
13351
|
+
chapter: $("#dialog-fields").querySelector("#task-chapter-summary")
|
|
13284
13352
|
};
|
|
13285
13353
|
const scopeCounts = {
|
|
13286
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]')
|
|
13287
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-count="volume"]')
|
|
13354
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]')
|
|
13288
13355
|
};
|
|
13289
13356
|
const scopeClearButtons = {
|
|
13290
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]')
|
|
13291
|
-
volume: $("#dialog-fields").querySelector('[data-task-scope-clear="volume"]')
|
|
13357
|
+
chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]')
|
|
13292
13358
|
};
|
|
13293
13359
|
const scopeEmptyMessages = {
|
|
13294
|
-
chapter: $("#dialog-fields").querySelector('[data-task-scope-empty="chapter"]')
|
|
13295
|
-
|
|
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"]')
|
|
13296
13370
|
};
|
|
13297
13371
|
const description = $("#analysis-type-description");
|
|
13298
13372
|
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
@@ -13325,18 +13399,38 @@ async function openTaskDialog() {
|
|
|
13325
13399
|
const setTaskScopeBubbleOpen = (kind, open) => {
|
|
13326
13400
|
scopeBubbles[kind].classList.toggle("hidden", !open);
|
|
13327
13401
|
scopeTriggers[kind].setAttribute("aria-expanded", String(open));
|
|
13328
|
-
|
|
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
|
+
}
|
|
13329
13416
|
};
|
|
13330
13417
|
const syncTaskScopePicker = (kind) => {
|
|
13331
13418
|
const selected = scopeInputs[kind].filter((input) => input.checked);
|
|
13332
|
-
|
|
13333
|
-
const
|
|
13419
|
+
syncTaskScopeVolumeInputs();
|
|
13420
|
+
const selectedNames = selected.map((input) => input.dataset.taskScopeTitle ?? "");
|
|
13334
13421
|
scopeSummaries[kind].textContent = selectedNames.length
|
|
13335
|
-
?
|
|
13336
|
-
:
|
|
13422
|
+
? selectedNames.length === 1 ? selectedNames[0] : `已选择 ${selectedNames.length} 个章节`
|
|
13423
|
+
: "选择需要分析的章节";
|
|
13337
13424
|
scopeCounts[kind].textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
13425
|
+
scopeSummaryCounts[kind].textContent = `${selected.length} 章`;
|
|
13338
13426
|
scopeClearButtons[kind].disabled = selected.length === 0;
|
|
13339
|
-
scopeTriggers[kind].setAttribute("aria-label",
|
|
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>`;
|
|
13340
13434
|
};
|
|
13341
13435
|
const filterTaskScopeOptions = (kind) => {
|
|
13342
13436
|
const query = scopeSearches[kind].value.trim().toLocaleLowerCase();
|
|
@@ -13347,21 +13441,26 @@ async function openTaskDialog() {
|
|
|
13347
13441
|
option.classList.toggle("hidden", !visible);
|
|
13348
13442
|
if (visible) visibleCount += 1;
|
|
13349
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} 章`;
|
|
13350
13450
|
scopeEmptyMessages[kind].classList.toggle("hidden", visibleCount > 0);
|
|
13351
13451
|
};
|
|
13352
13452
|
const syncChapterField = () => {
|
|
13353
|
-
const
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
}
|
|
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);
|
|
13365
13464
|
};
|
|
13366
13465
|
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
13367
13466
|
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
@@ -13511,7 +13610,7 @@ async function openTaskDialog() {
|
|
|
13511
13610
|
syncRelationshipOptions();
|
|
13512
13611
|
}
|
|
13513
13612
|
});
|
|
13514
|
-
for (const kind of ["chapter"
|
|
13613
|
+
for (const kind of ["chapter"]) {
|
|
13515
13614
|
scopeTriggers[kind].addEventListener("click", () => {
|
|
13516
13615
|
setTaskScopeBubbleOpen(kind, scopeTriggers[kind].getAttribute("aria-expanded") !== "true");
|
|
13517
13616
|
});
|
|
@@ -13527,16 +13626,33 @@ async function openTaskDialog() {
|
|
|
13527
13626
|
clearContextWarning();
|
|
13528
13627
|
invalidateRelationshipSourcePreview();
|
|
13529
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
|
+
});
|
|
13530
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
|
+
});
|
|
13531
13647
|
$("#dynamic-form").onclick = (event) => {
|
|
13532
13648
|
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
13533
|
-
for (const kind of ["chapter"
|
|
13649
|
+
for (const kind of ["chapter"]) {
|
|
13534
13650
|
if (!scopeFields[kind].contains(event.target)) setTaskScopeBubbleOpen(kind, false);
|
|
13535
13651
|
}
|
|
13536
13652
|
};
|
|
13537
13653
|
$("#dynamic-form").onkeydown = (event) => {
|
|
13538
13654
|
if (event.key !== "Escape") return;
|
|
13539
|
-
const openScope = ["chapter"
|
|
13655
|
+
const openScope = ["chapter"].find((kind) => !scopeBubbles[kind].classList.contains("hidden"));
|
|
13540
13656
|
if (openScope) {
|
|
13541
13657
|
event.preventDefault();
|
|
13542
13658
|
setTaskScopeBubbleOpen(openScope, false);
|
|
@@ -13930,6 +14046,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13930
14046
|
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
|
|
13931
14047
|
const content = message.querySelector(".message-body");
|
|
13932
14048
|
const meta = message.querySelector(".message-meta");
|
|
14049
|
+
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
13933
14050
|
let messageMounted = false;
|
|
13934
14051
|
const mountAssistantMessage = () => {
|
|
13935
14052
|
if (messageMounted) return true;
|
|
@@ -13941,6 +14058,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13941
14058
|
return true;
|
|
13942
14059
|
};
|
|
13943
14060
|
const typewriter = createStreamTypewriter({
|
|
14061
|
+
speedController: streamSpeedController,
|
|
13944
14062
|
onRender: (text, progress) => {
|
|
13945
14063
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
|
|
13946
14064
|
content.innerHTML = renderMarkdown(text);
|
|
@@ -13976,6 +14094,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13976
14094
|
if (existing) return existing;
|
|
13977
14095
|
processStepVisibleContents.set(step, "");
|
|
13978
14096
|
const typewriter = createStreamTypewriter({
|
|
14097
|
+
speedController: streamSpeedController,
|
|
13979
14098
|
onRender: (text) => {
|
|
13980
14099
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
13981
14100
|
processStepVisibleContents.set(step, text);
|
|
@@ -15629,6 +15748,10 @@ $("#work-recycle-bin-close").addEventListener("click", () => $("#work-recycle-bi
|
|
|
15629
15748
|
$("#chapter-recycle-bin-close").addEventListener("click", () => $("#chapter-recycle-bin-dialog").close());
|
|
15630
15749
|
$("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
|
|
15631
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
|
+
});
|
|
15632
15755
|
$("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
|
|
15633
15756
|
$("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
|
|
15634
15757
|
$("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
|
|
@@ -16345,6 +16468,9 @@ document.addEventListener("click", (event) => {
|
|
|
16345
16468
|
window.addEventListener("pagehide", dismissDeleteToasts);
|
|
16346
16469
|
window.addEventListener("beforeunload", (event) => {
|
|
16347
16470
|
if (hasUnsavedEditorChanges()) event.preventDefault();
|
|
16471
|
+
if (aiRequestManager.hasActive()) {
|
|
16472
|
+
toast("当前 turn 尚未结束,刷新会中断生成;已收到的内容会保留在历史记录中", "warning");
|
|
16473
|
+
}
|
|
16348
16474
|
});
|
|
16349
16475
|
window.addEventListener("online", () => {
|
|
16350
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-
|
|
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>
|
|
@@ -9,11 +9,22 @@ export type StreamTypewriter = {
|
|
|
9
9
|
reveal(): string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type StreamTypewriterSpeedController = {
|
|
13
|
+
observe(characterCount: number): void;
|
|
14
|
+
charactersPerSecond(): number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createStreamTypewriterSpeedController(options?: {
|
|
18
|
+
now?: () => number;
|
|
19
|
+
initialCharactersPerSecond?: number;
|
|
20
|
+
}): StreamTypewriterSpeedController;
|
|
21
|
+
|
|
22
|
+
export function streamTypewriterBatchSize(pendingCharacters: number, finishing?: boolean, charactersPerSecond?: number): number;
|
|
13
23
|
|
|
14
24
|
export function createStreamTypewriter<FrameHandle = number>(options: {
|
|
15
25
|
onRender: (text: string, progress: StreamTypewriterProgress) => void;
|
|
16
26
|
scheduleFrame?: (callback: () => void) => FrameHandle;
|
|
17
27
|
cancelFrame?: (handle: FrameHandle) => void;
|
|
18
28
|
reducedMotion?: boolean;
|
|
29
|
+
speedController?: StreamTypewriterSpeedController | null;
|
|
19
30
|
}): StreamTypewriter;
|