@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.
- package/dist/ai.js +11 -89
- package/dist/ai.js.map +1 -1
- package/dist/app.js +33 -42
- package/dist/app.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 +284 -194
- package/dist/public/index.html +6 -4
- 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 +28 -38
- package/dist/skills/continue-writing/SKILL.md +2 -2
- package/dist/skills/polish-writing/SKILL.md +2 -2
- package/dist/user-auth.js +0 -3
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -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=
|
|
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
|
|
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=
|
|
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 {
|
|
@@ -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("&", "&").replaceAll("<", "<");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
function escapeAiReferenceXmlAttribute(value) {
|
|
2334
|
+
return escapeAiReferenceXmlText(value).replaceAll('"', """);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
function unescapeAiReferenceXmlText(value) {
|
|
2338
|
+
return String(value ?? "").replaceAll(""", '"').replaceAll("<", "<").replaceAll("&", "&");
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
3740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3780
|
-
section.append(label, body);
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
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
|
|
4968
|
-
|
|
5155
|
+
const model = activeAiModel();
|
|
5156
|
+
const enabled = model?.multimodalEnabled === true;
|
|
4969
5157
|
button.disabled = !enabled || aiInteractionBusy();
|
|
4970
|
-
button.
|
|
4971
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
18500
|
-
|
|
18501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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]) =>
|
|
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
|
}
|