@musnows/scriverse 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +4 -1
- package/dist/ai.js.map +1 -1
- package/dist/public/ai-context-meter.js +45 -4
- package/dist/public/ai-render-scheduler.js +30 -0
- package/dist/public/app.js +108 -61
- package/dist/public/index.html +5 -3
- package/dist/public/markdown.js +22 -10
- package/dist/public/stream-markdown.js +107 -0
- package/dist/public/stream-typewriter.d.ts +1 -0
- package/dist/public/stream-typewriter.js +33 -19
- package/dist/public/styles.css +14 -15
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
export function formatAiContextCacheHitPercent(value) {
|
|
2
|
+
const rate = Number(value);
|
|
3
|
+
if (!Number.isFinite(rate)) return "—";
|
|
4
|
+
const clamped = Math.max(0, Math.min(100, Math.round((rate + Number.EPSILON) * 10) / 10));
|
|
5
|
+
const label = Number.isInteger(clamped) ? String(clamped) : clamped.toFixed(1);
|
|
6
|
+
return `${label}%`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatAiContextPercentSummary(usage) {
|
|
10
|
+
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
11
|
+
const contextPercent = formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow);
|
|
12
|
+
return `context ${contextPercent} · cache hit ${formatAiContextCacheHitPercent(usage?.cacheHitPercent)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatAiContextPopoverDescription(usage) {
|
|
16
|
+
if (!usage) return "选择可用模型后显示当前上下文用量";
|
|
17
|
+
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
18
|
+
const occupied = distribution.occupiedTokens.toLocaleString("zh-CN");
|
|
19
|
+
const contextWindow = distribution.contextWindow.toLocaleString("zh-CN");
|
|
20
|
+
return `已占用 ${occupied} / ${contextWindow} tok · ${formatAiContextPercentSummary(usage)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
export function formatAiContextUsageTooltip(usage) {
|
|
2
24
|
if (!usage) return "选择可用模型后显示当前上下文用量";
|
|
3
25
|
const inputTokens = Math.max(0, Math.round(Number(usage.inputTokens) || 0)).toLocaleString("zh-CN");
|
|
@@ -6,7 +28,23 @@ export function formatAiContextUsageTooltip(usage) {
|
|
|
6
28
|
const conversationTokens = Math.max(0, Math.round(Number(usage.conversationTokens) || 0)).toLocaleString("zh-CN");
|
|
7
29
|
const conversationBudget = Math.max(0, Math.round(Number(usage.conversationBudgetTokens) || 0)).toLocaleString("zh-CN");
|
|
8
30
|
const outputTokens = Math.max(0, Math.round(Number(usage.outputTokens) || 0)).toLocaleString("zh-CN");
|
|
9
|
-
return `总输入 ${inputTokens} / ${contextWindow} tok · 作品上下文 ${contextTokens} tok · 对话历史 ${conversationTokens} / ${conversationBudget} tok · 当前调用实际输出 ${outputTokens} tok`;
|
|
31
|
+
return `总输入 ${inputTokens} / ${contextWindow} tok · 作品上下文 ${contextTokens} tok · 对话历史 ${conversationTokens} / ${conversationBudget} tok · 当前调用实际输出 ${outputTokens} tok · ${formatAiContextPercentSummary(usage)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function attachAiContextCacheHitPercent(usage, cacheHitPercent) {
|
|
35
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return usage ?? null;
|
|
36
|
+
const rate = Number(cacheHitPercent);
|
|
37
|
+
if (!Number.isFinite(rate)) return usage;
|
|
38
|
+
return { ...usage, cacheHitPercent: Math.max(0, Math.min(100, rate)) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function withPreservedCacheHitPercent(previousUsage, nextUsage) {
|
|
42
|
+
if (!nextUsage || typeof nextUsage !== "object" || Array.isArray(nextUsage)) return nextUsage;
|
|
43
|
+
const nextCacheHit = Number(nextUsage.cacheHitPercent);
|
|
44
|
+
if (Number.isFinite(nextCacheHit)) return nextUsage;
|
|
45
|
+
const previousCacheHit = Number(previousUsage?.cacheHitPercent);
|
|
46
|
+
if (!Number.isFinite(previousCacheHit)) return nextUsage;
|
|
47
|
+
return { ...nextUsage, cacheHitPercent: previousCacheHit };
|
|
10
48
|
}
|
|
11
49
|
|
|
12
50
|
function tokenCount(value) {
|
|
@@ -14,7 +52,8 @@ function tokenCount(value) {
|
|
|
14
52
|
}
|
|
15
53
|
|
|
16
54
|
export function resolveAiContextUsage(previousUsage, nextUsage) {
|
|
17
|
-
|
|
55
|
+
if (!nextUsage || typeof nextUsage !== "object") return previousUsage ?? null;
|
|
56
|
+
return withPreservedCacheHitPercent(previousUsage, nextUsage);
|
|
18
57
|
}
|
|
19
58
|
|
|
20
59
|
const maximumAiContextUsageFields = [
|
|
@@ -28,7 +67,9 @@ const minimumAiContextUsageFields = ["remainingTokens"];
|
|
|
28
67
|
|
|
29
68
|
export function mergeAiContextUsage(previousUsage, nextUsage, allowShrink = false) {
|
|
30
69
|
if (!nextUsage || typeof nextUsage !== "object" || Array.isArray(nextUsage)) return previousUsage ?? null;
|
|
31
|
-
if (!previousUsage || typeof previousUsage !== "object" || Array.isArray(previousUsage) || allowShrink)
|
|
70
|
+
if (!previousUsage || typeof previousUsage !== "object" || Array.isArray(previousUsage) || allowShrink) {
|
|
71
|
+
return withPreservedCacheHitPercent(previousUsage, nextUsage);
|
|
72
|
+
}
|
|
32
73
|
const mergedUsage = { ...nextUsage };
|
|
33
74
|
for (const field of [...maximumAiContextUsageFields, ...minimumAiContextUsageFields]) {
|
|
34
75
|
const previousValue = Number(previousUsage[field]);
|
|
@@ -41,7 +82,7 @@ export function mergeAiContextUsage(previousUsage, nextUsage, allowShrink = fals
|
|
|
41
82
|
else if (Number.isFinite(previousValue)) mergedUsage[field] = previousValue;
|
|
42
83
|
else if (Number.isFinite(nextValue)) mergedUsage[field] = nextValue;
|
|
43
84
|
}
|
|
44
|
-
return mergedUsage;
|
|
85
|
+
return withPreservedCacheHitPercent(previousUsage, mergedUsage);
|
|
45
86
|
}
|
|
46
87
|
|
|
47
88
|
export function formatAiContextUsagePercent(occupiedTokens, contextWindow) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function createAiRenderScheduler({ isVisible, isConnected = () => true, schedule = requestAnimationFrame, now = () => performance.now(), intervalMs = 32, budgetMs = 4 }) {
|
|
2
|
+
const pending = new Map();
|
|
3
|
+
let frame = null;
|
|
4
|
+
let lastRendered = -Infinity;
|
|
5
|
+
const refresh = () => {
|
|
6
|
+
for (const target of pending.keys()) {
|
|
7
|
+
if (!isConnected(target)) pending.delete(target);
|
|
8
|
+
}
|
|
9
|
+
if (frame !== null || ![...pending.keys()].some(isVisible)) return;
|
|
10
|
+
frame = schedule(() => {
|
|
11
|
+
frame = null;
|
|
12
|
+
const started = now();
|
|
13
|
+
if (started - lastRendered >= intervalMs) {
|
|
14
|
+
for (const [target, render] of pending) {
|
|
15
|
+
if (!isConnected(target)) { pending.delete(target); continue; }
|
|
16
|
+
if (!isVisible(target)) continue;
|
|
17
|
+
pending.delete(target);
|
|
18
|
+
render();
|
|
19
|
+
lastRendered = started;
|
|
20
|
+
if (now() - started >= budgetMs) break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
refresh();
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
enqueue(target, render) { pending.set(target, render); refresh(); },
|
|
28
|
+
refresh
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/public/app.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
|
|
2
2
|
import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { countProseWords } from "/text-count.js?v=20260906-chapter-word-count-consistency-v1";
|
|
4
|
-
import { renderMarkdown } from "/markdown.js?v=
|
|
4
|
+
import { renderMarkdown } from "/markdown.js?v=20260912-stream-render-v2";
|
|
5
|
+
import { createStreamingMarkdownRenderer } from "/stream-markdown.js?v=20260912-stream-render-v2";
|
|
6
|
+
import { createAiRenderScheduler } from "/ai-render-scheduler.js?v=20260912-stream-render-v2";
|
|
5
7
|
import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
|
|
6
8
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
7
9
|
import { applyAiSkillCommand, findAiSkillCommand, listAiSkillOptions } from "/ai-skill-menu.js?v=20260830-ai-skill-slash-menu-v1";
|
|
@@ -32,11 +34,11 @@ import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_
|
|
|
32
34
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
33
35
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
34
36
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
35
|
-
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=
|
|
37
|
+
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260912-stream-render-v2";
|
|
36
38
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
37
39
|
import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount, usageCalendarYears } from "/ai-usage.js?v=20260830-ai-usage-year-v1";
|
|
38
40
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
|
|
39
|
-
import {
|
|
41
|
+
import { attachAiContextCacheHitPercent, formatAiContextPopoverDescription, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260911-context-cache-hit-v1";
|
|
40
42
|
import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
|
|
41
43
|
import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
|
|
42
44
|
import {
|
|
@@ -2688,6 +2690,7 @@ function activateAiChatTab(tabId, { persistCurrent = true, force = false } = {})
|
|
|
2688
2690
|
feed.id = feed === tab.feed ? "ai-feed" : `ai-chat-panel-${feed.dataset.aiTabId}`;
|
|
2689
2691
|
}
|
|
2690
2692
|
tab.feed.classList.remove("hidden");
|
|
2693
|
+
aiStreamRenders.refresh();
|
|
2691
2694
|
applyAiChatTabState(tab);
|
|
2692
2695
|
renderAiChatTabs();
|
|
2693
2696
|
return tab;
|
|
@@ -2703,6 +2706,7 @@ function closeAiChatTab(tabId) {
|
|
|
2703
2706
|
}
|
|
2704
2707
|
const { active } = aiChatTabManager.close(tab.id);
|
|
2705
2708
|
tab.feed.remove();
|
|
2709
|
+
aiStreamRenders.refresh();
|
|
2706
2710
|
if (!wasActive) {
|
|
2707
2711
|
renderAiChatTabs();
|
|
2708
2712
|
return;
|
|
@@ -3087,6 +3091,42 @@ const aiFeedScrollFrames = new WeakMap();
|
|
|
3087
3091
|
const aiFeedAutoScrollStates = new WeakMap();
|
|
3088
3092
|
const aiFeedScrollBindings = new WeakSet();
|
|
3089
3093
|
const aiProcessScrollFrames = new WeakMap();
|
|
3094
|
+
const aiProcessRenderStates = new WeakMap();
|
|
3095
|
+
const aiMarkdownRenderers = new WeakMap();
|
|
3096
|
+
function aiStreamTargetVisible(target) {
|
|
3097
|
+
target = target.element ?? target;
|
|
3098
|
+
const feed = target.closest(".ai-feed");
|
|
3099
|
+
const app = $("#app");
|
|
3100
|
+
return document.visibilityState !== "hidden" && Boolean(feed?.isConnected)
|
|
3101
|
+
&& !feed.classList.contains("hidden") && !app.classList.contains("shelf-mode")
|
|
3102
|
+
&& (!app.classList.contains("ai-panel-collapsed") || app.classList.contains("ai-workspace-mode"));
|
|
3103
|
+
}
|
|
3104
|
+
const aiStreamRenders = createAiRenderScheduler({
|
|
3105
|
+
isVisible: aiStreamTargetVisible,
|
|
3106
|
+
isConnected: (target) => (target.element ?? target).isConnected
|
|
3107
|
+
});
|
|
3108
|
+
document.addEventListener("visibilitychange", () => aiStreamRenders.refresh());
|
|
3109
|
+
// 只监听面板显隐,不观察正文或消息内容,避免渲染自身触发新的刷新。
|
|
3110
|
+
const aiPanelVisibilityObserver = new MutationObserver(() => aiStreamRenders.refresh());
|
|
3111
|
+
aiPanelVisibilityObserver.observe($("#app"), { attributes: true, attributeFilter: ["class"] });
|
|
3112
|
+
|
|
3113
|
+
function updateAiMarkdown(body, content) {
|
|
3114
|
+
let render = aiMarkdownRenderers.get(body);
|
|
3115
|
+
if (!render) {
|
|
3116
|
+
const target = { element: body };
|
|
3117
|
+
render = createStreamingMarkdownRenderer(body, {
|
|
3118
|
+
enqueue: (callback) => queueMicrotask(() => aiStreamRenders.enqueue(target, callback)),
|
|
3119
|
+
onRender: () => {
|
|
3120
|
+
const message = body.closest(".assistant-message");
|
|
3121
|
+
if (message?.classList.contains("is-streaming")) scrollAiProcessStepsToBottom(message);
|
|
3122
|
+
const feed = body.closest(".ai-feed");
|
|
3123
|
+
if (feed) scrollAiFeedToBottom(feed);
|
|
3124
|
+
}
|
|
3125
|
+
});
|
|
3126
|
+
aiMarkdownRenderers.set(body, render);
|
|
3127
|
+
}
|
|
3128
|
+
return render(content);
|
|
3129
|
+
}
|
|
3090
3130
|
const AI_FEED_BOTTOM_THRESHOLD_PX = 24;
|
|
3091
3131
|
let markdownTableMenuTarget = null;
|
|
3092
3132
|
let markdownTableMenuTrigger = null;
|
|
@@ -3153,9 +3193,8 @@ function scrollAiFeedToBottom(feed = $("#ai-feed"), { force = false } = {}) {
|
|
|
3153
3193
|
bindAiFeedAutoScroll(feed);
|
|
3154
3194
|
if (force) aiFeedAutoScrollStates.set(feed, true);
|
|
3155
3195
|
if (!aiFeedAutoScrollStates.get(feed)) return;
|
|
3156
|
-
feed.scrollTop = feed.scrollHeight;
|
|
3157
3196
|
const currentFrame = aiFeedScrollFrames.get(feed);
|
|
3158
|
-
if (currentFrame !== undefined
|
|
3197
|
+
if (currentFrame !== undefined || !aiStreamTargetVisible(feed)) return;
|
|
3159
3198
|
const nextFrame = window.requestAnimationFrame(() => {
|
|
3160
3199
|
feed.scrollTop = feed.scrollHeight;
|
|
3161
3200
|
aiFeedScrollFrames.delete(feed);
|
|
@@ -3169,9 +3208,8 @@ function scrollAiProcessStepsToBottom(message) {
|
|
|
3169
3208
|
body.scrollTop = body.scrollHeight;
|
|
3170
3209
|
});
|
|
3171
3210
|
};
|
|
3172
|
-
scroll();
|
|
3173
3211
|
const currentFrame = aiProcessScrollFrames.get(message);
|
|
3174
|
-
if (currentFrame !== undefined
|
|
3212
|
+
if (currentFrame !== undefined || !aiStreamTargetVisible(message)) return;
|
|
3175
3213
|
const nextFrame = window.requestAnimationFrame(() => {
|
|
3176
3214
|
scroll();
|
|
3177
3215
|
aiProcessScrollFrames.delete(message);
|
|
@@ -3724,74 +3762,80 @@ function shouldRenderAiProcessStep(step) {
|
|
|
3724
3762
|
}
|
|
3725
3763
|
|
|
3726
3764
|
function renderAiProcessSteps(message, steps, completed, durationMs = null, visibleContents = null) {
|
|
3727
|
-
const
|
|
3728
|
-
const previousScrollStates = new Map();
|
|
3729
|
-
previousDetails?.querySelectorAll(".ai-process-step[data-ai-process-step-id]").forEach((section) => {
|
|
3730
|
-
const body = section.querySelector(".ai-process-step-body");
|
|
3731
|
-
if (!body) return;
|
|
3732
|
-
previousScrollStates.set(section.dataset.aiProcessStepId, {
|
|
3733
|
-
scrollTop: body.scrollTop,
|
|
3734
|
-
nearBottom: body.scrollHeight - body.scrollTop - body.clientHeight < 24
|
|
3735
|
-
});
|
|
3736
|
-
});
|
|
3737
|
-
previousDetails?.remove();
|
|
3765
|
+
const cached = aiProcessRenderStates.get(message);
|
|
3738
3766
|
const renderableSteps = (Array.isArray(steps) ? steps : []).filter(shouldRenderAiProcessStep);
|
|
3739
|
-
if (!renderableSteps.length)
|
|
3740
|
-
|
|
3767
|
+
if (!renderableSteps.length) {
|
|
3768
|
+
cached?.details.remove();
|
|
3769
|
+
aiProcessRenderStates.delete(message);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const details = cached?.details ?? document.createElement("details");
|
|
3741
3773
|
details.className = "ai-process-details";
|
|
3742
3774
|
// 存在待确认/待回答的交互卡片时保持展开,避免审批入口在历史消息中被折叠。
|
|
3743
3775
|
details.open = !completed || steps.some((step) => step?.type === "tool" && isInteractiveToolPending(step.toolCall));
|
|
3744
|
-
const summary = document.createElement("summary");
|
|
3745
|
-
const title = document.createElement("span");
|
|
3776
|
+
const summary = cached?.summary ?? document.createElement("summary");
|
|
3777
|
+
const title = cached?.title ?? document.createElement("span");
|
|
3746
3778
|
title.textContent = completed ? "思考与执行过程" : "正在思考与执行";
|
|
3747
|
-
const status = document.createElement("small");
|
|
3779
|
+
const status = cached?.status ?? document.createElement("small");
|
|
3748
3780
|
const duration = durationMs === null || durationMs === undefined ? "" : formatAiProcessDuration(durationMs);
|
|
3749
3781
|
status.textContent = `${renderableSteps.length} 个步骤${duration ? ` · 耗时 ${duration}` : ""}`;
|
|
3750
|
-
summary.append(title, status);
|
|
3751
|
-
const list = document.createElement("div");
|
|
3782
|
+
if (!cached) summary.append(title, status);
|
|
3783
|
+
const list = cached?.list ?? document.createElement("div");
|
|
3752
3784
|
list.className = "ai-process-list";
|
|
3753
|
-
|
|
3785
|
+
const entries = new Map();
|
|
3786
|
+
const nextNodes = [];
|
|
3787
|
+
for (const [index, step] of renderableSteps.entries()) {
|
|
3788
|
+
const key = `${step.type}:${String(step.id ?? index)}`;
|
|
3789
|
+
const previous = cached?.entries.get(key);
|
|
3754
3790
|
if (step?.type === "context_compaction") {
|
|
3755
|
-
|
|
3791
|
+
const divider = previous?.node ?? createAiContextCompactionDivider({
|
|
3756
3792
|
kind: "tool",
|
|
3757
3793
|
ariaLabel: `第 ${Number(step.round) || 1} 轮已压缩上下文`,
|
|
3758
3794
|
title: `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`
|
|
3759
|
-
})
|
|
3795
|
+
});
|
|
3796
|
+
entries.set(key, { node: divider });
|
|
3797
|
+
nextNodes.push(divider);
|
|
3760
3798
|
continue;
|
|
3761
3799
|
}
|
|
3762
3800
|
if (step?.type === "tool" && step.toolCall) {
|
|
3801
|
+
if (previous?.value === step.toolCall) {
|
|
3802
|
+
entries.set(key, previous);
|
|
3803
|
+
nextNodes.push(previous.node);
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3763
3806
|
const tool = document.createElement("section");
|
|
3764
3807
|
tool.className = "ai-process-step ai-process-tool-step";
|
|
3765
3808
|
const label = document.createElement("small");
|
|
3766
3809
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · 工具调用`;
|
|
3767
3810
|
tool.append(label, createAiToolCallButton(step.toolCall));
|
|
3768
|
-
|
|
3811
|
+
entries.set(key, { node: tool, value: step.toolCall });
|
|
3812
|
+
nextNodes.push(tool);
|
|
3769
3813
|
continue;
|
|
3770
3814
|
}
|
|
3771
|
-
const section = document.createElement("section");
|
|
3815
|
+
const section = previous?.node ?? document.createElement("section");
|
|
3772
3816
|
section.className = `ai-process-step ai-process-${step.type}-step`;
|
|
3773
3817
|
section.dataset.aiProcessStepId = `${step.type}:${String(step.id ?? step.round ?? "")}`;
|
|
3774
|
-
const label = document.createElement("small");
|
|
3818
|
+
const label = previous?.label ?? document.createElement("small");
|
|
3775
3819
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · ${step.type === "thinking" ? "Thinking" : "中间输出"}`;
|
|
3776
|
-
const body = document.createElement("div");
|
|
3820
|
+
const body = previous?.body ?? document.createElement("div");
|
|
3777
3821
|
body.className = "message-body ai-process-step-body";
|
|
3778
3822
|
const content = visibleContents?.has(step) ? visibleContents.get(step) : step.content;
|
|
3779
|
-
body
|
|
3780
|
-
section.append(label, body);
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3823
|
+
updateAiMarkdown(body, content);
|
|
3824
|
+
if (!previous) section.append(label, body);
|
|
3825
|
+
entries.set(key, { node: section, label, body });
|
|
3826
|
+
nextNodes.push(section);
|
|
3827
|
+
}
|
|
3828
|
+
if (!cached) details.append(summary, list);
|
|
3829
|
+
nextNodes.forEach((node, index) => {
|
|
3830
|
+
if (list.children[index] !== node) list.insertBefore(node, list.children[index] ?? null);
|
|
3831
|
+
});
|
|
3832
|
+
while (list.children.length > nextNodes.length) list.lastElementChild.remove();
|
|
3833
|
+
if (!cached) {
|
|
3834
|
+
const body = message.querySelector(".message-body");
|
|
3835
|
+
if (body) body.before(details);
|
|
3836
|
+
else message.append(details);
|
|
3837
|
+
}
|
|
3838
|
+
aiProcessRenderStates.set(message, { details, summary, title, status, list, entries, completed });
|
|
3795
3839
|
if (!completed) scrollAiProcessStepsToBottom(message);
|
|
3796
3840
|
}
|
|
3797
3841
|
|
|
@@ -9108,6 +9152,7 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
|
|
|
9108
9152
|
menu.querySelector("strong").textContent = `操作“${chapter.title}”`;
|
|
9109
9153
|
menu.querySelectorAll("[data-chapter-type], [data-delete-chapter]").forEach((button) => button.classList.toggle("hidden", !canManageChapter));
|
|
9110
9154
|
menu.querySelector("[data-add-chapter-ai-reference]")?.classList.toggle("hidden", !canAddAiReference);
|
|
9155
|
+
menu.querySelector("#chapter-type-ai-reference-separator")?.classList.toggle("hidden", !(canManageChapter && canAddAiReference));
|
|
9111
9156
|
menu.querySelectorAll("[data-chapter-type]").forEach((button) => {
|
|
9112
9157
|
button.classList.toggle("active", button.dataset.chapterType === (chapter.chapterType || "正文"));
|
|
9113
9158
|
button.setAttribute("aria-checked", String(button.classList.contains("active")));
|
|
@@ -14633,10 +14678,7 @@ function renderAiContextDistribution(usage) {
|
|
|
14633
14678
|
const popover = $("#ai-context-popover");
|
|
14634
14679
|
const host = $("#ai-context-distribution");
|
|
14635
14680
|
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
14636
|
-
|
|
14637
|
-
$("#ai-context-popover-description").textContent = usage
|
|
14638
|
-
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok · ${formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow)}`
|
|
14639
|
-
: "选择可用模型后显示当前上下文用量";
|
|
14681
|
+
$("#ai-context-popover-description").textContent = formatAiContextPopoverDescription(usage);
|
|
14640
14682
|
host.replaceChildren(...distribution.items.map((item) => {
|
|
14641
14683
|
const row = document.createElement("div");
|
|
14642
14684
|
row.className = "ai-context-distribution-row";
|
|
@@ -14678,13 +14720,11 @@ function setAiContextMeter(usage, allowShrink = true) {
|
|
|
14678
14720
|
: mergeAiContextUsage(latestAiContextUsage, usage, false);
|
|
14679
14721
|
latestAiContextUsage = displayUsage;
|
|
14680
14722
|
const meter = $("#ai-context-meter");
|
|
14681
|
-
const value = meter.querySelector("b");
|
|
14682
14723
|
const distribution = renderAiContextDistribution(displayUsage);
|
|
14683
14724
|
if (!displayUsage) {
|
|
14684
14725
|
meter.classList.add("is-empty");
|
|
14685
14726
|
meter.classList.remove("is-warning", "is-danger");
|
|
14686
14727
|
meter.style.setProperty("--context-usage", "0");
|
|
14687
|
-
value.textContent = "—";
|
|
14688
14728
|
const tooltip = formatAiContextUsageTooltip(null);
|
|
14689
14729
|
meter.dataset.tooltip = tooltip;
|
|
14690
14730
|
meter.setAttribute("aria-label", tooltip);
|
|
@@ -14697,7 +14737,6 @@ function setAiContextMeter(usage, allowShrink = true) {
|
|
|
14697
14737
|
meter.classList.toggle("is-warning", percent >= 70 && percent < 90);
|
|
14698
14738
|
meter.classList.toggle("is-danger", percent >= 90);
|
|
14699
14739
|
meter.style.setProperty("--context-usage", String(percent));
|
|
14700
|
-
value.textContent = formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow);
|
|
14701
14740
|
const tooltip = formatAiContextUsageTooltip(displayUsage);
|
|
14702
14741
|
meter.dataset.tooltip = tooltip;
|
|
14703
14742
|
meter.setAttribute("aria-label", `当前上下文用量:${tooltip}`);
|
|
@@ -18494,11 +18533,15 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18494
18533
|
};
|
|
18495
18534
|
const typewriter = createStreamTypewriter({
|
|
18496
18535
|
speedController: streamSpeedController,
|
|
18536
|
+
shouldAnimate: () => aiStreamTargetVisible(feed),
|
|
18497
18537
|
onRender: (text, progress) => {
|
|
18498
18538
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
|
|
18499
|
-
content
|
|
18500
|
-
|
|
18501
|
-
|
|
18539
|
+
aiStreamRenders.enqueue(content, () => {
|
|
18540
|
+
if (!aiRequestTargetsCurrentState(requestHolder.snapshot) && message.classList.contains("is-streaming")) return;
|
|
18541
|
+
updateAiMarkdown(content, text);
|
|
18542
|
+
if (message.classList.contains("is-streaming")) renderAiStreamingCharacterProgress(meta, progress.visibleCharacters);
|
|
18543
|
+
scrollAiFeedToBottom(feed);
|
|
18544
|
+
});
|
|
18502
18545
|
}
|
|
18503
18546
|
});
|
|
18504
18547
|
let streamedText = "";
|
|
@@ -18522,7 +18565,10 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18522
18565
|
const processStepVisibleContents = new Map();
|
|
18523
18566
|
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
18524
18567
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
18525
|
-
|
|
18568
|
+
aiStreamRenders.enqueue(message, () => {
|
|
18569
|
+
renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
|
|
18570
|
+
scrollAiFeedToBottom(feed);
|
|
18571
|
+
});
|
|
18526
18572
|
};
|
|
18527
18573
|
const processStepTypewriter = (step) => {
|
|
18528
18574
|
const existing = processStepTypewriters.get(step);
|
|
@@ -18530,6 +18576,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18530
18576
|
processStepVisibleContents.set(step, "");
|
|
18531
18577
|
const typewriter = createStreamTypewriter({
|
|
18532
18578
|
speedController: streamSpeedController,
|
|
18579
|
+
shouldAnimate: () => aiStreamTargetVisible(feed),
|
|
18533
18580
|
onRender: (text) => {
|
|
18534
18581
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
18535
18582
|
processStepVisibleContents.set(step, text);
|
|
@@ -18692,7 +18739,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18692
18739
|
|| writingSuggestion?.processSteps?.some((step) => step?.toolCall?.status === "failed");
|
|
18693
18740
|
if (writingSuggestionFailed) setAiChatTabStatus(tab, "error");
|
|
18694
18741
|
const announcedCompaction = contextAction === "compacted" || streamContextCompacted;
|
|
18695
|
-
setAiChatTabContextUsage(tab, payload.contextUsage, announcedCompaction);
|
|
18742
|
+
setAiChatTabContextUsage(tab, attachAiContextCacheHitPercent(payload.contextUsage, payload.cacheHitPercent), announcedCompaction);
|
|
18696
18743
|
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
18697
18744
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18698
18745
|
message.classList.remove("is-streaming");
|
|
@@ -18715,7 +18762,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18715
18762
|
writingSuggestionId: writingSuggestion.id
|
|
18716
18763
|
} : {})
|
|
18717
18764
|
};
|
|
18718
|
-
|
|
18765
|
+
renderStreamingProcessSteps(true, processDurationMs);
|
|
18719
18766
|
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
|
|
18720
18767
|
attachAssistantCopyAction(message, streamedText);
|
|
18721
18768
|
scrollAiFeedToBottom(feed);
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=continuation-guard-failure-details-v1&feature=entity-editor-back-icon-v1&feature=toast-stack-v1">
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=continuation-guard-failure-details-v1&feature=entity-editor-back-icon-v1&feature=toast-stack-v1">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
15
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -440,7 +440,7 @@
|
|
|
440
440
|
<div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-controls="ai-mention-menu" aria-haspopup="listbox" aria-expanded="false" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
|
|
441
441
|
<div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定、章节或上下文能力"></div>
|
|
442
442
|
<div class="prompt-composer-actions">
|
|
443
|
-
<button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"
|
|
443
|
+
<button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"></button>
|
|
444
444
|
<section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
|
|
445
445
|
<header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
|
|
446
446
|
<div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
|
|
@@ -522,6 +522,7 @@
|
|
|
522
522
|
<button type="button" role="menuitemradio" data-chapter-type="设定">设定</button>
|
|
523
523
|
<button type="button" role="menuitemradio" data-chapter-type="作者的话">作者的话</button>
|
|
524
524
|
<button type="button" role="menuitemradio" data-chapter-type="其他">其他</button>
|
|
525
|
+
<div id="chapter-type-ai-reference-separator" class="line-citation-menu-separator hidden" role="separator" aria-orientation="horizontal"></div>
|
|
525
526
|
<button type="button" role="menuitem" data-add-chapter-ai-reference>添加到助手引用</button>
|
|
526
527
|
<button class="danger-button" type="button" role="menuitem" data-delete-chapter>删除章节</button>
|
|
527
528
|
</div>
|
|
@@ -1426,6 +1427,7 @@
|
|
|
1426
1427
|
</dialog>
|
|
1427
1428
|
|
|
1428
1429
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1429
|
-
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=continuation-guard-failure-details-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=ai-suggestion-editor-boundary-v1&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-
|
|
1430
|
+
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=continuation-guard-failure-details-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=ai-suggestion-editor-boundary-v1&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-v3&feature=ai-error-origin-v1&feature=ai-stream-render-performance-v2"></script>
|
|
1431
|
+
|
|
1430
1432
|
</body>
|
|
1431
1433
|
</html>
|
package/dist/public/markdown.js
CHANGED
|
@@ -142,8 +142,20 @@ function renderMarkdownTable(headers, alignments, rows) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export function renderMarkdown(value) {
|
|
145
|
+
return renderMarkdownParts(value).map((part) => part.html).join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function renderMarkdownParts(value) {
|
|
145
149
|
const lines = String(value ?? "").replace(/\r\n?/gu, "\n").split("\n");
|
|
146
150
|
const output = [];
|
|
151
|
+
const offsets = [];
|
|
152
|
+
let offset = 0;
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
offsets.push(offset);
|
|
155
|
+
offset += line.length + 1;
|
|
156
|
+
}
|
|
157
|
+
let lineIndex = 0;
|
|
158
|
+
const emit = (html, endLine = lineIndex) => output.push({ html, end: offsets[endLine] ?? offset - 1 });
|
|
147
159
|
let paragraph = [];
|
|
148
160
|
let list = null;
|
|
149
161
|
let quote = null;
|
|
@@ -151,12 +163,12 @@ export function renderMarkdown(value) {
|
|
|
151
163
|
|
|
152
164
|
const flushParagraph = () => {
|
|
153
165
|
if (!paragraph.length) return;
|
|
154
|
-
|
|
166
|
+
emit(`<p>${paragraph.map(renderInlineMarkdown).join("<br>")}</p>`);
|
|
155
167
|
paragraph = [];
|
|
156
168
|
};
|
|
157
169
|
const flushList = () => {
|
|
158
170
|
if (!list) return;
|
|
159
|
-
|
|
171
|
+
emit(`<${list.tag}>${list.items.map((item) => `<li class="markdown-depth-${item.depth}">${renderInlineMarkdown(item.text)}</li>`).join("")}</${list.tag}>`);
|
|
160
172
|
list = null;
|
|
161
173
|
};
|
|
162
174
|
const flushQuote = () => {
|
|
@@ -166,7 +178,7 @@ export function renderMarkdown(value) {
|
|
|
166
178
|
const html = content.split(/\n\s*\n/gu)
|
|
167
179
|
.map((part) => part.split("\n").map(renderInlineMarkdown).join("<br>"))
|
|
168
180
|
.join("<br><br>");
|
|
169
|
-
|
|
181
|
+
emit(`<blockquote>${html}</blockquote>`);
|
|
170
182
|
}
|
|
171
183
|
quote = null;
|
|
172
184
|
};
|
|
@@ -176,11 +188,11 @@ export function renderMarkdown(value) {
|
|
|
176
188
|
flushQuote();
|
|
177
189
|
};
|
|
178
190
|
|
|
179
|
-
for (
|
|
191
|
+
for (lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
180
192
|
const line = lines[lineIndex];
|
|
181
193
|
if (codeFence) {
|
|
182
194
|
if (/^\s*```/u.test(line)) {
|
|
183
|
-
|
|
195
|
+
emit(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`, lineIndex + 1);
|
|
184
196
|
codeFence = null;
|
|
185
197
|
} else {
|
|
186
198
|
codeFence.lines.push(line);
|
|
@@ -230,12 +242,12 @@ export function renderMarkdown(value) {
|
|
|
230
242
|
if (heading) {
|
|
231
243
|
flushBlocks();
|
|
232
244
|
const level = heading[1].length;
|
|
233
|
-
|
|
245
|
+
emit(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`, lineIndex + 1);
|
|
234
246
|
continue;
|
|
235
247
|
}
|
|
236
248
|
if (/^\s*(---+|___+|\*\*\*+)\s*$/u.test(line)) {
|
|
237
249
|
flushBlocks();
|
|
238
|
-
|
|
250
|
+
emit("<hr>", lineIndex + 1);
|
|
239
251
|
continue;
|
|
240
252
|
}
|
|
241
253
|
const tableHeaders = splitMarkdownTableRow(line);
|
|
@@ -251,7 +263,7 @@ export function renderMarkdown(value) {
|
|
|
251
263
|
lineIndex += 1;
|
|
252
264
|
}
|
|
253
265
|
lineIndex -= 1;
|
|
254
|
-
|
|
266
|
+
emit(renderMarkdownTable(tableHeaders, alignments, rows), lineIndex + 1);
|
|
255
267
|
continue;
|
|
256
268
|
}
|
|
257
269
|
const listItem = line.match(/^(\s*)([-+*]|\d+\.)\s+(.+)$/u);
|
|
@@ -266,7 +278,7 @@ export function renderMarkdown(value) {
|
|
|
266
278
|
if (list) flushList();
|
|
267
279
|
paragraph.push(line);
|
|
268
280
|
}
|
|
269
|
-
if (codeFence)
|
|
281
|
+
if (codeFence) emit(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`);
|
|
270
282
|
flushBlocks();
|
|
271
|
-
return output
|
|
283
|
+
return output;
|
|
272
284
|
}
|