@musnows/scriverse 0.5.12 → 0.6.0
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-protocol.js +6 -1
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai.js +336 -21
- package/dist/ai.js.map +1 -1
- package/dist/app.js +14 -4
- package/dist/app.js.map +1 -1
- package/dist/database.js +17 -0
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +32 -0
- package/dist/public/app.js +180 -38
- package/dist/public/index.html +7 -3
- package/dist/public/styles.css +45 -6
- package/dist/store.js +40 -7
- package/dist/store.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -11,7 +11,7 @@ import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.j
|
|
|
11
11
|
import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
|
|
12
12
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
13
13
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
14
|
-
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=
|
|
14
|
+
import { formatAiContextUsageTooltip, normalizeAiContextTokenDistribution } from "/ai-context-meter.js?v=20260730-token-distribution-v2";
|
|
15
15
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
16
16
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
17
17
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
@@ -86,6 +86,12 @@ function normalizePageSizes(value) {
|
|
|
86
86
|
]));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function isSelectableModel(model) {
|
|
90
|
+
return Boolean(model?.enabled)
|
|
91
|
+
&& model?.providerStatus === "enabled"
|
|
92
|
+
&& model?.providerConnectionStatus === "success";
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
const state = {
|
|
90
96
|
user: null,
|
|
91
97
|
csrfToken: null,
|
|
@@ -1612,6 +1618,14 @@ function renderAiConversationHistory() {
|
|
|
1612
1618
|
}
|
|
1613
1619
|
}
|
|
1614
1620
|
|
|
1621
|
+
function applyAiConversationTitle(title) {
|
|
1622
|
+
if (!title || !state.aiConversationId) return;
|
|
1623
|
+
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1624
|
+
if (current) current.title = title;
|
|
1625
|
+
$("#ai-conversation-title").textContent = title;
|
|
1626
|
+
renderAiConversationHistory();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1615
1629
|
async function loadAiConversations(openLatest = true) {
|
|
1616
1630
|
const workId = state.work?.id;
|
|
1617
1631
|
if (!workId) return;
|
|
@@ -2149,9 +2163,7 @@ async function api(path, options = {}) {
|
|
|
2149
2163
|
moduleRequestCache.clear();
|
|
2150
2164
|
showAuth(false);
|
|
2151
2165
|
}
|
|
2152
|
-
|
|
2153
|
-
error.code = payload.error?.code;
|
|
2154
|
-
throw error;
|
|
2166
|
+
throw createClientError(payload.error, `请求失败:${response.status}`, response.status);
|
|
2155
2167
|
}
|
|
2156
2168
|
if (response.status === 204) {
|
|
2157
2169
|
invalidateModuleRequestsAfterMutation(path, method);
|
|
@@ -2162,6 +2174,32 @@ async function api(path, options = {}) {
|
|
|
2162
2174
|
return payload.data;
|
|
2163
2175
|
}
|
|
2164
2176
|
|
|
2177
|
+
function createClientError(payload, fallbackMessage, fallbackStatus = null) {
|
|
2178
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
2179
|
+
const error = new Error(typeof source.message === "string" ? source.message : fallbackMessage);
|
|
2180
|
+
error.code = typeof source.code === "string" ? source.code : undefined;
|
|
2181
|
+
error.status = Number.isInteger(source.status) ? source.status : fallbackStatus;
|
|
2182
|
+
error.details = source.details;
|
|
2183
|
+
error.failure = typeof source.failure === "string" ? source.failure : undefined;
|
|
2184
|
+
error.callId = typeof source.callId === "string" ? source.callId : undefined;
|
|
2185
|
+
return error;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function formatAiFailureMessage(error) {
|
|
2189
|
+
const message = error instanceof Error ? error.message : String(error ?? "未知错误");
|
|
2190
|
+
const lines = [`调用失败:${message}`];
|
|
2191
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
2192
|
+
const status = Number.isInteger(error?.status) ? error.status : null;
|
|
2193
|
+
const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
|
|
2194
|
+
const failure = typeof error?.failure === "string" ? error.failure : typeof details.failure === "string" ? details.failure : "";
|
|
2195
|
+
const callId = typeof error?.callId === "string" ? error.callId : typeof details.callId === "string" ? details.callId : "";
|
|
2196
|
+
if (code) lines.push(`错误码:${code}`);
|
|
2197
|
+
if (status) lines.push(`服务端状态:HTTP ${status}`);
|
|
2198
|
+
if (failure && failure !== message) lines.push(`详细原因:${failure}`);
|
|
2199
|
+
if (callId) lines.push(`调用 ID:${callId}`);
|
|
2200
|
+
return lines.join("\n\n");
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2165
2203
|
async function apiPage(path, page = 1, limit = 30) {
|
|
2166
2204
|
const separator = path.includes("?") ? "&" : "?";
|
|
2167
2205
|
const result = await api(`${path}${separator}page=${page}&limit=${limit}`);
|
|
@@ -5376,12 +5414,7 @@ async function rerunAnalysisTaskWithModel(task, button) {
|
|
|
5376
5414
|
try {
|
|
5377
5415
|
const models = await api(`/api/works/${encodeURIComponent(task.workId)}/models`);
|
|
5378
5416
|
const currentModelId = String(task.model?.id ?? "");
|
|
5379
|
-
const availableModels = models.filter((model) =>
|
|
5380
|
-
model.id !== currentModelId
|
|
5381
|
-
&& model.enabled
|
|
5382
|
-
&& model.providerStatus === "enabled"
|
|
5383
|
-
&& model.providerConnectionStatus === "success"
|
|
5384
|
-
);
|
|
5417
|
+
const availableModels = models.filter((model) => model.id !== currentModelId && isSelectableModel(model));
|
|
5385
5418
|
if (!availableModels.length) throw new Error("当前没有其他可用模型,请先配置并测试模型");
|
|
5386
5419
|
$("#form-dialog").close();
|
|
5387
5420
|
const currentModelLabel = task.model?.displayName || "运行时默认模型";
|
|
@@ -5892,11 +5925,28 @@ function openTaskDetailDialog(task, trace) {
|
|
|
5892
5925
|
}
|
|
5893
5926
|
|
|
5894
5927
|
function renderProviderCards(providers, models) {
|
|
5895
|
-
return providers.length ? `<div class="card-grid provider-card-grid">${providers.map((provider) =>
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5928
|
+
return providers.length ? `<div class="card-grid provider-card-grid">${providers.map((provider) => {
|
|
5929
|
+
const providerModels = models.filter((model) => model.providerId === provider.id);
|
|
5930
|
+
const providerStatusClass = provider.status === "disabled" ? "is-disabled" : provider.status === "error" ? "is-error" : "is-enabled";
|
|
5931
|
+
const disabledNotice = provider.status === "disabled"
|
|
5932
|
+
? `<div class="provider-disabled-notice" role="status"><strong>已停用</strong><span>不会出现在新任务的模型列表中,历史任务仍可查看。</span></div>`
|
|
5933
|
+
: "";
|
|
5934
|
+
return `
|
|
5935
|
+
<article class="record-card provider-card ${provider.status === "disabled" ? "is-disabled" : ""}"><div class="provider-card-meta"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><span class="provider-status-badge ${providerStatusClass}">${esc(providerStatusLabel(provider.status))}</span></div><h3>${esc(provider.name)}</h3>
|
|
5936
|
+
${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
|
|
5937
|
+
<div class="provider-models">${providerModels.map((model) => {
|
|
5938
|
+
const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
|
|
5939
|
+
const modelStatus = !model.enabled
|
|
5940
|
+
? `<span class="model-status-badge is-disabled">模型已停用</span>`
|
|
5941
|
+
: provider.status !== "enabled"
|
|
5942
|
+
? `<span class="model-status-badge is-disabled">供应商已停用</span>`
|
|
5943
|
+
: provider.connectionStatus !== "success"
|
|
5944
|
+
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
5945
|
+
: "";
|
|
5946
|
+
return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}<button class="ghost-button model-test-button" type="button" data-test-model="${esc(model.id)}" aria-label="测试模型 ${esc(model.displayName)}">测试连接</button></div>`;
|
|
5947
|
+
}).join("")}</div>
|
|
5948
|
+
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button><button data-test-provider="${esc(provider.id)}" ${providerModels.length ? "" : "disabled aria-disabled=\"true\" title=\"请先添加模型\""}>测试连接</button><button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`;
|
|
5949
|
+
}).join("")}</div>`
|
|
5900
5950
|
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 或 Anthropic 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
5901
5951
|
}
|
|
5902
5952
|
|
|
@@ -5909,28 +5959,50 @@ function bindPlatformProviderActions(host, providers, models) {
|
|
|
5909
5959
|
await renderPlatformAiConfig();
|
|
5910
5960
|
await loadModels();
|
|
5911
5961
|
}));
|
|
5962
|
+
host.querySelectorAll("[data-test-model]").forEach((button) => button.addEventListener("click", async () => {
|
|
5963
|
+
button.disabled = true;
|
|
5964
|
+
button.textContent = "测试中";
|
|
5965
|
+
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
5966
|
+
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
5967
|
+
await renderPlatformAiConfig();
|
|
5968
|
+
await loadModels();
|
|
5969
|
+
}));
|
|
5912
5970
|
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel)));
|
|
5913
5971
|
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(undefined, models.find((model) => model.id === button.dataset.editModel))));
|
|
5914
5972
|
host.querySelectorAll("[data-edit-provider]").forEach((button) => button.addEventListener("click", () => openProviderDialog(providers.find((provider) => provider.id === button.dataset.editProvider))));
|
|
5915
5973
|
}
|
|
5916
5974
|
|
|
5917
|
-
function renderTaskDefaults(models, providers, taskDefaults) {
|
|
5975
|
+
function renderTaskDefaults(models, providers, taskDefaults, settings) {
|
|
5918
5976
|
const providerById = new Map(providers.map((provider) => [provider.id, provider]));
|
|
5919
5977
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
5920
|
-
|
|
5978
|
+
const availableModels = models.filter((model) => isSelectableModel(model));
|
|
5979
|
+
const availableModelIds = new Set(availableModels.map((model) => model.id));
|
|
5980
|
+
const currentDefaultModels = taskDefaults
|
|
5981
|
+
.map((item) => item.model)
|
|
5982
|
+
.filter((model) => model && !availableModelIds.has(model.id));
|
|
5983
|
+
const optionModels = [...availableModels, ...currentDefaultModels];
|
|
5984
|
+
return optionModels.length ? `<section class="config-section">
|
|
5921
5985
|
<div class="config-section-header"><div><h2>本书任务默认模型</h2><p>选择平台模型作为当前作品的默认模型;所有请求都会携带最大输出令牌数,默认值为 32000。</p></div></div>
|
|
5922
|
-
<table class="table-list"><thead><tr><th>任务能力</th><th>默认模型</th></tr></thead><tbody
|
|
5986
|
+
<table class="table-list"><thead><tr><th>任务能力</th><th>默认模型</th></tr></thead><tbody><tr><td>创作助手对话标题生成</td><td><select class="default-model-select" data-title-generation-default aria-label="创作助手对话标题生成">
|
|
5987
|
+
<option value="" ${settings.titleGenerationModelId ? "" : "selected"}>使用提示词前 15 个字</option>
|
|
5988
|
+
${models.map((model) => {
|
|
5989
|
+
const provider = providerById.get(model.providerId);
|
|
5990
|
+
const available = model.enabled && provider?.status === "enabled" && provider?.connectionStatus === "success";
|
|
5991
|
+
return `<option value="${esc(model.id)}" ${model.id === settings.titleGenerationModelId ? "selected" : ""} ${available || model.id === settings.titleGenerationModelId ? "" : "disabled"}>${esc(modelOptionLabel({ ...model, providerName: model.providerName || provider?.name }))}</option>`;
|
|
5992
|
+
}).join("")}
|
|
5993
|
+
</select></td></tr>${taskTypeLabels.map(([taskType, label]) => {
|
|
5923
5994
|
const currentModelId = defaultModelByTask.get(taskType) ?? "";
|
|
5924
5995
|
return `<tr><td>${esc(label)}</td><td><select class="default-model-select" data-task-default="${esc(taskType)}">
|
|
5925
5996
|
<option value="" disabled ${currentModelId ? "" : "selected"}>请选择模型</option>
|
|
5926
|
-
${
|
|
5997
|
+
${optionModels.map((model) => {
|
|
5927
5998
|
const provider = providerById.get(model.providerId);
|
|
5928
|
-
const available = model.
|
|
5929
|
-
|
|
5999
|
+
const available = isSelectableModel({ ...model, providerStatus: model.providerStatus ?? provider?.status, providerConnectionStatus: model.providerConnectionStatus ?? provider?.connectionStatus });
|
|
6000
|
+
const unavailableLabel = !model.enabled ? "模型已停用 · " : provider?.status !== "enabled" ? "供应商已停用 · " : "连接不可用 · ";
|
|
6001
|
+
return `<option value="${esc(model.id)}" ${model.id === currentModelId ? "selected" : ""} ${available ? "" : "disabled"}>${esc(`${available ? "" : unavailableLabel}${modelOptionLabel({ ...model, providerName: model.providerName || provider?.name })}`)}</option>`;
|
|
5930
6002
|
}).join("")}
|
|
5931
6003
|
</select></td></tr>`;
|
|
5932
6004
|
}).join("")}</tbody></table>
|
|
5933
|
-
</section>` : emptyModule("
|
|
6005
|
+
</section>` : emptyModule("尚未配置可用模型", "请先启用并测试供应商模型,已停用模型不会出现在新任务选择中。");
|
|
5934
6006
|
}
|
|
5935
6007
|
|
|
5936
6008
|
const relationshipIndexStatusLabels = Object.freeze({
|
|
@@ -6324,7 +6396,7 @@ async function renderBookAiSettings() {
|
|
|
6324
6396
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
6325
6397
|
title: "本书 Token 用量",
|
|
6326
6398
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
6327
|
-
})}</section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults)}`;
|
|
6399
|
+
})}</section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>对话 context 使用独立预算。达到该百分比阈值时先提醒;继续发送会对较早消息执行 compact,压缩上下文占用,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
|
|
6328
6400
|
scrollUsageCalendarsToLatest(host);
|
|
6329
6401
|
host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
|
|
6330
6402
|
"beforebegin",
|
|
@@ -6461,6 +6533,18 @@ async function renderBookAiSettings() {
|
|
|
6461
6533
|
button.disabled = false;
|
|
6462
6534
|
}
|
|
6463
6535
|
});
|
|
6536
|
+
host.querySelector("[data-title-generation-default]")?.addEventListener("change", async (event) => {
|
|
6537
|
+
const select = event.currentTarget;
|
|
6538
|
+
select.disabled = true;
|
|
6539
|
+
try {
|
|
6540
|
+
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { titleGenerationModelId: select.value } });
|
|
6541
|
+
toast("创作助手对话标题生成模型已更新");
|
|
6542
|
+
} catch (error) {
|
|
6543
|
+
toast(error.message, "error");
|
|
6544
|
+
}
|
|
6545
|
+
await renderBookAiSettings();
|
|
6546
|
+
await loadModels();
|
|
6547
|
+
});
|
|
6464
6548
|
host.querySelectorAll("[data-task-default]").forEach((select) => select.addEventListener("change", async () => {
|
|
6465
6549
|
select.disabled = true;
|
|
6466
6550
|
try {
|
|
@@ -6480,11 +6564,11 @@ async function loadModels() {
|
|
|
6480
6564
|
const generation = workScopedUiGeneration;
|
|
6481
6565
|
const models = await api(`/api/works/${workId}/models`);
|
|
6482
6566
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
6483
|
-
state.models = models;
|
|
6567
|
+
state.models = models.filter((model) => isSelectableModel(model));
|
|
6484
6568
|
loadedAiModelsWorkId = workId;
|
|
6485
6569
|
const select = $("#ai-model");
|
|
6486
6570
|
select.innerHTML = state.models.length
|
|
6487
|
-
? state.models.map((model) => `<option value="${esc(model.id)}"
|
|
6571
|
+
? state.models.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")
|
|
6488
6572
|
: '<option value="">请先配置模型</option>';
|
|
6489
6573
|
scheduleAiContextUsage();
|
|
6490
6574
|
}
|
|
@@ -6532,9 +6616,50 @@ function currentAiRequestScope() {
|
|
|
6532
6616
|
return { taskType, scope, selection };
|
|
6533
6617
|
}
|
|
6534
6618
|
|
|
6619
|
+
function renderAiContextDistribution(usage) {
|
|
6620
|
+
const popover = $("#ai-context-popover");
|
|
6621
|
+
const host = $("#ai-context-distribution");
|
|
6622
|
+
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
6623
|
+
const contextWindow = distribution.contextWindow.toLocaleString("zh-CN");
|
|
6624
|
+
$("#ai-context-popover-description").textContent = usage
|
|
6625
|
+
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok`
|
|
6626
|
+
: "选择可用模型后显示当前上下文用量";
|
|
6627
|
+
host.replaceChildren(...distribution.items.map((item) => {
|
|
6628
|
+
const row = document.createElement("div");
|
|
6629
|
+
row.className = "ai-context-distribution-row";
|
|
6630
|
+
row.dataset.key = item.key;
|
|
6631
|
+
row.setAttribute("role", "listitem");
|
|
6632
|
+
row.setAttribute("aria-label", `${item.label}:${item.tokens.toLocaleString("zh-CN")} tok,占 ${item.percent}%`);
|
|
6633
|
+
|
|
6634
|
+
const label = document.createElement("div");
|
|
6635
|
+
label.className = "ai-context-distribution-label";
|
|
6636
|
+
const title = document.createElement("span");
|
|
6637
|
+
title.textContent = item.label;
|
|
6638
|
+
if (item.key === "context") {
|
|
6639
|
+
const description = document.createElement("small");
|
|
6640
|
+
description.textContent = "用户和 agent 的交互";
|
|
6641
|
+
title.append(" ", description);
|
|
6642
|
+
}
|
|
6643
|
+
const value = document.createElement("strong");
|
|
6644
|
+
value.textContent = `${item.tokens.toLocaleString("zh-CN")} tok · ${item.percent}%`;
|
|
6645
|
+
label.append(title, value);
|
|
6646
|
+
|
|
6647
|
+
const track = document.createElement("div");
|
|
6648
|
+
track.className = "ai-context-distribution-track";
|
|
6649
|
+
track.setAttribute("aria-hidden", "true");
|
|
6650
|
+
const bar = document.createElement("span");
|
|
6651
|
+
bar.style.setProperty("--distribution-percent", String(item.percent));
|
|
6652
|
+
track.append(bar);
|
|
6653
|
+
row.append(label, track);
|
|
6654
|
+
return row;
|
|
6655
|
+
}));
|
|
6656
|
+
popover.dataset.hasUsage = String(Boolean(usage));
|
|
6657
|
+
}
|
|
6658
|
+
|
|
6535
6659
|
function setAiContextMeter(usage) {
|
|
6536
6660
|
const meter = $("#ai-context-meter");
|
|
6537
6661
|
const value = meter.querySelector("b");
|
|
6662
|
+
renderAiContextDistribution(usage);
|
|
6538
6663
|
if (!usage) {
|
|
6539
6664
|
meter.classList.add("is-empty");
|
|
6540
6665
|
meter.classList.remove("is-warning", "is-danger");
|
|
@@ -6556,6 +6681,14 @@ function setAiContextMeter(usage) {
|
|
|
6556
6681
|
meter.setAttribute("aria-label", `当前上下文用量:${tooltip}`);
|
|
6557
6682
|
}
|
|
6558
6683
|
|
|
6684
|
+
function setAiContextDistributionVisible(visible) {
|
|
6685
|
+
const meter = $("#ai-context-meter");
|
|
6686
|
+
const popover = $("#ai-context-popover");
|
|
6687
|
+
popover.classList.toggle("hidden", !visible);
|
|
6688
|
+
meter.setAttribute("aria-expanded", String(visible));
|
|
6689
|
+
if (!visible && document.activeElement === $("#ai-context-popover-close")) meter.focus();
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6559
6692
|
function showAiContextWarning(usage = null) {
|
|
6560
6693
|
const percent = Math.max(0, Math.round(Number(usage?.conversationUsagePercent) || 0));
|
|
6561
6694
|
const threshold = Math.max(50, Math.min(90, Number(usage?.compactThreshold) || 85));
|
|
@@ -8567,11 +8700,7 @@ async function openTaskDialog() {
|
|
|
8567
8700
|
return;
|
|
8568
8701
|
}
|
|
8569
8702
|
const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
|
|
8570
|
-
const availableTaskModels = taskModels.filter((model) =>
|
|
8571
|
-
model.enabled
|
|
8572
|
-
&& model.providerStatus === "enabled"
|
|
8573
|
-
&& model.providerConnectionStatus === "success"
|
|
8574
|
-
);
|
|
8703
|
+
const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
|
|
8575
8704
|
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
8576
8705
|
const relationshipCharacterPicker = `<div class="form-field relationship-character-field">
|
|
8577
8706
|
<span id="relationship-character-label">被分析角色(可多选)</span>
|
|
@@ -8877,8 +9006,8 @@ async function openTaskDialog() {
|
|
|
8877
9006
|
function openProviderDialog(item) {
|
|
8878
9007
|
const protocol = item?.protocol ?? "openai-chat-completions";
|
|
8879
9008
|
const defaultBaseUrl = protocol === "anthropic-messages" ? "https://api.anthropic.com" : "https://api.openai.com/v1";
|
|
8880
|
-
openDialog(item ? "编辑 AI 供应商" : "新建 AI 供应商", field("name", "显示名称", "text", item?.name) + field("protocol", "接口协议", "select", protocol, [["openai-chat-completions", "OpenAI Chat Completions"], ["anthropic-messages", "Anthropic Messages"]]) + field("baseUrl", "API 基础地址", "url", item?.baseUrl ?? defaultBaseUrl) + field("apiKey", item ? "替换 API 密钥(留空则不变)" : "API 密钥", "password") + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10) + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10) + field("
|
|
8881
|
-
const body = { name: form.get("name"), protocol: form.get("protocol"), baseUrl: form.get("baseUrl"), concurrencyLimit: Number(form.get("concurrencyLimit")), rpmLimit: Number(form.get("rpmLimit")),
|
|
9009
|
+
openDialog(item ? "编辑 AI 供应商" : "新建 AI 供应商", field("name", "显示名称", "text", item?.name) + field("protocol", "接口协议", "select", protocol, [["openai-chat-completions", "OpenAI Chat Completions"], ["anthropic-messages", "Anthropic Messages"]]) + field("baseUrl", "API 基础地址", "url", item?.baseUrl ?? defaultBaseUrl) + field("apiKey", item ? "替换 API 密钥(留空则不变)" : "API 密钥", "password") + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10) + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10) + field("note", "用途备注", "textarea", item?.note) + field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true), async (form) => {
|
|
9010
|
+
const body = { name: form.get("name"), protocol: form.get("protocol"), baseUrl: form.get("baseUrl"), concurrencyLimit: Number(form.get("concurrencyLimit")), rpmLimit: Number(form.get("rpmLimit")), note: form.get("note"), status: form.get("enabled") === "on" ? "enabled" : "disabled" };
|
|
8882
9011
|
if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
|
|
8883
9012
|
await api(item ? `/api/providers/${item.id}` : "/api/platform/ai/providers", { method: item ? "PATCH" : "POST", body });
|
|
8884
9013
|
await renderPlatformAiConfig();
|
|
@@ -8972,6 +9101,7 @@ async function sendAi() {
|
|
|
8972
9101
|
assistantMessage = streamed.message;
|
|
8973
9102
|
assistantMetadata = streamed.metadata;
|
|
8974
9103
|
persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
|
|
9104
|
+
applyAiConversationTitle(streamed.conversationTitle);
|
|
8975
9105
|
} else {
|
|
8976
9106
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
8977
9107
|
assistantContent = suggestion.content;
|
|
@@ -8993,7 +9123,7 @@ async function sendAi() {
|
|
|
8993
9123
|
toast(`AI 回复已生成,但历史记录保存失败:${error.message}`, "error");
|
|
8994
9124
|
}
|
|
8995
9125
|
} catch (error) {
|
|
8996
|
-
const failureMessage =
|
|
9126
|
+
const failureMessage = formatAiFailureMessage(error);
|
|
8997
9127
|
let persistedFailureMessage = null;
|
|
8998
9128
|
try { persistedFailureMessage = await persistAiConversationMessage("assistant", failureMessage); } catch { /* 主请求错误已显示,历史记录保存失败不覆盖原始错误 */ }
|
|
8999
9129
|
appendMessage("assistant", failureMessage, [], persistedFailureMessage?.createdAt, {}, persistedFailureMessage?.id);
|
|
@@ -9029,6 +9159,7 @@ async function streamChat(body) {
|
|
|
9029
9159
|
let processSteps = [];
|
|
9030
9160
|
let persistedMessageId = null;
|
|
9031
9161
|
let persistedMessageCreatedAt = null;
|
|
9162
|
+
let conversationTitle = null;
|
|
9032
9163
|
let finalAnswerStarted = false;
|
|
9033
9164
|
const processStartedAt = Date.now();
|
|
9034
9165
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
@@ -9040,7 +9171,7 @@ async function streamChat(body) {
|
|
|
9040
9171
|
});
|
|
9041
9172
|
if (!response.ok || !response.body) {
|
|
9042
9173
|
const payload = await response.json().catch(() => ({ error: { message: `请求失败:${response.status}` } }));
|
|
9043
|
-
throw
|
|
9174
|
+
throw createClientError(payload.error, `请求失败:${response.status}`, response.status);
|
|
9044
9175
|
}
|
|
9045
9176
|
const reader = response.body.getReader();
|
|
9046
9177
|
const decoder = new TextDecoder();
|
|
@@ -9085,6 +9216,7 @@ async function streamChat(body) {
|
|
|
9085
9216
|
} else if (eventName === "complete") {
|
|
9086
9217
|
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
9087
9218
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
9219
|
+
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
9088
9220
|
await typewriter.finish();
|
|
9089
9221
|
message.classList.remove("is-streaming");
|
|
9090
9222
|
content.setAttribute("aria-busy", "false");
|
|
@@ -9098,7 +9230,7 @@ async function streamChat(body) {
|
|
|
9098
9230
|
attachAssistantCopyAction(message, streamedText);
|
|
9099
9231
|
scrollAiFeedToBottom();
|
|
9100
9232
|
} else if (eventName === "error") {
|
|
9101
|
-
streamError =
|
|
9233
|
+
streamError = createClientError(payload, "AI 流式调用失败", response.status);
|
|
9102
9234
|
}
|
|
9103
9235
|
};
|
|
9104
9236
|
while (true) {
|
|
@@ -9112,7 +9244,7 @@ async function streamChat(body) {
|
|
|
9112
9244
|
if (buffer.trim()) await consume(buffer);
|
|
9113
9245
|
await typewriter.finish();
|
|
9114
9246
|
if (streamError) throw streamError;
|
|
9115
|
-
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt };
|
|
9247
|
+
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle };
|
|
9116
9248
|
} catch (error) {
|
|
9117
9249
|
typewriter.reveal();
|
|
9118
9250
|
message.classList.remove("is-streaming");
|
|
@@ -9127,8 +9259,12 @@ async function streamChat(body) {
|
|
|
9127
9259
|
|
|
9128
9260
|
function appendMessage(role, text, citations = [], createdAt = null, metadata = {}, messageId = null) {
|
|
9129
9261
|
const message = document.createElement("div");
|
|
9130
|
-
|
|
9131
|
-
message.
|
|
9262
|
+
const isFailure = role === "assistant" && text.startsWith("调用失败:");
|
|
9263
|
+
message.className = `${role === "user" ? "user-message" : "assistant-message"}${isFailure ? " is-error" : ""}`;
|
|
9264
|
+
const messageBody = isFailure
|
|
9265
|
+
? `<p class="ai-error-text">${esc(text)}</p>`
|
|
9266
|
+
: renderMarkdown(text);
|
|
9267
|
+
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
9132
9268
|
attachMessageHeading(message, role === "user" ? "作者" : "助手", createdAt ?? undefined);
|
|
9133
9269
|
if (citations.length) {
|
|
9134
9270
|
const references = document.createElement("div");
|
|
@@ -10487,6 +10623,7 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
10487
10623
|
if (!event.target.closest("#line-citation-menu")) closeLineCitationMenu();
|
|
10488
10624
|
if (!event.target.closest("#markdown-table-menu")) closeMarkdownTableMenu();
|
|
10489
10625
|
if (!event.target.closest(".prompt-composer")) hideAiMentionMenu();
|
|
10626
|
+
if (!event.target.closest("#ai-context-meter") && !event.target.closest("#ai-context-popover")) setAiContextDistributionVisible(false);
|
|
10490
10627
|
if (!event.target.closest("#account-button") && !event.target.closest("#account-menu")) {
|
|
10491
10628
|
$("#account-menu").classList.add("hidden");
|
|
10492
10629
|
$("#account-button").setAttribute("aria-expanded", "false");
|
|
@@ -10508,6 +10645,7 @@ document.addEventListener("keydown", (event) => {
|
|
|
10508
10645
|
closeLineCitationMenu();
|
|
10509
10646
|
closeMarkdownTableMenu(true);
|
|
10510
10647
|
hideAiMentionMenu();
|
|
10648
|
+
setAiContextDistributionVisible(false);
|
|
10511
10649
|
}
|
|
10512
10650
|
});
|
|
10513
10651
|
document.addEventListener("keydown", (event) => {
|
|
@@ -10517,6 +10655,10 @@ document.addEventListener("keydown", (event) => {
|
|
|
10517
10655
|
if (event.repeat) return;
|
|
10518
10656
|
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
10519
10657
|
}, { capture: true });
|
|
10658
|
+
$("#ai-context-meter").addEventListener("click", () => {
|
|
10659
|
+
setAiContextDistributionVisible($("#ai-context-popover").classList.contains("hidden"));
|
|
10660
|
+
});
|
|
10661
|
+
$("#ai-context-popover-close").addEventListener("click", () => setAiContextDistributionVisible(false));
|
|
10520
10662
|
$("#ai-send").addEventListener("click", sendAi);
|
|
10521
10663
|
$("#ai-new-conversation").addEventListener("click", async () => {
|
|
10522
10664
|
const button = $("#ai-new-conversation");
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260730-drafts-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260730-ai-error-drafts-filter-align-model-availability-token-distribution-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -309,7 +309,11 @@
|
|
|
309
309
|
<div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
|
|
310
310
|
<div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定或章节"></div>
|
|
311
311
|
<div class="prompt-composer-actions">
|
|
312
|
-
<
|
|
312
|
+
<button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"><b>—</b></button>
|
|
313
|
+
<section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
|
|
314
|
+
<header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
|
|
315
|
+
<div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
|
|
316
|
+
</section>
|
|
313
317
|
<button id="ai-send" class="ai-send-button" type="button" aria-label="发送消息">发送</button>
|
|
314
318
|
</div>
|
|
315
319
|
</div>
|
|
@@ -884,6 +888,6 @@
|
|
|
884
888
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
885
889
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
886
890
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
887
|
-
<script type="module" src="/app.js?v=20260730-
|
|
891
|
+
<script type="module" src="/app.js?v=20260730-ai-error-model-availability-token-distribution-conversation-title-v1"></script>
|
|
888
892
|
</body>
|
|
889
893
|
</html>
|
package/dist/public/styles.css
CHANGED
|
@@ -1275,7 +1275,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1275
1275
|
.character-filter-toolbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; min-height: 38px; }
|
|
1276
1276
|
.character-filter-result-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
|
|
1277
1277
|
.character-filter-toolbar-actions button:disabled { cursor: default; opacity: .45; }
|
|
1278
|
-
.draft-filter-toolbar { display: flex; align-items:
|
|
1278
|
+
.draft-filter-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; padding: 12px 14px; border: 1px solid var(--line); background: var(--surface-soft); }
|
|
1279
1279
|
.draft-filter-toolbar label { color: var(--muted); font-size: 11px; }
|
|
1280
1280
|
.draft-filter-toolbar select { min-width: 180px; min-height: 38px; padding: 8px 10px; background: var(--surface); font-size: 11px; }
|
|
1281
1281
|
.draft-filter-toolbar span { margin-left: auto; align-self: center; color: var(--muted); font-size: 11px; }
|
|
@@ -1301,9 +1301,24 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1301
1301
|
.card-actions .primary-button:hover, .card-actions .primary-button:focus-visible { background: var(--accent-dark); }
|
|
1302
1302
|
.provider-card-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
|
|
1303
1303
|
.provider-card { min-width: 0; }
|
|
1304
|
+
.provider-card.is-disabled { border-style: dashed; border-color: color-mix(in srgb, var(--accent) 68%, var(--line)); background: color-mix(in srgb, var(--accent) 5%, var(--surface-soft)); }
|
|
1305
|
+
.provider-card-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
1306
|
+
.provider-card-meta small { min-width: 0; }
|
|
1307
|
+
.provider-status-badge, .model-status-badge { display: inline-flex; flex: 0 0 auto; align-items: center; min-height: 22px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px; font-size: 9px; font-weight: 700; line-height: 1.1; white-space: nowrap; }
|
|
1308
|
+
.provider-status-badge.is-enabled { border-color: color-mix(in srgb, var(--green) 48%, var(--line)); background: color-mix(in srgb, var(--green) 10%, var(--surface)); color: var(--green); }
|
|
1309
|
+
.provider-status-badge.is-disabled, .model-status-badge.is-disabled { border-color: color-mix(in srgb, var(--accent) 68%, var(--line)); background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent-dark); }
|
|
1310
|
+
.provider-status-badge.is-error, .model-status-badge.is-unavailable { border-color: color-mix(in srgb, var(--accent) 46%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); }
|
|
1311
|
+
.provider-disabled-notice { display: flex; align-items: baseline; gap: 8px; margin: 12px 0; padding: 9px 11px; border-left: 3px solid var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
1312
|
+
.provider-disabled-notice strong { color: var(--accent-dark); font-size: 12px; }
|
|
1313
|
+
.provider-disabled-notice span { color: var(--muted); font-size: 10px; line-height: 1.4; }
|
|
1304
1314
|
.provider-card p, .provider-card .model-pill { overflow-wrap: anywhere; word-break: break-word; }
|
|
1305
1315
|
.provider-models { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
1306
|
-
.provider-
|
|
1316
|
+
.provider-model-row { display: flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
|
|
1317
|
+
.provider-model-row.is-unavailable { padding: 4px 6px; border: 1px dashed color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 4px; background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
1318
|
+
.provider-card .model-pill { flex: 1 1 auto; max-width: 100%; margin: 0; border: 0; text-align: left; white-space: normal; cursor: pointer; }
|
|
1319
|
+
.provider-model-row .model-pill { min-width: 0; }
|
|
1320
|
+
.provider-model-row.is-unavailable .model-pill { color: var(--muted); opacity: .72; }
|
|
1321
|
+
.provider-model-row .model-test-button { flex: 0 0 auto; min-height: 30px; padding: 6px 9px; }
|
|
1307
1322
|
.provider-card .model-pill:hover, .provider-card .model-pill:focus-visible { background: var(--surface-hover); color: var(--ink); }
|
|
1308
1323
|
.provider-card .card-actions { flex-wrap: wrap; }
|
|
1309
1324
|
.pill { display: inline-block; padding: 3px 7px; margin: 0 4px 4px 0; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); color: var(--muted); font-size: 9px; }
|
|
@@ -2038,6 +2053,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2038
2053
|
.assistant-message > .message-heading, .user-message > .message-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; opacity: .65; font-size: 9px; }
|
|
2039
2054
|
.message-heading > span { letter-spacing: .12em; }.message-heading time { flex: 0 0 auto; font-variant-numeric: tabular-nums; letter-spacing: .03em; }
|
|
2040
2055
|
.message-body { min-width: 0; overflow-wrap: anywhere; }
|
|
2056
|
+
.assistant-message.is-error .message-body { font-family: inherit; font-size: inherit; line-height: inherit; }
|
|
2057
|
+
.assistant-message.is-error .ai-error-text { margin: 0; white-space: pre-wrap; font: inherit; }
|
|
2041
2058
|
.message-card-actions { position: absolute; right: 0; bottom: -31px; display: flex; align-items: center; gap: 12px; height: 24px; }
|
|
2042
2059
|
.message-card-actions button { display: inline-flex; align-items: center; gap: 4px; min-width: 0; padding: 3px 2px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
|
|
2043
2060
|
.message-card-actions button:hover, .message-card-actions button:focus-visible { color: var(--accent-dark); }
|
|
@@ -2139,11 +2156,33 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2139
2156
|
.ai-prompt-reference button { width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; font: 14px/1 var(--font-latin), monospace; }
|
|
2140
2157
|
.ai-prompt-reference button:hover, .ai-prompt-reference button:focus-visible { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
|
|
2141
2158
|
.prompt-composer-actions { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; align-items: center; }
|
|
2142
|
-
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; }
|
|
2159
|
+
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
|
|
2143
2160
|
.ai-context-meter::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--paper); box-shadow: inset 0 0 0 1px rgba(143,132,116,.12); }
|
|
2144
|
-
.ai-context-meter
|
|
2145
|
-
.ai-context-meter:
|
|
2161
|
+
.ai-context-meter:hover, .ai-context-meter:focus-visible, .ai-context-meter[aria-expanded="true"] { outline: 0; filter: brightness(.96); }
|
|
2162
|
+
.ai-context-meter:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
2146
2163
|
.ai-context-meter b { position: relative; z-index: 1; font-size: 9px; font-weight: 600; }.ai-context-meter.is-warning { --context-meter-color: #bf6a35; }.ai-context-meter.is-danger { --context-meter-color: #ad463c; }.ai-context-meter.is-empty { opacity: .62; }
|
|
2164
|
+
.ai-context-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 40; display: grid; width: min(320px, calc(100vw - 32px)); gap: 12px; padding: 14px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
|
|
2165
|
+
.ai-context-popover.hidden { display: none; }
|
|
2166
|
+
.ai-context-popover::after { position: absolute; right: 12px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
|
|
2167
|
+
.ai-context-popover-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
|
2168
|
+
.ai-context-popover-header > div { display: grid; gap: 3px; min-width: 0; }
|
|
2169
|
+
.ai-context-popover-header strong { font-size: 12px; }
|
|
2170
|
+
.ai-context-popover-header small { color: var(--muted); font: 9px/1.4 var(--font-latin), var(--font-cjk), sans-serif; }
|
|
2171
|
+
.ai-context-popover-close { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font: 17px/1 var(--font-latin), sans-serif; }
|
|
2172
|
+
.ai-context-popover-close:hover, .ai-context-popover-close:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: 0; }
|
|
2173
|
+
.ai-context-distribution { display: grid; gap: 10px; }
|
|
2174
|
+
.ai-context-distribution-row { display: grid; gap: 5px; }
|
|
2175
|
+
.ai-context-distribution-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font: 10px/1.3 var(--font-latin), sans-serif; }
|
|
2176
|
+
.ai-context-distribution-label > span { min-width: 0; color: var(--ink); }
|
|
2177
|
+
.ai-context-distribution-label small { color: var(--muted); font: 9px/1.3 var(--font-cjk), sans-serif; }
|
|
2178
|
+
.ai-context-distribution-label strong { flex: 0 0 auto; color: var(--muted); font: 10px/1.3 var(--font-latin), monospace; font-weight: 500; }
|
|
2179
|
+
.ai-context-distribution-track { height: 5px; overflow: hidden; border-radius: 3px; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
2180
|
+
.ai-context-distribution-track > span { display: block; width: calc(var(--distribution-percent) * 1%); height: 100%; min-width: 0; border-radius: inherit; background: var(--distribution-color, var(--accent)); transition: width .16s ease; }
|
|
2181
|
+
.ai-context-distribution-row[data-key="system-prompt"] { --distribution-color: var(--accent); }
|
|
2182
|
+
.ai-context-distribution-row[data-key="function"] { --distribution-color: #9a7651; }
|
|
2183
|
+
.ai-context-distribution-row[data-key="skills"] { --distribution-color: var(--muted); }
|
|
2184
|
+
.ai-context-distribution-row[data-key="context"] { --distribution-color: var(--green); }
|
|
2185
|
+
.ai-context-distribution-row[data-key="left"] { --distribution-color: color-mix(in srgb, var(--muted) 55%, transparent); }
|
|
2147
2186
|
.ai-send-button { min-width: 54px; height: 32px; padding: 0 11px; border: 0; border-radius: 4px; background: var(--accent); color: #fff; font-size: 10px; font-weight: 600; box-shadow: 0 4px 12px rgba(139,61,44,.2); }
|
|
2148
2187
|
.ai-send-button:hover { background: var(--accent-dark); }.ai-send-button:disabled { cursor: wait; opacity: .62; }
|
|
2149
2188
|
.ai-citations { display: grid; gap: 6px; max-height: 190px; overflow-y: auto; margin-bottom: 8px; }
|
|
@@ -2970,7 +3009,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2970
3009
|
.app-shell.shelf-mode .main-panel { grid-column: 1; grid-row: 2; min-height: 0; }
|
|
2971
3010
|
.ai-panel {
|
|
2972
3011
|
position: fixed;
|
|
2973
|
-
z-index:
|
|
3012
|
+
z-index: 35;
|
|
2974
3013
|
right: 0;
|
|
2975
3014
|
bottom: 0;
|
|
2976
3015
|
left: 0;
|