@musnows/scriverse 0.9.0 → 0.9.2

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.
@@ -1052,9 +1052,14 @@ function renderPresence() {
1052
1052
  return;
1053
1053
  }
1054
1054
  const groups = groupedPresenceParticipants();
1055
+ const showControl = groups.length > 1;
1055
1056
  const localKey = presencePageKey(presencePageForRoute());
1056
1057
  syncChapterAutoSaveWithPresence();
1057
- control.classList.remove("hidden");
1058
+ control.classList.toggle("hidden", !showControl);
1059
+ if (!showControl) {
1060
+ $("#presence-panel").classList.add("hidden");
1061
+ $("#presence-button").setAttribute("aria-expanded", "false");
1062
+ }
1058
1063
  $("#presence-count").textContent = `${groups.length} 人`;
1059
1064
  $("#presence-list").innerHTML = groups.map((participant) => {
1060
1065
  const isCurrent = participant.userId === state.user?.userId;
@@ -3445,15 +3450,18 @@ async function createNewAiConversation(taskType = "chat") {
3445
3450
  function favoriteCharactersFirst(characters) {
3446
3451
  const items = Array.isArray(characters) ? characters : [];
3447
3452
  return [
3448
- ...items.filter((character) => character.isFavorite === true),
3449
- ...items.filter((character) => character.isFavorite !== true)
3453
+ ...items.filter((character) => character.isPinned === true && character.isFavorite === true),
3454
+ ...items.filter((character) => character.isPinned === true && character.isFavorite !== true),
3455
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite === true),
3456
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite !== true)
3450
3457
  ];
3451
3458
  }
3452
3459
 
3453
3460
  function roleplayCharacterOptionLabel(character) {
3454
- const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
3461
+ const pinLabel = character?.isPinned === true ? "[置顶]" : "";
3462
+ const favoriteLabel = character?.isFavorite === true ? "[收藏]" : "";
3455
3463
  const deathLabel = character?.isDead ? "(已死亡)" : "";
3456
- return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
3464
+ return `${pinLabel}${favoriteLabel}${pinLabel || favoriteLabel ? " " : ""}${String(character?.name ?? "")}${deathLabel}`;
3457
3465
  }
3458
3466
 
3459
3467
  function renderAiRoleplayCharacterSelect() {
@@ -4109,6 +4117,31 @@ function aiPromptTextBeforeCursor() {
4109
4117
  return promptTextFromNode(fragment).replace(/\n$/u, "");
4110
4118
  }
4111
4119
 
4120
+ function aiPromptTextFromRange(range, prompt) {
4121
+ const beforeCursor = range.cloneRange();
4122
+ beforeCursor.selectNodeContents(prompt);
4123
+ beforeCursor.setEnd(range.endContainer, range.endOffset);
4124
+ const fragment = document.createElement("div");
4125
+ fragment.append(beforeCursor.cloneContents());
4126
+ return promptTextFromNode(fragment).replace(/\n$/u, "");
4127
+ }
4128
+
4129
+ function aiPromptTextBoundary(prompt, offset) {
4130
+ const walker = document.createTreeWalker(prompt, NodeFilter.SHOW_TEXT);
4131
+ let remaining = Math.max(0, Number(offset) || 0);
4132
+ let lastTextNode = null;
4133
+ while (walker.nextNode()) {
4134
+ const textNode = walker.currentNode;
4135
+ if (textNode.parentElement?.closest("[data-ai-reference-key]")) continue;
4136
+ lastTextNode = textNode;
4137
+ const length = textNode.textContent?.length ?? 0;
4138
+ if (remaining <= length) return { node: textNode, offset: remaining };
4139
+ remaining -= length;
4140
+ }
4141
+ if (lastTextNode) return { node: lastTextNode, offset: lastTextNode.textContent?.length ?? 0 };
4142
+ return { node: prompt, offset: prompt.childNodes.length };
4143
+ }
4144
+
4112
4145
  function hideAiMentionMenu() {
4113
4146
  aiMentionMatch = null;
4114
4147
  aiMentionRange = null;
@@ -4197,12 +4230,14 @@ function selectAiMention(button) {
4197
4230
  id: button.dataset.aiReferenceId,
4198
4231
  name: button.dataset.aiReferenceName
4199
4232
  };
4200
- const range = aiMentionRange.cloneRange();
4201
- const textNode = range.startContainer;
4202
- const localText = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent?.slice(0, range.startOffset) ?? "" : "";
4203
- const localMention = findAiMention(localText);
4233
+ const cursorText = aiPromptTextFromRange(aiMentionRange, prompt);
4234
+ const localMention = findAiMention(cursorText);
4204
4235
  if (!localMention) return hideAiMentionMenu();
4205
- range.setStart(textNode, localMention.start);
4236
+ const range = document.createRange();
4237
+ const startBoundary = aiPromptTextBoundary(prompt, localMention.start);
4238
+ const endBoundary = aiPromptTextBoundary(prompt, cursorText.length);
4239
+ range.setStart(startBoundary.node, startBoundary.offset);
4240
+ range.setEnd(endBoundary.node, endBoundary.offset);
4206
4241
  range.deleteContents();
4207
4242
  const spacer = document.createTextNode(" ");
4208
4243
  range.insertNode(spacer);
@@ -4705,6 +4740,7 @@ function uploadWithProgress(path, options = {}, onProgress = () => {}) {
4705
4740
  request.upload.addEventListener("progress", (event) => {
4706
4741
  onProgress(event.lengthComputable && event.total > 0 ? (event.loaded / event.total) * 100 : null);
4707
4742
  });
4743
+ request.upload.addEventListener("load", () => onProgress(100));
4708
4744
  request.addEventListener("error", () => {
4709
4745
  updateSystemHealth({ status: "offline" });
4710
4746
  reject(new Error("网络连接失败"));
@@ -5365,6 +5401,56 @@ function persistentToast(message, type = "info") {
5365
5401
  return () => dismissToastElement(element);
5366
5402
  }
5367
5403
 
5404
+ function createImportProgressToast(fileName) {
5405
+ const region = $("#import-progress-region");
5406
+ const element = document.createElement("div");
5407
+ element.className = "toast import-progress-toast";
5408
+ element.setAttribute("role", "status");
5409
+ element.setAttribute("aria-atomic", "true");
5410
+ const copy = document.createElement("div");
5411
+ copy.className = "import-progress-copy";
5412
+ const title = document.createElement("strong");
5413
+ title.textContent = `正在导入“${fileName}”`;
5414
+ const status = document.createElement("span");
5415
+ status.className = "import-progress-status";
5416
+ status.textContent = "正在上传 · 0%";
5417
+ const progress = document.createElement("progress");
5418
+ progress.max = 100;
5419
+ progress.value = 0;
5420
+ progress.setAttribute("aria-label", "书籍导入上传进度");
5421
+ copy.append(title, status);
5422
+ element.append(copy, progress);
5423
+ region.append(element);
5424
+ if (typeof region.showPopover === "function" && !region.matches(":popover-open")) region.showPopover();
5425
+ let closed = false;
5426
+ return {
5427
+ update(uploadProgress) {
5428
+ if (closed) return;
5429
+ if (Number.isFinite(uploadProgress) && uploadProgress >= 100) {
5430
+ progress.removeAttribute("value");
5431
+ status.textContent = "上传完成,正在解析并写入作品…";
5432
+ return;
5433
+ }
5434
+ if (Number.isFinite(uploadProgress)) {
5435
+ const percentage = Math.max(0, Math.min(99, Math.round(uploadProgress)));
5436
+ progress.value = percentage;
5437
+ status.textContent = `正在上传 · ${percentage}%`;
5438
+ } else {
5439
+ progress.removeAttribute("value");
5440
+ status.textContent = "正在上传…";
5441
+ }
5442
+ },
5443
+ close() {
5444
+ if (closed) return;
5445
+ closed = true;
5446
+ element.remove();
5447
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
5448
+ region.hidePopover();
5449
+ }
5450
+ }
5451
+ };
5452
+ }
5453
+
5368
5454
  function restoreToastFocus(previousFocus) {
5369
5455
  if (
5370
5456
  previousFocus instanceof HTMLElement
@@ -8687,6 +8773,18 @@ function characterFavoriteIconMarkup() {
8687
8773
  return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path></svg>';
8688
8774
  }
8689
8775
 
8776
+ function pushPinIconMarkup() {
8777
+ return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 9V4h1V2H7v2h1v5c0 1.66-1.34 3-3 3v2h5.97v6l1.03 1 1.03-1v-6H20v-2c-2.21 0-4-1.79-4-4Z"></path></svg>';
8778
+ }
8779
+
8780
+ function characterPinButton(item) {
8781
+ const isPinned = item.isPinned === true;
8782
+ const canPin = canEditModule("characters");
8783
+ const action = isPinned ? "取消置顶" : "置顶";
8784
+ const title = canPin ? action : "当前账户没有角色模块写入权限";
8785
+ return `<button class="character-pin-button${isPinned ? " is-pinned" : ""}" type="button" data-character-pin="${esc(item.id)}" aria-label="${action}角色“${esc(item.name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8786
+ }
8787
+
8690
8788
  function characterFavoriteButton(item) {
8691
8789
  const isFavorite = item.isFavorite === true;
8692
8790
  const canFavorite = canEditModule("characters");
@@ -8713,6 +8811,17 @@ function recordFavoriteButton(type, item, { cardControl = false } = {}) {
8713
8811
  return `<button class="record-favorite-button${cardControl ? " is-card-control" : ""}${isFavorite ? " is-favorite" : ""}" type="button" data-record-favorite="${esc(item.id)}" data-record-favorite-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
8714
8812
  }
8715
8813
 
8814
+ function recordPinButton(type, item, { cardControl = false } = {}) {
8815
+ const config = recordFavoriteConfigs[type];
8816
+ if (!config) return "";
8817
+ const isPinned = item.isPinned === true;
8818
+ const canPin = canEditModule(config.module);
8819
+ const action = isPinned ? "取消置顶" : "置顶";
8820
+ const title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8821
+ const name = String(item[config.nameField] ?? "");
8822
+ return `<button class="record-pin-button${cardControl ? " is-card-control" : ""}${isPinned ? " is-pinned" : ""}" type="button" data-record-pin="${esc(item.id)}" data-record-pin-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8823
+ }
8824
+
8716
8825
  async function toggleRecordFavorite(type, item, render) {
8717
8826
  const config = recordFavoriteConfigs[type];
8718
8827
  if (!config) return;
@@ -8729,6 +8838,22 @@ async function toggleRecordFavorite(type, item, render) {
8729
8838
  toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
8730
8839
  }
8731
8840
 
8841
+ async function toggleRecordPin(type, item, render) {
8842
+ const config = recordFavoriteConfigs[type];
8843
+ if (!config) return;
8844
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8845
+ method: "PATCH",
8846
+ body: { isPinned: item.isPinned !== true }
8847
+ });
8848
+ await render();
8849
+ const pinButton = [...$("#module-content").querySelectorAll("[data-record-pin]")].find((button) => (
8850
+ button.dataset.recordPinType === type && button.dataset.recordPin === String(updated.id)
8851
+ ));
8852
+ pinButton?.focus({ preventScroll: true });
8853
+ const name = String(updated[config.nameField] ?? "");
8854
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8855
+ }
8856
+
8732
8857
  function bindRecordFavoriteButtons(type, items, render) {
8733
8858
  const config = recordFavoriteConfigs[type];
8734
8859
  if (!config) return;
@@ -8745,6 +8870,22 @@ function bindRecordFavoriteButtons(type, items, render) {
8745
8870
  }));
8746
8871
  }
8747
8872
 
8873
+ function bindRecordPinButtons(type, items, render) {
8874
+ const config = recordFavoriteConfigs[type];
8875
+ if (!config) return;
8876
+ $("#module-content").querySelectorAll(`[data-record-pin-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
8877
+ const item = items.find((candidate) => candidate.id === button.dataset.recordPin);
8878
+ if (!item || !canEditModule(config.module)) return;
8879
+ button.disabled = true;
8880
+ try {
8881
+ await toggleRecordPin(type, item, render);
8882
+ } catch (error) {
8883
+ button.disabled = false;
8884
+ toast(`${config.label}置顶状态更新失败:${error.message}`, "error");
8885
+ }
8886
+ }));
8887
+ }
8888
+
8748
8889
  function syncEntityDetailFavoriteButton(button, type, item) {
8749
8890
  const config = recordFavoriteConfigs[type];
8750
8891
  const visible = Boolean(config && item);
@@ -8769,6 +8910,30 @@ function syncEntityDetailFavoriteButton(button, type, item) {
8769
8910
  button.disabled = !canFavorite;
8770
8911
  }
8771
8912
 
8913
+ function syncEntityDetailPinButton(button, type, item) {
8914
+ const config = recordFavoriteConfigs[type];
8915
+ const visible = Boolean(config && item);
8916
+ button.classList.toggle("hidden", !visible);
8917
+ button.innerHTML = pushPinIconMarkup();
8918
+ if (!visible) {
8919
+ button.disabled = true;
8920
+ button.classList.remove("is-pinned");
8921
+ button.setAttribute("aria-pressed", "false");
8922
+ button.removeAttribute("aria-label");
8923
+ button.removeAttribute("title");
8924
+ return;
8925
+ }
8926
+ const isPinned = item.isPinned === true;
8927
+ const canPin = canEditModule(config.module);
8928
+ const action = isPinned ? "取消置顶" : "置顶";
8929
+ const name = String(item[config.nameField] ?? "");
8930
+ button.classList.toggle("is-pinned", isPinned);
8931
+ button.setAttribute("aria-pressed", String(isPinned));
8932
+ button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
8933
+ button.title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8934
+ button.disabled = !canPin;
8935
+ }
8936
+
8772
8937
  function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8773
8938
  syncEntityDetailFavoriteButton(button, type, getItem());
8774
8939
  button.onclick = async () => {
@@ -8793,6 +8958,30 @@ function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8793
8958
  };
8794
8959
  }
8795
8960
 
8961
+ function bindEntityDetailPinButton(button, type, getItem, onUpdated) {
8962
+ syncEntityDetailPinButton(button, type, getItem());
8963
+ button.onclick = async () => {
8964
+ const config = recordFavoriteConfigs[type];
8965
+ const item = getItem();
8966
+ if (!config || !item || !canEditModule(config.module)) return;
8967
+ button.disabled = true;
8968
+ try {
8969
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8970
+ method: "PATCH",
8971
+ body: { isPinned: item.isPinned !== true }
8972
+ });
8973
+ onUpdated(updated);
8974
+ syncEntityDetailPinButton(button, type, getItem() ?? updated);
8975
+ button.focus({ preventScroll: true });
8976
+ const name = String(updated[config.nameField] ?? "");
8977
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8978
+ } catch (error) {
8979
+ syncEntityDetailPinButton(button, type, getItem());
8980
+ toast(`${config.label}置顶状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
8981
+ }
8982
+ };
8983
+ }
8984
+
8796
8985
  function recordCardEditButton(attribute, id, label) {
8797
8986
  return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
8798
8987
  }
@@ -9131,7 +9320,7 @@ function settingRecordActions(item) {
9131
9320
  const recordAction = canEditModule("settings")
9132
9321
  ? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
9133
9322
  : recordHistoryButton("setting", item.id, item.title);
9134
- return `${recordFavoriteButton("setting", item)}${recordAction}`;
9323
+ return `${recordPinButton("setting", item)}${recordFavoriteButton("setting", item)}${recordAction}`;
9135
9324
  }
9136
9325
 
9137
9326
  function draftTypeLabel(draftType) {
@@ -9245,7 +9434,11 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
9245
9434
  }
9246
9435
  if (item && viewOnly) {
9247
9436
  const headerActions = $("#dialog-context-actions");
9248
- headerActions.innerHTML = `<button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9437
+ headerActions.innerHTML = `<button id="draft-dialog-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button><button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9438
+ bindEntityDetailPinButton($("#draft-dialog-pin"), "draft", () => draftDialogItem, (updated) => {
9439
+ draftDialogItem = updated;
9440
+ void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
9441
+ });
9249
9442
  bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
9250
9443
  draftDialogItem = updated;
9251
9444
  void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
@@ -9288,7 +9481,7 @@ async function renderDrafts(page = moduleListPages.drafts) {
9288
9481
  <details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
9289
9482
  <div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
9290
9483
  </section>`;
9291
- const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9484
+ const actions = (item) => `${recordPinButton("draft", item)}${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9292
9485
  ? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
9293
9486
  : recordHistoryButton("draft", item.id, item.title)}`;
9294
9487
  const cards = `<div class="card-grid">${pageResult.items.map((item) => `
@@ -9350,6 +9543,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
9350
9543
  moduleListPages.drafts = 1;
9351
9544
  await renderDrafts(1);
9352
9545
  });
9546
+ bindRecordPinButtons("draft", pageResult.items, async () => {
9547
+ moduleListPages.drafts = 1;
9548
+ await renderDrafts(1);
9549
+ });
9353
9550
  bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
9354
9551
  }
9355
9552
 
@@ -9417,6 +9614,10 @@ async function renderSettings(page = moduleListPages.settings) {
9417
9614
  moduleListPages.settings = 1;
9418
9615
  await renderSettings(1);
9419
9616
  });
9617
+ bindRecordPinButtons("setting", pageResult.items, async () => {
9618
+ moduleListPages.settings = 1;
9619
+ await renderSettings(1);
9620
+ });
9420
9621
  bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
9421
9622
  };
9422
9623
 
@@ -9468,6 +9669,21 @@ async function toggleCharacterFavorite(item) {
9468
9669
  toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
9469
9670
  }
9470
9671
 
9672
+ async function toggleCharacterPin(item) {
9673
+ const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/pin`, {
9674
+ method: "PATCH",
9675
+ body: { isPinned: item.isPinned !== true }
9676
+ });
9677
+ if (loadedAiReferencesWorkId === state.work?.id) {
9678
+ state.characters = upsertEntityCollection(state.characters, updated);
9679
+ renderAiRoleplayCharacterSelect();
9680
+ }
9681
+ characterListPage = 1;
9682
+ await renderCharacters(1);
9683
+ $("#module-content").querySelector(`[data-character-pin="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
9684
+ toast(updated.isPinned ? `已置顶角色“${updated.name}”` : `已取消置顶角色“${updated.name}”`);
9685
+ }
9686
+
9471
9687
  async function renderCharacters(page = characterListPage) {
9472
9688
  const hasCharacterFilters = characterFilters.raceIds.length > 0
9473
9689
  || characterFilters.organizationIds.length > 0
@@ -9490,14 +9706,14 @@ async function renderCharacters(page = characterListPage) {
9490
9706
  [state.races, state.organizations] = [races, organizations];
9491
9707
  mountModuleCount(characterPage.total);
9492
9708
  const layout = readModuleLayout();
9493
- const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9709
+ const characterActions = (item) => `${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9494
9710
  const characterLockBadge = (item) => item.lockedFields.length
9495
9711
  ? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
9496
9712
  : "";
9497
9713
  const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
9498
9714
  const details = normalizeCharacterDetails(item.attributes?.details);
9499
9715
  return `
9500
- <article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9716
+ <article class="record-card character-card preview-record-card has-card-edit has-pin-control" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9501
9717
  <div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
9502
9718
  ${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
9503
9719
  <div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
@@ -9574,6 +9790,17 @@ async function renderCharacters(page = characterListPage) {
9574
9790
  toast(`角色收藏状态更新失败:${error.message}`, "error");
9575
9791
  }
9576
9792
  }));
9793
+ $("#module-content").querySelectorAll("[data-character-pin]").forEach((button) => button.addEventListener("click", async () => {
9794
+ const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterPin);
9795
+ if (!item || !canEditModule("characters")) return;
9796
+ button.disabled = true;
9797
+ try {
9798
+ await toggleCharacterPin(item);
9799
+ } catch (error) {
9800
+ button.disabled = false;
9801
+ toast(`角色置顶状态更新失败:${error.message}`, "error");
9802
+ }
9803
+ }));
9577
9804
  const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
9578
9805
  $("#character-race-filter").addEventListener("change", async () => {
9579
9806
  characterFiltersPanelOpen = true;
@@ -9728,14 +9955,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9728
9955
  moduleListPages.organizations = pageResult.page;
9729
9956
  const layout = readModuleLayout();
9730
9957
  const canEditOrganizations = canEditModule("organizations");
9731
- const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9958
+ const organizationActions = (item, { cardControl = false } = {}) => `${recordPinButton("organization", item, { cardControl })}${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9732
9959
  ? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
9733
9960
  : recordHistoryButton("organization", item.id, item.name)}`;
9734
9961
  const organizationCardActions = (item) => canEditOrganizations
9735
9962
  ? organizationActions(item, { cardControl: true })
9736
9963
  : `<div class="card-actions">${organizationActions(item)}</div>`;
9737
9964
  const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
9738
- <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9965
+ <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control has-pin-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9739
9966
  <h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
9740
9967
  <div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
9741
9968
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
@@ -9764,6 +9991,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9764
9991
  moduleListPages.organizations = 1;
9765
9992
  await renderOrganizations(1);
9766
9993
  });
9994
+ bindRecordPinButtons("organization", pageResult.items, async () => {
9995
+ moduleListPages.organizations = 1;
9996
+ await renderOrganizations(1);
9997
+ });
9767
9998
  bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
9768
9999
  }
9769
10000
 
@@ -13476,6 +13707,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
13476
13707
  const editButton = $("#setting-editor-edit");
13477
13708
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
13478
13709
  editButton.onclick = () => openSettingEditor(settingEditorItem);
13710
+ bindEntityDetailPinButton($("#setting-editor-pin"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13711
+ settingEditorItem = updated;
13712
+ state.settings = upsertEntityCollection(state.settings, updated);
13713
+ });
13479
13714
  bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13480
13715
  settingEditorItem = updated;
13481
13716
  state.settings = upsertEntityCollection(state.settings, updated);
@@ -14712,6 +14947,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
14712
14947
  const editButton = $("#character-editor-edit");
14713
14948
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
14714
14949
  editButton.onclick = () => void openCharacterEditor(characterEditorItem);
14950
+ bindEntityDetailPinButton($("#character-editor-pin"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14951
+ characterEditorItem = updated;
14952
+ if (loadedAiReferencesWorkId === state.work?.id) {
14953
+ state.characters = upsertEntityCollection(state.characters, updated);
14954
+ renderAiRoleplayCharacterSelect();
14955
+ }
14956
+ });
14715
14957
  bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14716
14958
  characterEditorItem = updated;
14717
14959
  if (loadedAiReferencesWorkId === state.work?.id) {
@@ -14905,6 +15147,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
14905
15147
  editButton.textContent = `编辑${label}`;
14906
15148
  editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
14907
15149
  editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
15150
+ bindEntityDetailPinButton($("#knowledge-editor-pin"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
15151
+ knowledgeEditorItem = updated;
15152
+ state.organizations = upsertEntityCollection(state.organizations, updated);
15153
+ });
14908
15154
  bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
14909
15155
  knowledgeEditorItem = updated;
14910
15156
  state.organizations = upsertEntityCollection(state.organizations, updated);
@@ -18663,15 +18909,22 @@ $("#import-file").addEventListener("change", async (event) => {
18663
18909
  body.append("file", file);
18664
18910
  body.append("mode", mode);
18665
18911
  body.append("expectedVersionNo", String(state.work.versionNo));
18912
+ const importProgress = createImportProgressToast(file.name);
18666
18913
  try {
18667
- const result = await api(`/api/works/${state.work.id}/import`, { method: "POST", body });
18914
+ const result = await uploadWithProgress(
18915
+ `/api/works/${state.work.id}/import`,
18916
+ { method: "POST", body },
18917
+ (progress) => importProgress.update(progress)
18918
+ );
18668
18919
  setSaveState(mode === "append" ? "已追加" : "已覆盖");
18669
18920
  state.work = result.tree;
18670
18921
  renderTree();
18671
18922
  const completion = mode === "append" ? "正文追加完成" : "正文覆盖完成";
18672
- toast(result.warnings.length ? `${completion}:${result.warnings.join(";")}` : completion);
18673
18923
  if (result.firstImportedChapterId) await selectChapter(result.firstImportedChapterId);
18924
+ importProgress.close();
18925
+ toast(result.warnings.length ? `${completion}:${result.warnings.join(";")}` : completion);
18674
18926
  } catch (error) {
18927
+ importProgress.close();
18675
18928
  toast(error.message, "error");
18676
18929
  if (state.dirty) scheduleChapterAutoSave();
18677
18930
  }
@@ -18686,11 +18939,18 @@ $("#new-import-file").addEventListener("change", async (event) => {
18686
18939
  body.append("title", metadata.title ?? "");
18687
18940
  body.append("author", metadata.author ?? "");
18688
18941
  body.append("description", metadata.description ?? "");
18942
+ const importProgress = createImportProgressToast(file.name);
18689
18943
  try {
18690
- const result = await api("/api/works/import", { method: "POST", body });
18691
- toast(result.warnings.length ? `作品已导入:${result.warnings.join("")}` : "作品已导入");
18944
+ const result = await uploadWithProgress(
18945
+ "/api/works/import",
18946
+ { method: "POST", body },
18947
+ (progress) => importProgress.update(progress)
18948
+ );
18692
18949
  await loadWorks(result.work.id);
18950
+ importProgress.close();
18951
+ toast(result.warnings.length ? `作品已导入:${result.warnings.join(";")}` : "作品已导入");
18693
18952
  } catch (error) {
18953
+ importProgress.close();
18694
18954
  toast(error.message, "error");
18695
18955
  } finally {
18696
18956
  state.pendingImportMeta = null;
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -476,6 +476,7 @@
476
476
  </section>
477
477
 
478
478
  <div id="toast-region" class="toast-region" data-position="bottom-right" aria-live="polite" popover="manual"></div>
479
+ <div id="import-progress-region" class="toast-region import-progress-region" data-position="bottom-right" aria-live="polite" popover="manual"></div>
479
480
  <div id="chapter-type-menu" class="chapter-type-menu hidden" role="menu" aria-label="章节操作" data-testid="chapter-type-menu">
480
481
  <strong>标记章节</strong>
481
482
  <div>
@@ -658,6 +659,7 @@
658
659
  </div>
659
660
  </div>
660
661
  <div class="entity-editor-mode-actions">
662
+ <button id="setting-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
661
663
  <button id="setting-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
662
664
  <button id="setting-editor-edit" class="ghost-button hidden" type="button">编辑设定</button>
663
665
  <button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button>
@@ -685,6 +687,7 @@
685
687
  <div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span><span id="character-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
686
688
  </div>
687
689
  <div class="character-editor-header-actions">
690
+ <button id="character-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
688
691
  <button id="character-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
689
692
  <button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
690
693
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
@@ -721,6 +724,7 @@
721
724
  </div>
722
725
  <div class="character-editor-header-actions">
723
726
  <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
727
+ <button id="knowledge-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
724
728
  <button id="knowledge-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
725
729
  <button id="knowledge-editor-edit" class="primary-button hidden" type="button">编辑档案</button>
726
730
  <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
@@ -1258,6 +1262,6 @@
1258
1262
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1259
1263
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1260
1264
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1261
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1"></script>
1265
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1"></script>
1262
1266
  </body>
1263
1267
  </html>