@musnows/scriverse 1.0.7 → 1.0.9

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.
@@ -1,9 +1,11 @@
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=20260830-adjacent-blockquotes-v1";
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
- import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
8
+ import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } 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";
8
10
  import {
9
11
  emptyRoleplayScenePin,
@@ -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=20260906-background-stream-v2";
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 { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260828-context-output-usage-v1";
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 {
@@ -2324,6 +2326,148 @@ function aiReferenceKindLabel(reference) {
2324
2326
  return ({ character: "角色", setting: "设定", chapter: "章节", "context-settings": "能力" })[reference.kind] ?? "引用";
2325
2327
  }
2326
2328
 
2329
+ function escapeAiReferenceXmlText(value) {
2330
+ return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;");
2331
+ }
2332
+
2333
+ function escapeAiReferenceXmlAttribute(value) {
2334
+ return escapeAiReferenceXmlText(value).replaceAll('"', "&quot;");
2335
+ }
2336
+
2337
+ function unescapeAiReferenceXmlText(value) {
2338
+ return String(value ?? "").replaceAll("&quot;", '"').replaceAll("&lt;", "<").replaceAll("&amp;", "&");
2339
+ }
2340
+
2341
+ function serializeAiReference(reference) {
2342
+ return `<ai_reference kind="${escapeAiReferenceXmlAttribute(reference.kind)}" id="${escapeAiReferenceXmlAttribute(reference.id)}">${escapeAiReferenceXmlText(reference.name)}</ai_reference>`;
2343
+ }
2344
+
2345
+ function parseAiReferenceMarkup(value) {
2346
+ const references = [];
2347
+ const pattern = /<ai_reference kind="(character|setting|chapter|context-settings)" id="([^"]+)">([\s\S]*?)<\/ai_reference>/gu;
2348
+ let text = "";
2349
+ let cursor = 0;
2350
+ for (const match of String(value ?? "").matchAll(pattern)) {
2351
+ const index = match.index ?? 0;
2352
+ const reference = {
2353
+ kind: match[1],
2354
+ id: unescapeAiReferenceXmlText(match[2]),
2355
+ name: unescapeAiReferenceXmlText(match[3])
2356
+ };
2357
+ const marker = `\uE000ai-reference-${references.length}\uE001`;
2358
+ text += `${String(value).slice(cursor, index)}${marker}`;
2359
+ references.push({ ...reference, marker });
2360
+ cursor = index + match[0].length;
2361
+ }
2362
+ return { text: `${text}${String(value ?? "").slice(cursor)}`, references };
2363
+ }
2364
+
2365
+ function aiReferenceByKey(kind, referenceId) {
2366
+ return state.aiReferences.find((reference) => reference.kind === kind && String(reference.id) === String(referenceId)) ?? null;
2367
+ }
2368
+
2369
+ function aiPromptMarkupFromNode(node, root = node) {
2370
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? "";
2371
+ if (!(node instanceof Element)) return "";
2372
+ if (node.matches("[data-ai-reference-key]")) {
2373
+ const [kind, ...idParts] = String(node.dataset.aiReferenceKey ?? "").split(":");
2374
+ const reference = aiReferenceByKey(kind, idParts.join(":"));
2375
+ return reference ? serializeAiReference(reference) : "";
2376
+ }
2377
+ if (node.tagName === "BR") return "\n";
2378
+ const text = [...node.childNodes].map((child) => aiPromptMarkupFromNode(child, root)).join("");
2379
+ return node !== root && ["DIV", "P"].includes(node.tagName) ? `${text}\n` : text;
2380
+ }
2381
+
2382
+ function aiPromptMarkup() {
2383
+ return aiPromptMarkupFromNode($("#ai-prompt")).replace(/\n$/u, "");
2384
+ }
2385
+
2386
+ function setAiPromptMarkup(value) {
2387
+ const prompt = $("#ai-prompt");
2388
+ const { text, references } = parseAiReferenceMarkup(value);
2389
+ prompt.replaceChildren();
2390
+ let cursor = 0;
2391
+ for (const reference of references) {
2392
+ const markerIndex = text.indexOf(reference.marker, cursor);
2393
+ if (markerIndex < 0) continue;
2394
+ if (markerIndex > cursor) prompt.append(document.createTextNode(text.slice(cursor, markerIndex)));
2395
+ const currentReference = aiReferenceByKey(reference.kind, reference.id);
2396
+ if (currentReference) prompt.append(createAiReferenceChip(currentReference));
2397
+ else prompt.append(document.createTextNode(reference.name));
2398
+ cursor = markerIndex + reference.marker.length;
2399
+ }
2400
+ if (cursor < text.length) prompt.append(document.createTextNode(text.slice(cursor)));
2401
+ renderAiReferences();
2402
+ }
2403
+
2404
+ function aiMessageReferenceReadable(reference) {
2405
+ const module = ({
2406
+ character: "characters",
2407
+ setting: "settings",
2408
+ chapter: "prose",
2409
+ "context-settings": "settings"
2410
+ })[reference.kind];
2411
+ return !module || canReadModule(module);
2412
+ }
2413
+
2414
+ function createInlineUserMessageReference(reference) {
2415
+ const bubble = document.createElement("span");
2416
+ const kind = aiReferenceKindLabel(reference);
2417
+ const name = aiMessageReferenceReadable(reference) && reference.name.trim()
2418
+ ? reference.name.trim()
2419
+ : "已隐藏引用";
2420
+ bubble.className = "user-message-mention user-message-inline-mention";
2421
+ bubble.textContent = `${kind} · ${name}`;
2422
+ bubble.title = `${kind}:${name}`;
2423
+ bubble.setAttribute("aria-label", `${kind}:${name}`);
2424
+ return bubble;
2425
+ }
2426
+
2427
+ function replaceAiReferenceMarkers(host, references) {
2428
+ if (!host || references.length === 0) return;
2429
+ const markers = new Map(references.map((reference) => [reference.marker, reference]));
2430
+ const textNodes = [];
2431
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT);
2432
+ while (walker.nextNode()) textNodes.push(walker.currentNode);
2433
+ for (const textNode of textNodes) {
2434
+ const value = textNode.textContent ?? "";
2435
+ const matches = [...markers.keys()].filter((marker) => value.includes(marker));
2436
+ if (matches.length === 0) continue;
2437
+ const fragment = document.createDocumentFragment();
2438
+ let cursor = 0;
2439
+ while (cursor < value.length) {
2440
+ const match = [...markers.keys()]
2441
+ .map((marker) => ({ marker, index: value.indexOf(marker, cursor) }))
2442
+ .filter((item) => item.index >= 0)
2443
+ .sort((left, right) => left.index - right.index)[0];
2444
+ if (!match) {
2445
+ fragment.append(document.createTextNode(value.slice(cursor)));
2446
+ break;
2447
+ }
2448
+ if (match.index > cursor) fragment.append(document.createTextNode(value.slice(cursor, match.index)));
2449
+ fragment.append(createInlineUserMessageReference(markers.get(match.marker)));
2450
+ cursor = match.index + match.marker.length;
2451
+ }
2452
+ textNode.replaceWith(fragment);
2453
+ }
2454
+ }
2455
+
2456
+ function userMessageMentionEntries(ids, items) {
2457
+ if (!Array.isArray(ids) || !Array.isArray(items)) return [];
2458
+ const names = new Map(items.map((item) => [String(item?.id ?? ""), String(item?.name ?? "").trim()]));
2459
+ const seen = new Set();
2460
+ const entries = [];
2461
+ for (const id of ids) {
2462
+ const value = String(id ?? "");
2463
+ if (!value || seen.has(value)) continue;
2464
+ seen.add(value);
2465
+ const name = names.get(value);
2466
+ if (name) entries.push({ id: value, name });
2467
+ }
2468
+ return entries;
2469
+ }
2470
+
2327
2471
  function createAiReferenceChip(reference) {
2328
2472
  const chip = document.createElement("span");
2329
2473
  chip.className = "ai-prompt-reference";
@@ -2397,7 +2541,7 @@ function createAiChatTabState(input = {}) {
2397
2541
  roleplayUserCharacter: input.roleplayUserCharacter ?? null,
2398
2542
  citations: input.citations ?? [],
2399
2543
  references: input.references ?? [],
2400
- composer: input.composer ?? { text: "", citations: [], references: [], images: [], semanticSnapshot: null, sceneDirection: "", scenePin: emptyRoleplayScenePin() },
2544
+ composer: input.composer ?? { text: "", markup: "", citations: [], references: [], images: [], semanticSnapshot: null, sceneDirection: "", scenePin: emptyRoleplayScenePin() },
2401
2545
  contextUsage: input.contextUsage ?? null,
2402
2546
  contextWarning: input.contextWarning === true,
2403
2547
  lastMessageAt: input.lastMessageAt ?? null,
@@ -2437,6 +2581,7 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
2437
2581
  tab.references = snapshot.references.map((reference) => ({ ...reference }));
2438
2582
  tab.composer = {
2439
2583
  text: snapshot.text,
2584
+ markup: snapshot.markup,
2440
2585
  citations: tab.citations.map((citation) => ({ ...citation })),
2441
2586
  references: tab.references.map((reference) => ({ ...reference })),
2442
2587
  images: normalizeAiChatImageAttachments(snapshot.images),
@@ -2449,6 +2594,7 @@ function setAiChatTabComposerSnapshot(tab, snapshot) {
2449
2594
  function clearAiChatTabComposer(tab) {
2450
2595
  setAiChatTabComposerSnapshot(tab, {
2451
2596
  text: "",
2597
+ markup: "",
2452
2598
  citations: [],
2453
2599
  references: [],
2454
2600
  images: [],
@@ -2475,7 +2621,7 @@ function applyAiChatTabState(tab) {
2475
2621
  const selectedModelId = tab.modelId ?? tab.selectedModelId;
2476
2622
  if (selectedModelId && state.models.some((model) => model.id === selectedModelId)) $("#ai-model").value = selectedModelId;
2477
2623
  syncAiModelPicker();
2478
- setAiPromptText(tab.composer.text);
2624
+ setAiPromptMarkup(tab.composer.markup ?? tab.composer.text);
2479
2625
  restoreAiSceneComposer(tab.composer);
2480
2626
  renderAiCitations();
2481
2627
  renderAiSemanticInjection();
@@ -2688,6 +2834,7 @@ function activateAiChatTab(tabId, { persistCurrent = true, force = false } = {})
2688
2834
  feed.id = feed === tab.feed ? "ai-feed" : `ai-chat-panel-${feed.dataset.aiTabId}`;
2689
2835
  }
2690
2836
  tab.feed.classList.remove("hidden");
2837
+ aiStreamRenders.refresh();
2691
2838
  applyAiChatTabState(tab);
2692
2839
  renderAiChatTabs();
2693
2840
  return tab;
@@ -2703,6 +2850,7 @@ function closeAiChatTab(tabId) {
2703
2850
  }
2704
2851
  const { active } = aiChatTabManager.close(tab.id);
2705
2852
  tab.feed.remove();
2853
+ aiStreamRenders.refresh();
2706
2854
  if (!wasActive) {
2707
2855
  renderAiChatTabs();
2708
2856
  return;
@@ -3087,6 +3235,42 @@ const aiFeedScrollFrames = new WeakMap();
3087
3235
  const aiFeedAutoScrollStates = new WeakMap();
3088
3236
  const aiFeedScrollBindings = new WeakSet();
3089
3237
  const aiProcessScrollFrames = new WeakMap();
3238
+ const aiProcessRenderStates = new WeakMap();
3239
+ const aiMarkdownRenderers = new WeakMap();
3240
+ function aiStreamTargetVisible(target) {
3241
+ target = target.element ?? target;
3242
+ const feed = target.closest(".ai-feed");
3243
+ const app = $("#app");
3244
+ return document.visibilityState !== "hidden" && Boolean(feed?.isConnected)
3245
+ && !feed.classList.contains("hidden") && !app.classList.contains("shelf-mode")
3246
+ && (!app.classList.contains("ai-panel-collapsed") || app.classList.contains("ai-workspace-mode"));
3247
+ }
3248
+ const aiStreamRenders = createAiRenderScheduler({
3249
+ isVisible: aiStreamTargetVisible,
3250
+ isConnected: (target) => (target.element ?? target).isConnected
3251
+ });
3252
+ document.addEventListener("visibilitychange", () => aiStreamRenders.refresh());
3253
+ // 只监听面板显隐,不观察正文或消息内容,避免渲染自身触发新的刷新。
3254
+ const aiPanelVisibilityObserver = new MutationObserver(() => aiStreamRenders.refresh());
3255
+ aiPanelVisibilityObserver.observe($("#app"), { attributes: true, attributeFilter: ["class"] });
3256
+
3257
+ function updateAiMarkdown(body, content) {
3258
+ let render = aiMarkdownRenderers.get(body);
3259
+ if (!render) {
3260
+ const target = { element: body };
3261
+ render = createStreamingMarkdownRenderer(body, {
3262
+ enqueue: (callback) => queueMicrotask(() => aiStreamRenders.enqueue(target, callback)),
3263
+ onRender: () => {
3264
+ const message = body.closest(".assistant-message");
3265
+ if (message?.classList.contains("is-streaming")) scrollAiProcessStepsToBottom(message);
3266
+ const feed = body.closest(".ai-feed");
3267
+ if (feed) scrollAiFeedToBottom(feed);
3268
+ }
3269
+ });
3270
+ aiMarkdownRenderers.set(body, render);
3271
+ }
3272
+ return render(content);
3273
+ }
3090
3274
  const AI_FEED_BOTTOM_THRESHOLD_PX = 24;
3091
3275
  let markdownTableMenuTarget = null;
3092
3276
  let markdownTableMenuTrigger = null;
@@ -3153,9 +3337,8 @@ function scrollAiFeedToBottom(feed = $("#ai-feed"), { force = false } = {}) {
3153
3337
  bindAiFeedAutoScroll(feed);
3154
3338
  if (force) aiFeedAutoScrollStates.set(feed, true);
3155
3339
  if (!aiFeedAutoScrollStates.get(feed)) return;
3156
- feed.scrollTop = feed.scrollHeight;
3157
3340
  const currentFrame = aiFeedScrollFrames.get(feed);
3158
- if (currentFrame !== undefined) window.cancelAnimationFrame(currentFrame);
3341
+ if (currentFrame !== undefined || !aiStreamTargetVisible(feed)) return;
3159
3342
  const nextFrame = window.requestAnimationFrame(() => {
3160
3343
  feed.scrollTop = feed.scrollHeight;
3161
3344
  aiFeedScrollFrames.delete(feed);
@@ -3169,9 +3352,8 @@ function scrollAiProcessStepsToBottom(message) {
3169
3352
  body.scrollTop = body.scrollHeight;
3170
3353
  });
3171
3354
  };
3172
- scroll();
3173
3355
  const currentFrame = aiProcessScrollFrames.get(message);
3174
- if (currentFrame !== undefined) window.cancelAnimationFrame(currentFrame);
3356
+ if (currentFrame !== undefined || !aiStreamTargetVisible(message)) return;
3175
3357
  const nextFrame = window.requestAnimationFrame(() => {
3176
3358
  scroll();
3177
3359
  aiProcessScrollFrames.delete(message);
@@ -3724,74 +3906,80 @@ function shouldRenderAiProcessStep(step) {
3724
3906
  }
3725
3907
 
3726
3908
  function renderAiProcessSteps(message, steps, completed, durationMs = null, visibleContents = null) {
3727
- const previousDetails = message.querySelector(".ai-process-details");
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();
3909
+ const cached = aiProcessRenderStates.get(message);
3738
3910
  const renderableSteps = (Array.isArray(steps) ? steps : []).filter(shouldRenderAiProcessStep);
3739
- if (!renderableSteps.length) return;
3740
- const details = document.createElement("details");
3911
+ if (!renderableSteps.length) {
3912
+ cached?.details.remove();
3913
+ aiProcessRenderStates.delete(message);
3914
+ return;
3915
+ }
3916
+ const details = cached?.details ?? document.createElement("details");
3741
3917
  details.className = "ai-process-details";
3742
3918
  // 存在待确认/待回答的交互卡片时保持展开,避免审批入口在历史消息中被折叠。
3743
3919
  details.open = !completed || steps.some((step) => step?.type === "tool" && isInteractiveToolPending(step.toolCall));
3744
- const summary = document.createElement("summary");
3745
- const title = document.createElement("span");
3920
+ const summary = cached?.summary ?? document.createElement("summary");
3921
+ const title = cached?.title ?? document.createElement("span");
3746
3922
  title.textContent = completed ? "思考与执行过程" : "正在思考与执行";
3747
- const status = document.createElement("small");
3923
+ const status = cached?.status ?? document.createElement("small");
3748
3924
  const duration = durationMs === null || durationMs === undefined ? "" : formatAiProcessDuration(durationMs);
3749
3925
  status.textContent = `${renderableSteps.length} 个步骤${duration ? ` · 耗时 ${duration}` : ""}`;
3750
- summary.append(title, status);
3751
- const list = document.createElement("div");
3926
+ if (!cached) summary.append(title, status);
3927
+ const list = cached?.list ?? document.createElement("div");
3752
3928
  list.className = "ai-process-list";
3753
- for (const step of renderableSteps) {
3929
+ const entries = new Map();
3930
+ const nextNodes = [];
3931
+ for (const [index, step] of renderableSteps.entries()) {
3932
+ const key = `${step.type}:${String(step.id ?? index)}`;
3933
+ const previous = cached?.entries.get(key);
3754
3934
  if (step?.type === "context_compaction") {
3755
- list.append(createAiContextCompactionDivider({
3935
+ const divider = previous?.node ?? createAiContextCompactionDivider({
3756
3936
  kind: "tool",
3757
3937
  ariaLabel: `第 ${Number(step.round) || 1} 轮已压缩上下文`,
3758
3938
  title: `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`
3759
- }));
3939
+ });
3940
+ entries.set(key, { node: divider });
3941
+ nextNodes.push(divider);
3760
3942
  continue;
3761
3943
  }
3762
3944
  if (step?.type === "tool" && step.toolCall) {
3945
+ if (previous?.value === step.toolCall) {
3946
+ entries.set(key, previous);
3947
+ nextNodes.push(previous.node);
3948
+ continue;
3949
+ }
3763
3950
  const tool = document.createElement("section");
3764
3951
  tool.className = "ai-process-step ai-process-tool-step";
3765
3952
  const label = document.createElement("small");
3766
3953
  label.textContent = `第 ${Number(step.round) || 1} 轮 · 工具调用`;
3767
3954
  tool.append(label, createAiToolCallButton(step.toolCall));
3768
- list.append(tool);
3955
+ entries.set(key, { node: tool, value: step.toolCall });
3956
+ nextNodes.push(tool);
3769
3957
  continue;
3770
3958
  }
3771
- const section = document.createElement("section");
3959
+ const section = previous?.node ?? document.createElement("section");
3772
3960
  section.className = `ai-process-step ai-process-${step.type}-step`;
3773
3961
  section.dataset.aiProcessStepId = `${step.type}:${String(step.id ?? step.round ?? "")}`;
3774
- const label = document.createElement("small");
3962
+ const label = previous?.label ?? document.createElement("small");
3775
3963
  label.textContent = `第 ${Number(step.round) || 1} 轮 · ${step.type === "thinking" ? "Thinking" : "中间输出"}`;
3776
- const body = document.createElement("div");
3964
+ const body = previous?.body ?? document.createElement("div");
3777
3965
  body.className = "message-body ai-process-step-body";
3778
3966
  const content = visibleContents?.has(step) ? visibleContents.get(step) : step.content;
3779
- body.innerHTML = renderMarkdown(content);
3780
- section.append(label, body);
3781
- list.append(section);
3782
- }
3783
- details.append(summary, list);
3784
- list.querySelectorAll(".ai-process-step[data-ai-process-step-id]").forEach((section) => {
3785
- const scrollState = previousScrollStates.get(section.dataset.aiProcessStepId);
3786
- const body = section.querySelector(".ai-process-step-body");
3787
- if (!scrollState || !body) return;
3788
- body.scrollTop = scrollState.nearBottom
3789
- ? body.scrollHeight
3790
- : Math.min(scrollState.scrollTop, body.scrollHeight);
3791
- });
3792
- const body = message.querySelector(".message-body");
3793
- if (body) body.before(details);
3794
- else message.append(details);
3967
+ updateAiMarkdown(body, content);
3968
+ if (!previous) section.append(label, body);
3969
+ entries.set(key, { node: section, label, body });
3970
+ nextNodes.push(section);
3971
+ }
3972
+ if (!cached) details.append(summary, list);
3973
+ nextNodes.forEach((node, index) => {
3974
+ if (list.children[index] !== node) list.insertBefore(node, list.children[index] ?? null);
3975
+ });
3976
+ while (list.children.length > nextNodes.length) list.lastElementChild.remove();
3977
+ if (!cached) {
3978
+ const body = message.querySelector(".message-body");
3979
+ if (body) body.before(details);
3980
+ else message.append(details);
3981
+ }
3982
+ aiProcessRenderStates.set(message, { details, summary, title, status, list, entries, completed });
3795
3983
  if (!completed) scrollAiProcessStepsToBottom(message);
3796
3984
  }
3797
3985
 
@@ -4964,11 +5152,14 @@ function aiModelSupportsImageInput() {
4964
5152
  function syncAiImageAttachmentControl() {
4965
5153
  const button = $("#ai-attachment-button");
4966
5154
  if (!button) return;
4967
- const enabled = aiModelSupportsImageInput();
4968
- button.classList.toggle("hidden", !enabled);
5155
+ const model = activeAiModel();
5156
+ const enabled = model?.multimodalEnabled === true;
4969
5157
  button.disabled = !enabled || aiInteractionBusy();
4970
- button.setAttribute("aria-hidden", String(!enabled));
4971
- button.title = enabled ? "添加图片附件" : "当前模型不支持图片输入";
5158
+ button.title = enabled
5159
+ ? "添加图片附件"
5160
+ : model
5161
+ ? "当前模型不支持图片输入"
5162
+ : "选择多模态模型后可添加图片附件";
4972
5163
  }
4973
5164
 
4974
5165
  function renderAiImageAttachments() {
@@ -5089,6 +5280,7 @@ function clearAiPromptComposer({ collapseScenePanel = false } = {}) {
5089
5280
  function captureAiPromptComposer() {
5090
5281
  return {
5091
5282
  text: aiPromptText(),
5283
+ markup: aiPromptMarkup(),
5092
5284
  citations: state.aiCitations.map((citation) => ({ ...citation })),
5093
5285
  references: state.aiReferences.map((reference) => ({ ...reference })),
5094
5286
  images: normalizeAiChatImageAttachments(state.aiImageAttachments),
@@ -5103,7 +5295,7 @@ function restoreAiPromptComposer(snapshot) {
5103
5295
  state.aiReferences = snapshot.references.map((reference) => ({ ...reference }));
5104
5296
  state.aiImageAttachments = normalizeAiChatImageAttachments(snapshot.images);
5105
5297
  state.aiSemanticSnapshot = snapshot.semanticSnapshot ? structuredClone(snapshot.semanticSnapshot) : null;
5106
- setAiPromptText(snapshot.text);
5298
+ setAiPromptMarkup(snapshot.markup ?? snapshot.text);
5107
5299
  restoreAiSceneComposer(snapshot);
5108
5300
  renderAiCitations();
5109
5301
  renderAiImageAttachments();
@@ -9108,6 +9300,7 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
9108
9300
  menu.querySelector("strong").textContent = `操作“${chapter.title}”`;
9109
9301
  menu.querySelectorAll("[data-chapter-type], [data-delete-chapter]").forEach((button) => button.classList.toggle("hidden", !canManageChapter));
9110
9302
  menu.querySelector("[data-add-chapter-ai-reference]")?.classList.toggle("hidden", !canAddAiReference);
9303
+ menu.querySelector("#chapter-type-ai-reference-separator")?.classList.toggle("hidden", !(canManageChapter && canAddAiReference));
9111
9304
  menu.querySelectorAll("[data-chapter-type]").forEach((button) => {
9112
9305
  button.classList.toggle("active", button.dataset.chapterType === (chapter.chapterType || "正文"));
9113
9306
  button.setAttribute("aria-checked", String(button.classList.contains("active")));
@@ -14633,10 +14826,7 @@ function renderAiContextDistribution(usage) {
14633
14826
  const popover = $("#ai-context-popover");
14634
14827
  const host = $("#ai-context-distribution");
14635
14828
  const distribution = normalizeAiContextTokenDistribution(usage);
14636
- const contextWindow = distribution.contextWindow.toLocaleString("zh-CN");
14637
- $("#ai-context-popover-description").textContent = usage
14638
- ? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok · ${formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow)}`
14639
- : "选择可用模型后显示当前上下文用量";
14829
+ $("#ai-context-popover-description").textContent = formatAiContextPopoverDescription(usage);
14640
14830
  host.replaceChildren(...distribution.items.map((item) => {
14641
14831
  const row = document.createElement("div");
14642
14832
  row.className = "ai-context-distribution-row";
@@ -14678,13 +14868,11 @@ function setAiContextMeter(usage, allowShrink = true) {
14678
14868
  : mergeAiContextUsage(latestAiContextUsage, usage, false);
14679
14869
  latestAiContextUsage = displayUsage;
14680
14870
  const meter = $("#ai-context-meter");
14681
- const value = meter.querySelector("b");
14682
14871
  const distribution = renderAiContextDistribution(displayUsage);
14683
14872
  if (!displayUsage) {
14684
14873
  meter.classList.add("is-empty");
14685
14874
  meter.classList.remove("is-warning", "is-danger");
14686
14875
  meter.style.setProperty("--context-usage", "0");
14687
- value.textContent = "—";
14688
14876
  const tooltip = formatAiContextUsageTooltip(null);
14689
14877
  meter.dataset.tooltip = tooltip;
14690
14878
  meter.setAttribute("aria-label", tooltip);
@@ -14697,7 +14885,6 @@ function setAiContextMeter(usage, allowShrink = true) {
14697
14885
  meter.classList.toggle("is-warning", percent >= 70 && percent < 90);
14698
14886
  meter.classList.toggle("is-danger", percent >= 90);
14699
14887
  meter.style.setProperty("--context-usage", String(percent));
14700
- value.textContent = formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow);
14701
14888
  const tooltip = formatAiContextUsageTooltip(displayUsage);
14702
14889
  meter.dataset.tooltip = tooltip;
14703
14890
  meter.setAttribute("aria-label", `当前上下文用量:${tooltip}`);
@@ -18227,6 +18414,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18227
18414
  const requestComposerSnapshot = retry
18228
18415
  ? {
18229
18416
  text: retry.prompt,
18417
+ markup: retry.prompt,
18230
18418
  citations: retry.citations ?? [],
18231
18419
  references: [],
18232
18420
  images: retry.images ?? [],
@@ -18234,14 +18422,15 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18234
18422
  scenePin: captureAiScenePin()
18235
18423
  }
18236
18424
  : composerSnapshot;
18237
- const instruction = requestComposerSnapshot.text.trim();
18425
+ const instruction = String(requestComposerSnapshot.markup ?? requestComposerSnapshot.text).trim();
18426
+ const instructionText = String(requestComposerSnapshot.text ?? "").trim();
18238
18427
  const sceneDirection = $("#ai-task").value === "roleplay"
18239
18428
  ? String(requestComposerSnapshot.sceneDirection ?? "").trim()
18240
18429
  : "";
18241
18430
  const scenePin = $("#ai-task").value === "roleplay"
18242
18431
  ? normalizeRoleplayScenePin(requestComposerSnapshot.scenePin)
18243
18432
  : emptyRoleplayScenePin();
18244
- if (!instruction && !sceneDirection) {
18433
+ if (!instructionText && !sceneDirection) {
18245
18434
  return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入指令", "error");
18246
18435
  }
18247
18436
  if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
@@ -18295,7 +18484,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18295
18484
  let assistantMessage;
18296
18485
  let assistantMetadata = {};
18297
18486
  let persistedStreamMessage = null;
18298
- let writingSuggestion = null;
18299
18487
  const streamed = await streamChat(requestHolder, aiRetryStreamRequestBody({
18300
18488
  instruction,
18301
18489
  ...(sceneDirection ? { sceneDirection } : {}),
@@ -18312,7 +18500,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18312
18500
  assistantContent = streamed.content;
18313
18501
  assistantMessage = streamed.message;
18314
18502
  assistantMetadata = streamed.metadata;
18315
- writingSuggestion = streamed.writingSuggestion;
18316
18503
  persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
18317
18504
  applyAiConversationTitle(streamed.conversationTitle, streamedRequest.conversationId);
18318
18505
  try {
@@ -18342,7 +18529,6 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
18342
18529
  attachMessageIdentity(assistantMessage, persistedAssistantMessage.id);
18343
18530
  }
18344
18531
  }
18345
- if (writingSuggestion && assistantMessage) attachWritingSuggestion(assistantMessage, writingSuggestion, { tab });
18346
18532
  } catch (error) {
18347
18533
  if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
18348
18534
  setAiChatTabStatus(tab, "error");
@@ -18494,11 +18680,15 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18494
18680
  };
18495
18681
  const typewriter = createStreamTypewriter({
18496
18682
  speedController: streamSpeedController,
18683
+ shouldAnimate: () => aiStreamTargetVisible(feed),
18497
18684
  onRender: (text, progress) => {
18498
18685
  if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
18499
- content.innerHTML = renderMarkdown(text);
18500
- renderAiStreamingCharacterProgress(meta, progress.visibleCharacters);
18501
- scrollAiFeedToBottom(feed);
18686
+ aiStreamRenders.enqueue(content, () => {
18687
+ if (!aiRequestTargetsCurrentState(requestHolder.snapshot) && message.classList.contains("is-streaming")) return;
18688
+ updateAiMarkdown(content, text);
18689
+ if (message.classList.contains("is-streaming")) renderAiStreamingCharacterProgress(meta, progress.visibleCharacters);
18690
+ scrollAiFeedToBottom(feed);
18691
+ });
18502
18692
  }
18503
18693
  });
18504
18694
  let streamedText = "";
@@ -18509,7 +18699,6 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18509
18699
  let persistedMessageId = null;
18510
18700
  let persistedMessageCreatedAt = null;
18511
18701
  let conversationTitle = null;
18512
- let writingSuggestion = null;
18513
18702
  let question = null;
18514
18703
  let persistedUserMessage = null;
18515
18704
  let contextAction = "ready";
@@ -18522,7 +18711,10 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18522
18711
  const processStepVisibleContents = new Map();
18523
18712
  const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
18524
18713
  if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
18525
- renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
18714
+ aiStreamRenders.enqueue(message, () => {
18715
+ renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
18716
+ scrollAiFeedToBottom(feed);
18717
+ });
18526
18718
  };
18527
18719
  const processStepTypewriter = (step) => {
18528
18720
  const existing = processStepTypewriters.get(step);
@@ -18530,6 +18722,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18530
18722
  processStepVisibleContents.set(step, "");
18531
18723
  const typewriter = createStreamTypewriter({
18532
18724
  speedController: streamSpeedController,
18725
+ shouldAnimate: () => aiStreamTargetVisible(feed),
18533
18726
  onRender: (text) => {
18534
18727
  if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
18535
18728
  processStepVisibleContents.set(step, text);
@@ -18684,15 +18877,8 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18684
18877
  persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
18685
18878
  persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
18686
18879
  conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
18687
- writingSuggestion = payload.writingSuggestion && typeof payload.writingSuggestion === "object"
18688
- ? payload.writingSuggestion
18689
- : null;
18690
- const writingSuggestionFailed = writingSuggestion?.guard?.status === "failed"
18691
- || writingSuggestion?.toolCalls?.some((toolCall) => toolCall.status === "failed")
18692
- || writingSuggestion?.processSteps?.some((step) => step?.toolCall?.status === "failed");
18693
- if (writingSuggestionFailed) setAiChatTabStatus(tab, "error");
18694
18880
  const announcedCompaction = contextAction === "compacted" || streamContextCompacted;
18695
- setAiChatTabContextUsage(tab, payload.contextUsage, announcedCompaction);
18881
+ setAiChatTabContextUsage(tab, attachAiContextCacheHitPercent(payload.contextUsage, payload.cacheHitPercent), announcedCompaction);
18696
18882
  await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
18697
18883
  assertAiRequestCurrent(requestHolder.snapshot);
18698
18884
  message.classList.remove("is-streaming");
@@ -18710,12 +18896,8 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18710
18896
  toolCalls,
18711
18897
  processSteps,
18712
18898
  processDurationMs,
18713
- ...(writingSuggestion ? {
18714
- activeSkills: [writingSuggestion.taskType === "continue" ? "continue-writing" : "polish-writing"],
18715
- writingSuggestionId: writingSuggestion.id
18716
- } : {})
18717
18899
  };
18718
- renderAiProcessSteps(message, processSteps, true, processDurationMs);
18900
+ renderStreamingProcessSteps(true, processDurationMs);
18719
18901
  meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent, "", processDurationMs);
18720
18902
  attachAssistantCopyAction(message, streamedText);
18721
18903
  scrollAiFeedToBottom(feed);
@@ -18731,7 +18913,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
18731
18913
  assertAiRequestCurrent(requestHolder.snapshot);
18732
18914
  if (streamError) throw streamError;
18733
18915
  assertAiStreamCompleted(streamCompleted);
18734
- return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, writingSuggestion, userMessage: persistedUserMessage, question };
18916
+ return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage, question };
18735
18917
  } catch (error) {
18736
18918
  const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
18737
18919
  const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
@@ -18821,10 +19003,14 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18821
19003
  if (errorCode) message.dataset.errorCode = errorCode;
18822
19004
  if (isFailure && typeof metadata?.pendingQuestionId === "string") message.dataset.pendingQuestionId = metadata.pendingQuestionId;
18823
19005
  const parsedUserTurn = role === "user" ? parseRoleplayUserTurn(text) : null;
19006
+ const userMessageContent = parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text;
19007
+ const inlineReferences = role === "user" ? parseAiReferenceMarkup(userMessageContent).references : [];
19008
+ const renderedUserMessage = role === "user" ? parseAiReferenceMarkup(userMessageContent).text : userMessageContent;
18824
19009
  const messageBody = isFailure
18825
19010
  ? `<p class="ai-error-text">${esc(text)}</p>${aiToolCallSettingsLinkMarkup(text)}`
18826
- : renderMarkdown(parsedUserTurn?.hasMarkup ? parsedUserTurn.userMessage : text);
19011
+ : renderMarkdown(renderedUserMessage);
18827
19012
  message.innerHTML = `<div class="message-body">${messageBody}</div>`;
19013
+ if (role === "user") replaceAiReferenceMarkers(message.querySelector(".message-body"), inlineReferences);
18828
19014
  message.querySelector("[data-ai-tool-call-settings-link]")?.addEventListener("click", (event) => {
18829
19015
  event.preventDefault();
18830
19016
  openAiToolCallSettings().catch((error) => toast(`打开 AI 设置失败:${error.message}`, "error"));
@@ -18863,16 +19049,19 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18863
19049
  }))) ?? [];
18864
19050
  const settingReferences = state.settings.map((setting) => ({ id: setting.id, name: setting.title }));
18865
19051
  const contextSettingReferences = [{ id: "include-setting-info", name: "注入上下文设定" }];
19052
+ const inlineReferenceKeys = new Set(inlineReferences.map((reference) => aiReferenceKey(reference)));
18866
19053
  const mentionGroups = role === "user"
18867
19054
  ? [
18868
- ["角色", metadata?.mentionCharacterIds, state.characters],
18869
- ["种族", metadata?.mentionRaceIds, state.races],
18870
- ["组织", metadata?.mentionOrganizationIds, state.organizations],
18871
- ["设定", metadata?.mentionSettingIds, settingReferences],
18872
- ["章节", metadata?.mentionChapterIds, chapterReferences],
18873
- ["能力", metadata?.mentionContextSettingIds, contextSettingReferences]
19055
+ ["character", "角色", metadata?.mentionCharacterIds, state.characters],
19056
+ ["race", "种族", metadata?.mentionRaceIds, state.races],
19057
+ ["organization", "组织", metadata?.mentionOrganizationIds, state.organizations],
19058
+ ["setting", "设定", metadata?.mentionSettingIds, settingReferences],
19059
+ ["chapter", "章节", metadata?.mentionChapterIds, chapterReferences],
19060
+ ["context-settings", "能力", metadata?.mentionContextSettingIds, contextSettingReferences]
18874
19061
  ]
18875
- .flatMap(([kind, ids, items]) => userMessageMentionNames(ids, items).map((name) => ({ kind, name })))
19062
+ .flatMap(([referenceKind, kind, ids, items]) => userMessageMentionEntries(ids, items)
19063
+ .filter((reference) => !inlineReferenceKeys.has(`${referenceKind}:${reference.id}`))
19064
+ .map((reference) => ({ kind, name: reference.name })))
18876
19065
  : [];
18877
19066
  if (mentionGroups.length) {
18878
19067
  const references = document.createElement("div");
@@ -18953,109 +19142,10 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18953
19142
  if (isFailure) renderMessageCardActions(message);
18954
19143
  attachMessageIdentity(message, messageId);
18955
19144
  feed.append(message);
18956
- const writingSuggestionId = role === "assistant" && typeof metadata?.writingSuggestionId === "string"
18957
- ? metadata.writingSuggestionId
18958
- : "";
18959
- if (writingSuggestionId && !isFailure && !isInterrupted) {
18960
- api(`/api/suggestions/${encodeURIComponent(writingSuggestionId)}`)
18961
- .then((suggestion) => attachWritingSuggestion(message, suggestion, { tab }))
18962
- .catch(() => undefined);
18963
- }
18964
19145
  scrollAiFeedToBottom(feed);
18965
19146
  return message;
18966
19147
  }
18967
19148
 
18968
- function continuationGuardMarkup(guard) {
18969
- if (!guard) return "";
18970
- const issues = Array.isArray(guard.issues) ? guard.issues : [];
18971
- const failure = typeof guard.failure === "string" && guard.failure.trim()
18972
- ? guard.failure.trim()
18973
- : "无法完成检查,请谨慎采纳";
18974
- return `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<details class="guard-failure-details"><summary>查看失败原因</summary><p>${esc(failure)}</p></details>` : issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>`;
18975
- }
18976
-
18977
- async function applyAcceptedWritingSuggestion(message, suggestion) {
18978
- if (state.work?.id === suggestion.workId && state.chapter?.id === suggestion.chapterId
18979
- && (state.dirty || chapterSaveInFlight || chapterSaveGuardInFlight)) {
18980
- throw new Error("当前章节有未保存修改或正在保存,请先完成保存,再重新生成正文建议");
18981
- }
18982
- const result = await api(`/api/suggestions/${encodeURIComponent(suggestion.id)}/accept`, { method: "POST", body: {} });
18983
- const workId = result.chapter.workId;
18984
- if (state.work?.id === workId && state.chapter?.id === result.chapter.id
18985
- && !state.dirty && !chapterSaveInFlight && !chapterSaveGuardInFlight) {
18986
- cancelChapterAutoSave();
18987
- state.chapter = result.chapter;
18988
- resetChapterDraftLineIds(state.chapter);
18989
- lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
18990
- $("#chapter-title").value = state.chapter.title;
18991
- $("#chapter-content").value = state.chapter.content;
18992
- scheduleChapterLineNumbers();
18993
- updateChapterStats();
18994
- }
18995
- if (state.work?.id === workId) {
18996
- const work = await api(`/api/works/${workId}`);
18997
- if (state.work?.id === workId) {
18998
- state.work = work;
18999
- renderTree();
19000
- }
19001
- }
19002
- message.querySelector("[data-writing-suggestion-actions]").innerHTML = "<span>已采纳并生成新版本</span>";
19003
- toast("AI 建议已采纳,正文已生成新版本");
19004
- }
19005
-
19006
- function attachWritingSuggestion(message, suggestion, options = {}) {
19007
- if (!suggestion || suggestion.action === "note" || !suggestion.id) return message;
19008
- const suggestionId = String(suggestion.id);
19009
- if (message.dataset.writingSuggestionId === suggestionId) return message;
19010
- message.dataset.writingSuggestionId = suggestionId;
19011
- message.querySelector("[data-writing-suggestion-ui]")?.remove();
19012
- const heading = message.querySelector(".message-heading > span");
19013
- if (heading) heading.textContent = "助手建议";
19014
- const host = document.createElement("div");
19015
- host.dataset.writingSuggestionUi = "";
19016
- host.className = "writing-suggestion-ui";
19017
- host.innerHTML = `${continuationGuardMarkup(suggestion.guard)}<div class="message-actions" data-writing-suggestion-actions></div>`;
19018
- const actions = host.querySelector("[data-writing-suggestion-actions]");
19019
- if (suggestion.status === "accepted") {
19020
- actions.innerHTML = "<span>已采纳并生成新版本</span>";
19021
- } else if (suggestion.status === "rejected") {
19022
- actions.innerHTML = "<span>已拒绝</span>";
19023
- } else {
19024
- actions.innerHTML = '<button type="button" data-action="accept">采纳到正文</button><button type="button" data-action="reject">拒绝</button>';
19025
- actions.querySelector('[data-action="accept"]').addEventListener("click", async () => {
19026
- try {
19027
- await applyAcceptedWritingSuggestion(message, suggestion);
19028
- } catch (error) {
19029
- toast(error.message, "error");
19030
- }
19031
- });
19032
- actions.querySelector('[data-action="reject"]').addEventListener("click", async () => {
19033
- try {
19034
- await api(`/api/suggestions/${encodeURIComponent(suggestion.id)}/reject`, { method: "POST", body: {} });
19035
- actions.innerHTML = "<span>已拒绝</span>";
19036
- } catch (error) {
19037
- toast(error.message, "error");
19038
- }
19039
- });
19040
- }
19041
- message.append(host);
19042
- const tab = options.tab ?? activeAiChatTab();
19043
- scrollAiFeedToBottom(options.feed ?? tab?.feed ?? $("#ai-feed"));
19044
- return message;
19045
- }
19046
-
19047
- function appendSuggestion(suggestion, createdAt = null, messageId = null, options = {}) {
19048
- const tab = options.tab ?? activeAiChatTab();
19049
- const feed = options.feed ?? tab?.feed ?? $("#ai-feed");
19050
- const message = appendMessage("assistant", suggestion.content, [], createdAt, {
19051
- modelDisplayName: suggestion.model?.displayName,
19052
- outputTokens: suggestion.outputTokens,
19053
- cacheHitPercent: suggestion.cacheHitPercent,
19054
- processDurationMs: suggestion.processDurationMs
19055
- }, messageId, { tab, feed });
19056
- return attachWritingSuggestion(message, suggestion, { tab, feed });
19057
- }
19058
-
19059
19149
  function chapterVersionCompareOption(version) {
19060
19150
  return `<option value="version:${Number(version.versionNo)}">v${Number(version.versionNo)} · ${esc(chapterVersionSourceLabel(version.source))}</option>`;
19061
19151
  }