merge: redesign chat settings
This commit is contained in:
+81
-19
@@ -16,7 +16,7 @@ import {
|
|||||||
import {conversationHash, conversationIdFromHash} from "../lib/conversation-hash";
|
import {conversationHash, conversationIdFromHash} from "../lib/conversation-hash";
|
||||||
import {conversationTitlePrompt, normalizeGeneratedConversationTitle, untitledConversationLabel} from "../lib/conversation-title";
|
import {conversationTitlePrompt, normalizeGeneratedConversationTitle, untitledConversationLabel} from "../lib/conversation-title";
|
||||||
import type {Conversation, ConversationSummary, MessageCompletion, ResponseMetadata, StoredChatMessage, WorkingItem} from "../lib/conversation-types";
|
import type {Conversation, ConversationSummary, MessageCompletion, ResponseMetadata, StoredChatMessage, WorkingItem} from "../lib/conversation-types";
|
||||||
import {defaultGenerationSettings, type GenerationSettings} from "../lib/generation-settings";
|
import {defaultGenerationSettings, type GenerationSettings, type ReasoningLevel} from "../lib/generation-settings";
|
||||||
import {
|
import {
|
||||||
activateOfflineProfile,
|
activateOfflineProfile,
|
||||||
cachedLastFetchAt,
|
cachedLastFetchAt,
|
||||||
@@ -115,7 +115,8 @@ const state = {
|
|||||||
importing: false,
|
importing: false,
|
||||||
importStatus: "",
|
importStatus: "",
|
||||||
importTitleTemplate: window.localStorage.getItem("xiteng-chat-import-title-template") || "{title}",
|
importTitleTemplate: window.localStorage.getItem("xiteng-chat-import-title-template") || "{title}",
|
||||||
composerFullscreen: false
|
composerFullscreen: false,
|
||||||
|
settingsOpen: false
|
||||||
};
|
};
|
||||||
const titleGenerationConversationIds = new Set<string>();
|
const titleGenerationConversationIds = new Set<string>();
|
||||||
|
|
||||||
@@ -749,23 +750,44 @@ async function updateAvatar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModelChoice = {provider: ChatProvider; model: ProviderModel; key: string};
|
||||||
|
|
||||||
|
function availableModelChoices(): ModelChoice[] {
|
||||||
|
return state.config?.providers.flatMap((item) => item.models.map((model) => ({provider: item, model, key: `${item.id}/${model.id}`}))) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEffortControl(name: string) {
|
||||||
|
const options: Array<[ReasoningLevel, string]> = [["auto", "自动"], ["none", "关闭"], ["low", "低"], ["medium", "中"], ["high", "高"]];
|
||||||
|
return `<div class="effort-control"><div class="effort-heading"><strong>Effort</strong><small>控制模型的思考强度</small></div><div class="effort-options" role="radiogroup" aria-label="Effort">${options.map(([value, label]) => `<label><input type="radio" name="${name}" value="${value}" data-setting="reasoning"${state.generationSettings.reasoning === value ? " checked" : ""}><span>${label}</span></label>`).join("")}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderModelOption(choice: ModelChoice) {
|
||||||
|
const active = choice.provider.id === state.providerId && choice.model.id === state.model;
|
||||||
|
const displayName = compactModelName(choice.model.name || choice.model.id);
|
||||||
|
const detail = displayName === choice.model.id
|
||||||
|
? choice.provider.name
|
||||||
|
: `${choice.model.id} · ${choice.provider.name}`;
|
||||||
|
return `<button class="model-option${active ? " active" : ""}" type="button" data-action="choose-model" data-provider="${escapeHtml(choice.provider.id)}" data-model="${escapeHtml(choice.model.id)}"><span><strong>${escapeHtml(displayName)}</strong><small title="${escapeHtml(choice.model.id)}">${escapeHtml(detail)}</small></span><small>${choice.provider.connection.type === "frontend" ? "Frontend" : "Backend"}</small></button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickModelChoices(choices: ModelChoice[]) {
|
||||||
|
const activeKey = `${state.providerId}/${state.model}`;
|
||||||
|
const preferredKeys = [activeKey, ...state.recentModelKeys, ...(state.config?.providers.map((item) => `${item.id}/${item.defaultModel}`) || [])];
|
||||||
|
const selected: ModelChoice[] = [];
|
||||||
|
for (const key of preferredKeys) {
|
||||||
|
const choice = choices.find((item) => item.key === key);
|
||||||
|
if (choice && !selected.includes(choice)) selected.push(choice);
|
||||||
|
}
|
||||||
|
for (const choice of choices) if (selected.length < 6 && !selected.includes(choice)) selected.push(choice);
|
||||||
|
return selected.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
function renderModelPicker() {
|
function renderModelPicker() {
|
||||||
const active = provider();
|
const active = provider();
|
||||||
if (!active || !state.config) return "";
|
if (!active || !state.config) return "";
|
||||||
const activeModelName = compactModelName(active.models.find((model) => model.id === state.model)?.name || state.model);
|
const activeModelName = compactModelName(active.models.find((model) => model.id === state.model)?.name || state.model);
|
||||||
const settings = state.generationSettings;
|
const choices = quickModelChoices(availableModelChoices());
|
||||||
const query = state.modelQuery.trim().toLowerCase();
|
return `<details class="model-picker"><summary aria-label="模型和 Effort"><span class="picker-label">${escapeHtml(activeModelName)}</span><span class="picker-icons"><i class="picker-chevron">${icons.down}</i></span></summary><div class="model-menu"><section class="quick-models"><div class="quick-config-heading"><strong>模型</strong><small>当前与最近使用</small></div><div class="quick-model-list">${choices.map(renderModelOption).join("")}</div></section>${renderEffortControl("quick-effort")}<button class="open-settings-button" type="button" data-action="open-settings">${icons.settings}<span><strong>打开全部设置</strong><small>模型、生成参数与 Provider</small></span></button></div></details>`;
|
||||||
const choices = state.config.providers.flatMap((item) => item.models.map((model) => ({provider: item, model, key: `${item.id}/${model.id}`})));
|
|
||||||
const matches = choices.filter((choice) => !query || choice.key.toLowerCase().includes(query) || choice.model.name.toLowerCase().includes(query));
|
|
||||||
const recent = state.recentModelKeys.map((key) => choices.find((choice) => choice.key === key)).filter((choice): choice is NonNullable<typeof choice> => Boolean(choice)).filter((choice) => matches.includes(choice));
|
|
||||||
const option = (choice: typeof choices[number]) => `<button class="model-option${choice.provider.id === state.providerId && choice.model.id === state.model ? " active" : ""}" type="button" data-action="choose-model" data-provider="${escapeHtml(choice.provider.id)}" data-model="${escapeHtml(choice.model.id)}"><span><strong>${escapeHtml(choice.key)}</strong>${choice.model.name !== choice.model.id ? `<small>${escapeHtml(choice.model.name)}</small>` : ""}</span><small>${choice.provider.connection.type === "frontend" ? "Frontend" : "Backend"}</small></button>`;
|
|
||||||
const groups = state.config.providers.map((item) => {
|
|
||||||
const items = matches.filter((choice) => choice.provider.id === item.id);
|
|
||||||
return items.length ? `<section class="model-menu-group"><h3>${escapeHtml(item.name)}</h3>${items.map(option).join("")}</section>` : "";
|
|
||||||
}).join("");
|
|
||||||
const providerSettings = renderProviderSettings();
|
|
||||||
const generationSettings = `<section class="model-generation-settings"><div class="settings-section-heading"><strong>生成参数</strong><small>随当前会话保存</small></div><label>思考强度<select data-setting="reasoning"><option value="auto"${settings.reasoning === "auto" ? " selected" : ""}>自动</option><option value="none"${settings.reasoning === "none" ? " selected" : ""}>关闭</option><option value="low"${settings.reasoning === "low" ? " selected" : ""}>低</option><option value="medium"${settings.reasoning === "medium" ? " selected" : ""}>中</option><option value="high"${settings.reasoning === "high" ? " selected" : ""}>高</option></select></label><label class="generation-check"><input type="checkbox" data-setting="showReasoningSummary"${settings.showReasoningSummary ? " checked" : ""}>显示思考摘要</label><label>Temperature<input type="number" min="0" max="2" step="0.1" placeholder="自动" data-setting="temperature" value="${settings.temperature ?? ""}"></label><label>最大输出 Tokens<input type="number" min="1" max="1000000" step="1" placeholder="自动" data-setting="maxOutputTokens" value="${settings.maxOutputTokens ?? ""}"></label><div class="settings-divider"></div><div class="settings-section-heading"><strong>界面</strong><small>仅保存在当前浏览器</small></div><label class="generation-check"><input type="checkbox" data-action="advanced-actions"${state.advancedActions ? " checked" : ""}>显示高级对话操作</label><small class="settings-help">显示“提交后让 AI 回答”和编辑助手回答。</small><button type="button" data-action="reset-settings">恢复默认生成参数</button></section>`;
|
|
||||||
return `<details class="model-picker"><summary aria-label="模型和生成参数"><span class="picker-label">${escapeHtml(activeModelName)}</span><span class="picker-icons"><i class="picker-chevron">${icons.down}</i></span></summary><div class="model-menu"><section class="model-browser"><label class="model-search">${icons.search}<input value="${escapeHtml(state.modelQuery)}" data-action="model-search" placeholder="搜索 provider/model"></label>${recent.length ? `<section class="model-menu-group recent-models"><h3>${icons.clock}最近使用</h3>${recent.map(option).join("")}</section>` : ""}${groups || '<p class="model-menu-empty">没有匹配的模型</p>'}${providerSettings}</section>${generationSettings}</div></details>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProviderSettings() {
|
function renderProviderSettings() {
|
||||||
@@ -776,6 +798,31 @@ function renderProviderSettings() {
|
|||||||
}).join("")}</section>`;
|
}).join("")}</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGenerationSettings() {
|
||||||
|
const settings = state.generationSettings;
|
||||||
|
return `${renderEffortControl("settings-effort")}<div class="settings-field-grid"><label class="settings-check"><input type="checkbox" data-setting="showReasoningSummary"${settings.showReasoningSummary ? " checked" : ""}><span><strong>显示思考摘要</strong><small>Provider 支持时返回可见的思考摘要</small></span></label><label>Temperature<input type="number" min="0" max="2" step="0.1" placeholder="自动" data-setting="temperature" value="${settings.temperature ?? ""}"></label><label>最大输出 Tokens<input type="number" min="1" max="1000000" step="1" placeholder="自动" data-setting="maxOutputTokens" value="${settings.maxOutputTokens ?? ""}"></label></div><button class="settings-reset-button" type="button" data-action="reset-settings">恢复默认生成参数</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettingsPage() {
|
||||||
|
if (!state.settingsOpen || !state.config) return "";
|
||||||
|
const query = state.modelQuery.trim().toLowerCase();
|
||||||
|
const choices = availableModelChoices();
|
||||||
|
const matches = choices.filter((choice) => !query || choice.key.toLowerCase().includes(query) || choice.model.name.toLowerCase().includes(query) || choice.provider.name.toLowerCase().includes(query));
|
||||||
|
const groups = state.config.providers.map((item) => {
|
||||||
|
const items = matches.filter((choice) => choice.provider.id === item.id);
|
||||||
|
return items.length ? `<section class="settings-model-group"><h3>${escapeHtml(item.name)}</h3><div>${items.map(renderModelOption).join("")}</div></section>` : "";
|
||||||
|
}).join("");
|
||||||
|
const providerSettings = renderProviderSettings();
|
||||||
|
const accountHref = state.authenticated ? "https://xiteng.site/account" : appUrl("/api/login");
|
||||||
|
const accountLabel = state.authenticated ? "管理 Backend Provider 与凭据" : "登录并配置 Backend Provider";
|
||||||
|
return `<section class="settings-page" role="dialog" aria-modal="true" aria-label="设置"><header class="settings-page-header"><button type="button" data-action="close-settings" aria-label="关闭设置">${icons.close}</button><span><strong>设置</strong><small>更改会自动保存</small></span></header><div class="settings-layout"><nav class="settings-nav" aria-label="设置分类"><button type="button" data-action="scroll-settings-section" data-id="settings-models">模型</button><button type="button" data-action="scroll-settings-section" data-id="settings-generation">生成</button><button type="button" data-action="scroll-settings-section" data-id="settings-providers">Provider</button><button type="button" data-action="scroll-settings-section" data-id="settings-interface">界面</button></nav><main class="settings-content"><section class="settings-card" id="settings-models"><header><h2>模型</h2><p>选择当前会话使用的模型。</p></header><label class="settings-model-search">${icons.search}<input value="${escapeHtml(state.modelQuery)}" data-action="model-search" placeholder="搜索 Provider 或模型"></label><div class="settings-model-groups">${groups || '<p class="settings-empty">没有匹配的模型</p>'}</div></section><section class="settings-card" id="settings-generation"><header><h2>生成</h2><p>这些参数随当前会话保存。</p></header>${renderGenerationSettings()}</section><section class="settings-card" id="settings-providers"><header><h2>Provider</h2><p>管理浏览器直连端点;Backend 凭据由账户安全保存。</p></header>${providerSettings || '<p class="settings-empty">当前没有可配置的浏览器 Provider。</p>'}<a class="settings-account-link" href="${accountHref}">${icons.settings}<span>${accountLabel}</span></a></section><section class="settings-card" id="settings-interface"><header><h2>界面</h2><p>这些选项仅保存在当前浏览器。</p></header><div class="settings-interface-options"><label class="settings-check"><input type="checkbox" data-action="advanced-actions"${state.advancedActions ? " checked" : ""}><span><strong>显示高级对话操作</strong><small>显示“需要回答”和编辑助手回答</small></span></label><label class="settings-check"><input type="checkbox" data-action="history-tree-setting"${state.historyTree ? " checked" : ""}><span><strong>树状显示聊天历史</strong><small>按会话名称中的路径组织侧栏</small></span></label></div></section></main></div></section>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollSettingsSection(id: string) {
|
||||||
|
const section = root.querySelector<HTMLElement>(`#${CSS.escape(id)}`);
|
||||||
|
section?.scrollIntoView({behavior: "smooth", block: "start"});
|
||||||
|
}
|
||||||
|
|
||||||
function historyItem(item: ConversationSummary, label = item.name, depth = 0) {
|
function historyItem(item: ConversationSummary, label = item.name, depth = 0) {
|
||||||
const displayLabel = String(label || "").trim() || untitledConversationLabel;
|
const displayLabel = String(label || "").trim() || untitledConversationLabel;
|
||||||
return `<article class="history-item${item.id === state.conversation?.id ? " active" : ""}" style="--history-depth:${depth}"><button class="history-select" type="button" data-action="select-conversation" data-id="${escapeHtml(item.id)}"><strong>${escapeHtml(displayLabel)}</strong><small>${escapeHtml(item.providerId)} · ${escapeHtml(item.model)}</small></button><button class="history-rename" type="button" data-action="rename-conversation" data-id="${escapeHtml(item.id)}" aria-label="重命名 ${escapeHtml(displayLabel)}">${icons.edit}</button><button class="history-delete" type="button" data-action="delete-conversation" data-id="${escapeHtml(item.id)}" aria-label="删除 ${escapeHtml(displayLabel)}">${icons.trash}</button></article>`;
|
return `<article class="history-item${item.id === state.conversation?.id ? " active" : ""}" style="--history-depth:${depth}"><button class="history-select" type="button" data-action="select-conversation" data-id="${escapeHtml(item.id)}"><strong>${escapeHtml(displayLabel)}</strong><small>${escapeHtml(item.providerId)} · ${escapeHtml(item.model)}</small></button><button class="history-rename" type="button" data-action="rename-conversation" data-id="${escapeHtml(item.id)}" aria-label="重命名 ${escapeHtml(displayLabel)}">${icons.edit}</button><button class="history-delete" type="button" data-action="delete-conversation" data-id="${escapeHtml(item.id)}" aria-label="删除 ${escapeHtml(displayLabel)}">${icons.trash}</button></article>`;
|
||||||
@@ -909,7 +956,7 @@ function renderWorkingPanel() {
|
|||||||
const unfinished = state.workingItems.filter((item) => item.kind === "assistant-stream" && item.status !== "streaming");
|
const unfinished = state.workingItems.filter((item) => item.kind === "assistant-stream" && item.status !== "streaming");
|
||||||
const requestAssistantReply = activeDraft()?.requestAssistantReply ?? true;
|
const requestAssistantReply = activeDraft()?.requestAssistantReply ?? true;
|
||||||
const assistantReplyToggle = state.advancedActions
|
const assistantReplyToggle = state.advancedActions
|
||||||
? `<label class="assistant-reply-toggle"><input type="checkbox" data-action="request-assistant-reply"${requestAssistantReply ? " checked" : ""}>提交后让 AI 回答</label>`
|
? `<label class="assistant-reply-toggle"><input type="checkbox" data-action="request-assistant-reply"${requestAssistantReply ? " checked" : ""}>需要回答</label>`
|
||||||
: "";
|
: "";
|
||||||
const draftRows = drafts.map((item) => {
|
const draftRows = drafts.map((item) => {
|
||||||
const text = workingItemText(item).trim().replace(/\s+/g, " ");
|
const text = workingItemText(item).trim().replace(/\s+/g, " ");
|
||||||
@@ -919,7 +966,7 @@ function renderWorkingPanel() {
|
|||||||
const text = workingItemText(item).trim().replace(/\s+/g, " ");
|
const text = workingItemText(item).trim().replace(/\s+/g, " ");
|
||||||
return `<div class="unfinished-row"><span><strong>未完成回答</strong><small>${escapeHtml(text.slice(0, 64) || "尚未输出正文")} · ${new Date(item.updatedAt).toLocaleString()}</small></span><button type="button" data-action="commit-partial" data-id="${escapeHtml(item.id)}">保留</button><button type="button" data-action="delete-working" data-id="${escapeHtml(item.id)}">清理</button></div>`;
|
return `<div class="unfinished-row"><span><strong>未完成回答</strong><small>${escapeHtml(text.slice(0, 64) || "尚未输出正文")} · ${new Date(item.updatedAt).toLocaleString()}</small></span><button type="button" data-action="commit-partial" data-id="${escapeHtml(item.id)}">保留</button><button type="button" data-action="delete-working" data-id="${escapeHtml(item.id)}">清理</button></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
return `<div class="working-panel">${assistantReplyToggle}${unfinishedRows ? `<details class="unfinished-menu"><summary>未完成 ${unfinished.length}</summary><div>${unfinishedRows}</div></details>` : ""}<details class="draft-menu"><summary>草稿 ${drafts.length}</summary><div><button class="new-draft" type="button" data-action="new-draft">${icons.plus}新草稿</button>${draftRows}</div></details></div>`;
|
return `<div class="working-panel">${assistantReplyToggle}${renderComposerControls(activeDraft())}${unfinishedRows ? `<details class="unfinished-menu"><summary>未完成 ${unfinished.length}</summary><div>${unfinishedRows}</div></details>` : ""}<details class="draft-menu"><summary>草稿 ${drafts.length}</summary><div><button class="new-draft" type="button" data-action="new-draft">${icons.plus}新草稿</button>${draftRows}</div></details></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replyTargetLabel(message: StoredChatMessage) {
|
function replyTargetLabel(message: StoredChatMessage) {
|
||||||
@@ -968,7 +1015,7 @@ function renderThread() {
|
|||||||
const placeholder = fullscreen
|
const placeholder = fullscreen
|
||||||
? editing ? "编辑消息;Ctrl/⌘ + Enter 提交" : "输入消息;Ctrl/⌘ + Enter 提交"
|
? editing ? "编辑消息;Ctrl/⌘ + Enter 提交" : "输入消息;Ctrl/⌘ + Enter 提交"
|
||||||
: editing ? "编辑消息,提交后从此处继续" : "输入消息,Enter 发送,Shift + Enter 换行";
|
: editing ? "编辑消息,提交后从此处继续" : "输入消息,Enter 发送,Shift + Enter 换行";
|
||||||
return `<section class="thread-root"><div class="thread-viewport" id="thread-viewport">${renderBranchPreviewNotice()}<div id="message-list">${renderMessagesMarkup()}</div><div class="thread-footer${fullscreen ? " fullscreen-editor" : ""}">${fullscreenHeader}<button class="scroll-button" type="button" data-action="scroll-bottom" aria-label="滚动到底部">${icons.scroll}</button>${renderWorkingPanel()}${editing ? `<div class="edit-context"><span>正在编辑${editRole};提交后当前会话将从这里继续</span><button type="button" data-action="cancel-edit">取消</button></div>` : ""}${renderComposerControls(draft)}<form class="composer" id="composer"><textarea class="composer-input" name="message" placeholder="${placeholder}" rows="1" aria-label="聊天消息">${escapeHtml(draft ? workingItemText(draft) : "")}</textarea><div class="composer-actions">${state.streaming ? `<button class="stop-button" type="button" data-action="stop" aria-label="停止生成">${icons.stop}</button>` : ""}<button class="fullscreen-button" type="button" data-action="toggle-composer-fullscreen" aria-label="${fullscreen ? "退出全屏编辑" : "进入全屏编辑"}" title="${fullscreen ? "退出全屏编辑" : "全屏编辑"}">${fullscreen ? icons.collapse : icons.expand}</button></div>${renderModelPicker()}<button class="send-button" type="submit" data-action="send" aria-label="${state.streaming ? "排队发送" : "发送消息"}">${icons.send}</button></form><p class="composer-note${state.offline ? " offline" : ""}${queued ? " queued" : ""}">${note}</p></div></div></section>`;
|
return `<section class="thread-root"><div class="thread-viewport" id="thread-viewport">${renderBranchPreviewNotice()}<div id="message-list">${renderMessagesMarkup()}</div><div class="thread-footer${fullscreen ? " fullscreen-editor" : ""}">${fullscreenHeader}<button class="scroll-button" type="button" data-action="scroll-bottom" aria-label="滚动到底部">${icons.scroll}</button>${renderWorkingPanel()}${editing ? `<div class="edit-context"><span>正在编辑${editRole};提交后当前会话将从这里继续</span><button type="button" data-action="cancel-edit">取消</button></div>` : ""}<form class="composer" id="composer"><textarea class="composer-input" name="message" placeholder="${placeholder}" rows="1" aria-label="聊天消息">${escapeHtml(draft ? workingItemText(draft) : "")}</textarea><div class="composer-actions">${state.streaming ? `<button class="stop-button" type="button" data-action="stop" aria-label="停止生成">${icons.stop}</button>` : ""}<button class="fullscreen-button" type="button" data-action="toggle-composer-fullscreen" aria-label="${fullscreen ? "退出全屏编辑" : "进入全屏编辑"}" title="${fullscreen ? "退出全屏编辑" : "全屏编辑"}">${fullscreen ? icons.collapse : icons.expand}</button></div>${renderModelPicker()}<button class="send-button" type="submit" data-action="send" aria-label="${state.streaming ? "排队发送" : "发送消息"}">${icons.send}</button></form><p class="composer-note${state.offline ? " offline" : ""}${queued ? " queued" : ""}">${note}</p></div></div></section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setComposerFullscreen(fullscreen: boolean) {
|
function setComposerFullscreen(fullscreen: boolean) {
|
||||||
@@ -1042,7 +1089,7 @@ function renderApp() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profile = state.config.profile;
|
const profile = state.config.profile;
|
||||||
root.innerHTML = `<main class="app-shell with-history${state.historyOpen ? " history-open" : ""}"><header class="app-header"><div class="header-leading"><button class="history-toggle" type="button" data-action="toggle-history" aria-label="聊天历史">${icons.history}</button></div><div class="brand"><a class="portal-home-link" href="/" aria-label="返回 xiteng.site 主页" title="返回 xiteng.site 主页"><img src="/favicon.svg" alt=""></a></div><div class="chat-controls">${renderIdentitySyncControl(profile)}</div></header>${renderHistory()}${renderThread()}${renderImportPanel()}</main>`;
|
root.innerHTML = `<main class="app-shell with-history${state.historyOpen ? " history-open" : ""}"><header class="app-header"><div class="header-leading"><button class="history-toggle" type="button" data-action="toggle-history" aria-label="聊天历史">${icons.history}</button></div><div class="brand"><a class="portal-home-link" href="/" aria-label="返回 xiteng.site 主页" title="返回 xiteng.site 主页"><img src="/favicon.svg" alt=""></a></div><div class="chat-controls">${renderIdentitySyncControl(profile)}</div></header>${renderHistory()}${renderThread()}${renderImportPanel()}${renderSettingsPage()}</main>`;
|
||||||
scheduleMathTypesetting(root);
|
scheduleMathTypesetting(root);
|
||||||
void updateAvatar();
|
void updateAvatar();
|
||||||
}
|
}
|
||||||
@@ -2336,6 +2383,12 @@ root.addEventListener("submit", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
root.addEventListener("keydown", (event) => {
|
root.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" && state.settingsOpen) {
|
||||||
|
state.settingsOpen = false;
|
||||||
|
state.modelQuery = "";
|
||||||
|
renderApp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === "Escape" && state.importPanelOpen && !state.importing) {
|
if (event.key === "Escape" && state.importPanelOpen && !state.importing) {
|
||||||
state.importPanelOpen = false;
|
state.importPanelOpen = false;
|
||||||
renderApp();
|
renderApp();
|
||||||
@@ -2405,6 +2458,12 @@ root.addEventListener("change", (event) => {
|
|||||||
renderApp();
|
renderApp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (target.dataset.action === "history-tree-setting" && target instanceof HTMLInputElement) {
|
||||||
|
state.historyTree = target.checked;
|
||||||
|
window.localStorage.setItem("xiteng-chat-history-tree", target.checked ? "1" : "0");
|
||||||
|
renderApp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (target.dataset.action === "session-file" && target instanceof HTMLInputElement) {
|
if (target.dataset.action === "session-file" && target instanceof HTMLInputElement) {
|
||||||
const files = [...(target.files || [])];
|
const files = [...(target.files || [])];
|
||||||
if (files.length) void expandImportFiles(files).then((items) => importSessionFiles(items, `${files.length} 个所选文件`)).catch(showError).finally(() => { target.value = ""; });
|
if (files.length) void expandImportFiles(files).then((items) => importSessionFiles(items, `${files.length} 个所选文件`)).catch(showError).finally(() => { target.value = ""; });
|
||||||
@@ -2451,6 +2510,9 @@ root.addEventListener("click", (event) => {
|
|||||||
const button = (event.target as Element).closest<HTMLElement>("[data-action]");
|
const button = (event.target as Element).closest<HTMLElement>("[data-action]");
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
const action = button.dataset.action;
|
const action = button.dataset.action;
|
||||||
|
if (action === "open-settings") { state.settingsOpen = true; state.modelQuery = ""; renderApp(); }
|
||||||
|
if (action === "close-settings") { state.settingsOpen = false; state.modelQuery = ""; renderApp(); }
|
||||||
|
if (action === "scroll-settings-section" && button.dataset.id) scrollSettingsSection(button.dataset.id);
|
||||||
if (action === "toggle-history") { state.historyOpen = !state.historyOpen; renderApp(); }
|
if (action === "toggle-history") { state.historyOpen = !state.historyOpen; renderApp(); }
|
||||||
if (action === "close-history") { state.historyOpen = false; renderApp(); }
|
if (action === "close-history") { state.historyOpen = false; renderApp(); }
|
||||||
if (action === "toggle-history-tree") {
|
if (action === "toggle-history-tree") {
|
||||||
|
|||||||
+87
-28
@@ -42,17 +42,17 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.identity-sync-control.offline .identity-sync-status { background: var(--danger); box-shadow: 0 0 0 1px rgba(163,56,45,.25); }
|
.identity-sync-control.offline .identity-sync-status { background: var(--danger); box-shadow: 0 0 0 1px rgba(163,56,45,.25); }
|
||||||
.identity-sync-control.error .identity-sync-status { background: #d3932f; box-shadow: 0 0 0 1px rgba(211,147,47,.3); }
|
.identity-sync-control.error .identity-sync-status { background: #d3932f; box-shadow: 0 0 0 1px rgba(211,147,47,.3); }
|
||||||
.identity-sync-control.fetching .identity-sync-status { border: 1px solid var(--muted); border-top-color: transparent; background: var(--panel); box-shadow: none; animation: spin .75s linear infinite; }
|
.identity-sync-control.fetching .identity-sync-status { border: 1px solid var(--muted); border-top-color: transparent; background: var(--panel); box-shadow: none; animation: spin .75s linear infinite; }
|
||||||
.model-provider-settings { display: grid; gap: 3px; margin-top: 8px; border-top: 1px solid var(--line); padding: 12px 0 4px; }
|
.model-provider-settings { display: grid; gap: 6px; }
|
||||||
.model-provider-settings > .settings-section-heading { padding: 0 8px 4px; }
|
.model-provider-settings > .settings-section-heading { padding: 0 2px 4px; }
|
||||||
.local-key-entry { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; border-radius: 9px; padding: 7px; }
|
.local-key-entry { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 11px; padding: 10px 11px; background: color-mix(in srgb, var(--panel) 70%, transparent); }
|
||||||
.local-key-entry:hover { background: var(--soft); }
|
.local-key-entry:hover { background: var(--panel); }
|
||||||
.local-key-entry > span { min-width: 0; text-align: left; }
|
.local-key-entry > span { min-width: 0; text-align: left; }
|
||||||
.local-key-entry > span strong, .local-key-entry > span small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.local-key-entry > span strong, .local-key-entry > span small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.local-key-entry > span small { margin-top: 2px; color: var(--muted); font-size: 9px; }
|
.local-key-entry > span small { margin-top: 3px; color: var(--muted); font-size: 10px; }
|
||||||
.local-key-entry > span .local-key-error { max-width: 190px; color: var(--danger); white-space: normal; }
|
.local-key-entry > span .local-key-error { max-width: 190px; color: var(--danger); white-space: normal; }
|
||||||
.local-key-entry > div { display: flex; flex: 0 0 auto; gap: 2px; }
|
.local-key-entry > div { display: flex; flex: 0 0 auto; gap: 2px; }
|
||||||
.model-provider-settings button { border: 0; border-radius: 8px; padding: 8px 9px; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
|
.model-provider-settings button { border: 0; border-radius: 8px; padding: 8px 9px; background: var(--soft); color: var(--text); text-align: left; cursor: pointer; }
|
||||||
.model-provider-settings button:hover { background: var(--panel); }
|
.model-provider-settings button:hover { background: color-mix(in srgb, var(--soft) 76%, var(--text)); }
|
||||||
.model-provider-settings button.dangerous { color: #b42318; }
|
.model-provider-settings button.dangerous { color: #b42318; }
|
||||||
.model-picker { position: relative; }
|
.model-picker { position: relative; }
|
||||||
.model-picker > summary { display: flex; min-width: 190px; max-width: 290px; height: 36px; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid var(--line); border-radius: 11px; padding: 0 10px; background: rgba(255,255,255,.72); color: var(--text); cursor: pointer; list-style: none; }
|
.model-picker > summary { display: flex; min-width: 190px; max-width: 290px; height: 36px; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid var(--line); border-radius: 11px; padding: 0 10px; background: rgba(255,255,255,.72); color: var(--text); cursor: pointer; list-style: none; }
|
||||||
@@ -63,32 +63,80 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.model-picker > summary::-webkit-details-marker { display: none; }
|
.model-picker > summary::-webkit-details-marker { display: none; }
|
||||||
.model-picker[open] > summary { border-color: rgba(23,23,23,.24); background: #fff; }
|
.model-picker[open] > summary { border-color: rgba(23,23,23,.24); background: #fff; }
|
||||||
.model-picker[open] > summary .picker-chevron { transform: rotate(180deg); }
|
.model-picker[open] > summary .picker-chevron { transform: rotate(180deg); }
|
||||||
.model-menu { position: fixed; z-index: 30; top: 56px; right: 22px; display: grid; width: min(680px, calc(100vw - 24px)); max-height: min(670px, calc(100dvh - 82px)); grid-template-columns: minmax(300px, 1fr) 240px; overflow: hidden; border: 1px solid var(--line); border-radius: 14px; padding: 0; background: var(--panel); box-shadow: 0 18px 48px rgba(0,0,0,.15); backdrop-filter: blur(18px); }
|
.model-menu { position: fixed; z-index: 30; top: 56px; right: 22px; display: grid; width: min(360px, calc(100vw - 24px)); max-height: min(520px, calc(100dvh - 82px)); overflow-y: auto; border: 1px solid var(--line); border-radius: 15px; padding: 8px; background: var(--panel); box-shadow: 0 18px 48px rgba(0,0,0,.15); backdrop-filter: blur(18px); }
|
||||||
.model-browser { min-height: 0; overflow-y: auto; padding: 8px; }
|
.quick-models { min-height: 0; }
|
||||||
.model-search { position: sticky; z-index: 1; top: -8px; display: flex; height: 40px; align-items: center; gap: 8px; margin: -1px -1px 5px; padding: 0 9px; border-bottom: 1px solid var(--line); background: var(--panel); color: var(--muted); }
|
.quick-config-heading, .effort-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 7px 8px; }
|
||||||
.model-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); }
|
.quick-config-heading strong, .effort-heading strong { font-size: 11px; }
|
||||||
.model-menu-group { display: grid; gap: 2px; padding: 5px 0 8px; }
|
.quick-config-heading small, .effort-heading small { color: var(--muted); font-size: 9px; }
|
||||||
.model-menu-group + .model-menu-group { border-top: 1px solid var(--line); }
|
.quick-model-list { display: grid; max-height: 238px; overflow-y: auto; gap: 2px; }
|
||||||
.model-menu-group h3 { display: flex; align-items: center; gap: 5px; margin: 0; padding: 4px 8px; color: var(--muted); font-size: 10px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; }
|
|
||||||
.model-option { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 12px; border: 0; border-radius: 8px; padding: 8px; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
|
.model-option { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 12px; border: 0; border-radius: 8px; padding: 8px; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
|
||||||
.model-option:hover, .model-option.active { background: var(--soft); }
|
.model-option:hover, .model-option.active { background: var(--soft); }
|
||||||
.model-option > span { min-width: 0; }
|
.model-option > span { min-width: 0; }
|
||||||
.model-option strong, .model-option small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.model-option strong, .model-option small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.model-option strong { font-size: 12px; font-weight: 620; }
|
.model-option strong { font-size: 12px; font-weight: 620; }
|
||||||
.model-option small { color: var(--muted); font-size: 10px; }
|
.model-option small { color: var(--muted); font-size: 10px; }
|
||||||
.model-menu-empty { margin: 10px; color: var(--muted); text-align: center; }
|
.model-option.active::after { flex: 0 0 auto; color: var(--text); content: "✓"; font-size: 11px; font-weight: 800; }
|
||||||
.model-generation-settings { display: grid; align-content: start; gap: 10px; overflow-y: auto; border-left: 1px solid var(--line); padding: 14px; background: color-mix(in srgb, var(--panel) 84%, var(--soft)); }
|
.model-option.active > small { display: none; }
|
||||||
|
.effort-control { display: grid; gap: 3px; margin-top: 7px; border-top: 1px solid var(--line); padding-top: 5px; }
|
||||||
|
.effort-options { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 3px; padding: 0 3px 6px; }
|
||||||
|
.effort-options label { min-width: 0; cursor: pointer; }
|
||||||
|
.effort-options input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
|
.effort-options span { display: grid; height: 30px; place-items: center; border-radius: 8px; color: var(--muted); font-size: 10px; }
|
||||||
|
.effort-options input:checked + span { background: var(--soft); color: var(--text); font-weight: 700; }
|
||||||
|
.effort-options input:focus-visible + span { outline: 2px solid var(--text); outline-offset: 1px; }
|
||||||
|
.open-settings-button { display: grid; width: 100%; grid-template-columns: 28px minmax(0, 1fr); align-items: center; gap: 9px; margin-top: 3px; border: 0; border-top: 1px solid var(--line); padding: 11px 8px 6px; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
|
||||||
|
.open-settings-button:hover { background: var(--soft); }
|
||||||
|
.open-settings-button .ui-icon { color: var(--muted); }
|
||||||
|
.open-settings-button strong, .open-settings-button small { display: block; }
|
||||||
|
.open-settings-button strong { font-size: 11px; }
|
||||||
|
.open-settings-button small { margin-top: 2px; color: var(--muted); font-size: 9px; }
|
||||||
.settings-section-heading { display: grid; gap: 2px; margin-bottom: 2px; }
|
.settings-section-heading { display: grid; gap: 2px; margin-bottom: 2px; }
|
||||||
.settings-section-heading strong { font-size: 12px; }
|
.settings-section-heading strong { font-size: 12px; }
|
||||||
.settings-section-heading small { color: var(--muted); font-size: 9px; }
|
.settings-section-heading small { color: var(--muted); font-size: 9px; }
|
||||||
.model-generation-settings label { display: grid; gap: 5px; color: var(--muted); font-size: 10px; }
|
.settings-page { position: fixed; z-index: 90; inset: 0; display: flex; flex-direction: column; background: var(--bg); }
|
||||||
.model-generation-settings select, .model-generation-settings input[type="number"] { min-width: 0; width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 8px; background: var(--bg); color: var(--text); }
|
.settings-page-header { display: flex; height: 64px; flex: 0 0 auto; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); padding: 0 22px; background: var(--panel); }
|
||||||
.model-generation-settings .generation-check { display: flex; align-items: center; gap: 7px; color: var(--text); }
|
.settings-page-header button { display: grid; width: 34px; height: 34px; place-items: center; border: 0; border-radius: 10px; background: transparent; color: var(--text); cursor: pointer; }
|
||||||
.settings-divider { border-top: 1px solid var(--line); margin: 2px 0; }
|
.settings-page-header button:hover { background: var(--soft); }
|
||||||
.settings-help { margin-top: -6px; color: var(--muted); font-size: 9px; line-height: 1.45; }
|
.settings-page-header > span strong, .settings-page-header > span small { display: block; }
|
||||||
.model-generation-settings button { border: 0; border-radius: 8px; padding: 9px; background: var(--soft); color: var(--text); cursor: pointer; }
|
.settings-page-header > span strong { font-size: 15px; }
|
||||||
.state-card > .model-picker { display: inline-block; margin-bottom: 24px; text-align: left; }
|
.settings-page-header > span small { margin-top: 2px; color: var(--muted); font-size: 9px; }
|
||||||
.state-card > .model-picker > summary { min-width: 260px; }
|
.settings-layout { display: grid; min-height: 0; flex: 1; grid-template-columns: 176px minmax(0, 760px); justify-content: center; gap: 28px; overflow: hidden; padding: 28px 24px; }
|
||||||
|
.settings-nav { display: grid; align-content: start; gap: 3px; }
|
||||||
|
.settings-nav button { border: 0; border-radius: 9px; padding: 9px 11px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 650; text-align: left; cursor: pointer; }
|
||||||
|
.settings-nav button:hover { background: var(--soft); color: var(--text); }
|
||||||
|
.settings-content { display: grid; min-width: 0; align-content: start; overflow-y: auto; gap: 16px; padding: 0 8px 40px 0; scroll-behavior: smooth; }
|
||||||
|
.settings-card { border: 1px solid var(--line); border-radius: 16px; padding: 18px; background: var(--panel); scroll-margin-top: 12px; }
|
||||||
|
.settings-card > header { margin-bottom: 16px; }
|
||||||
|
.settings-card h2, .settings-card h3, .settings-card p { margin: 0; }
|
||||||
|
.settings-card h2 { font-size: 16px; letter-spacing: -.02em; }
|
||||||
|
.settings-card > header p { margin-top: 5px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||||
|
.settings-model-search { display: flex; height: 40px; align-items: center; gap: 8px; border: 1px solid var(--line); border-radius: 10px; padding: 0 11px; background: var(--bg); color: var(--muted); }
|
||||||
|
.settings-model-search input { min-width: 0; width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); }
|
||||||
|
.settings-model-groups { display: grid; gap: 13px; margin-top: 13px; }
|
||||||
|
.settings-model-group { display: grid; gap: 5px; }
|
||||||
|
.settings-model-group h3 { padding: 0 8px; color: var(--muted); font-size: 10px; }
|
||||||
|
.settings-model-group > div { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px; }
|
||||||
|
.settings-model-group .model-option { border: 1px solid transparent; }
|
||||||
|
.settings-model-group .model-option.active { border-color: var(--line); }
|
||||||
|
.settings-empty { color: var(--muted); font-size: 11px; text-align: center; }
|
||||||
|
.settings-card > .effort-control { margin: 0; border: 0; padding: 0; }
|
||||||
|
.settings-card .effort-heading { padding: 0 0 8px; }
|
||||||
|
.settings-card .effort-options { max-width: 440px; padding: 0; }
|
||||||
|
.settings-field-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(160px, .65fr) minmax(160px, .65fr); gap: 10px; margin-top: 18px; }
|
||||||
|
.settings-field-grid > label:not(.settings-check) { display: grid; align-content: start; gap: 6px; color: var(--muted); font-size: 10px; }
|
||||||
|
.settings-field-grid input[type="number"] { min-width: 0; width: 100%; border: 1px solid var(--line); border-radius: 9px; padding: 9px; background: var(--bg); color: var(--text); }
|
||||||
|
.settings-check { display: flex; min-width: 0; align-items: flex-start; gap: 9px; border-radius: 10px; padding: 9px; color: var(--text); cursor: pointer; }
|
||||||
|
.settings-check:hover { background: var(--soft); }
|
||||||
|
.settings-check input { margin-top: 2px; }
|
||||||
|
.settings-check span { min-width: 0; }
|
||||||
|
.settings-check strong, .settings-check small { display: block; }
|
||||||
|
.settings-check strong { font-size: 11px; }
|
||||||
|
.settings-check small { margin-top: 3px; color: var(--muted); font-size: 9px; line-height: 1.45; }
|
||||||
|
.settings-reset-button { margin-top: 14px; border: 0; border-radius: 9px; padding: 9px 11px; background: var(--soft); color: var(--text); cursor: pointer; }
|
||||||
|
.settings-account-link { display: flex; align-items: center; gap: 9px; margin-top: 12px; border: 1px solid var(--line); border-radius: 11px; padding: 11px; color: var(--text); font-size: 11px; font-weight: 650; text-decoration: none; }
|
||||||
|
.settings-account-link:hover { background: var(--soft); }
|
||||||
|
.settings-account-link .ui-icon { color: var(--muted); }
|
||||||
|
.settings-interface-options { display: grid; gap: 4px; }
|
||||||
.history-toggle, .history-heading button, .history-delete { display: grid; place-items: center; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
.history-toggle, .history-heading button, .history-delete { display: grid; place-items: center; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
||||||
.history-toggle { width: 32px; height: 32px; border-radius: 9px; }
|
.history-toggle { width: 32px; height: 32px; border-radius: 9px; }
|
||||||
.history-toggle:hover, .history-heading button:hover, .history-delete:hover { background: var(--soft); }
|
.history-toggle:hover, .history-heading button:hover, .history-delete:hover { background: var(--soft); }
|
||||||
@@ -197,6 +245,7 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.message-error { margin-top: 10px; border: 1px solid rgba(163,56,45,.2); border-radius: 10px; padding: 10px 12px; background: rgba(163,56,45,.07); color: var(--danger); font-size: 13px; }
|
.message-error { margin-top: 10px; border: 1px solid rgba(163,56,45,.2); border-radius: 10px; padding: 10px 12px; background: rgba(163,56,45,.07); color: var(--danger); font-size: 13px; }
|
||||||
.partial-badge { display: inline-flex; max-width: min(240px, 40vw); flex: 0 0 auto; overflow: hidden; border: 1px solid rgba(163,56,45,.22); border-radius: 999px; padding: 3px 8px; background: rgba(163,56,45,.07); color: var(--danger); font-size: 10px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
.partial-badge { display: inline-flex; max-width: min(240px, 40vw); flex: 0 0 auto; overflow: hidden; border: 1px solid rgba(163,56,45,.22); border-radius: 999px; padding: 3px 8px; background: rgba(163,56,45,.07); color: var(--danger); font-size: 10px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.thread-footer { position: sticky; bottom: 0; display: flex; width: min(760px, 100%); flex-direction: column; align-items: center; margin: auto auto 0; padding: 40px 0 16px; background: linear-gradient(to bottom, transparent, var(--bg) 38%); }
|
.thread-footer { position: sticky; bottom: 0; display: flex; width: min(760px, 100%); flex-direction: column; align-items: center; margin: auto auto 0; padding: 40px 0 16px; background: linear-gradient(to bottom, transparent, var(--bg) 38%); }
|
||||||
|
.thread-footer:has(.model-picker[open]) { z-index: 50; }
|
||||||
.edit-context { display: flex; width: calc(100% - 24px); align-items: center; justify-content: space-between; gap: 12px; margin: 0 12px -1px; border: 1px solid var(--line); border-bottom: 0; border-radius: 12px 12px 0 0; padding: 8px 12px 9px; background: var(--soft); color: var(--muted); font-size: 11px; }
|
.edit-context { display: flex; width: calc(100% - 24px); align-items: center; justify-content: space-between; gap: 12px; margin: 0 12px -1px; border: 1px solid var(--line); border-bottom: 0; border-radius: 12px 12px 0 0; padding: 8px 12px 9px; background: var(--soft); color: var(--muted); font-size: 11px; }
|
||||||
.edit-context button { border: 0; background: transparent; color: var(--text); font: inherit; font-weight: 650; cursor: pointer; }
|
.edit-context button { border: 0; background: transparent; color: var(--text); font: inherit; font-weight: 650; cursor: pointer; }
|
||||||
.edit-context + .composer { border-top-left-radius: 10px; border-top-right-radius: 10px; }
|
.edit-context + .composer { border-top-left-radius: 10px; border-top-right-radius: 10px; }
|
||||||
@@ -247,6 +296,8 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.working-panel { display: flex; width: 100%; min-width: 0; align-items: center; justify-content: flex-end; gap: 6px; margin-bottom: 7px; }
|
.working-panel { display: flex; width: 100%; min-width: 0; align-items: center; justify-content: flex-end; gap: 6px; margin-bottom: 7px; }
|
||||||
.working-panel .assistant-reply-toggle { display: flex; min-width: 0; align-items: center; gap: 5px; margin-right: auto; color: var(--text); font-size: 10px; white-space: nowrap; }
|
.working-panel .assistant-reply-toggle { display: flex; min-width: 0; align-items: center; gap: 5px; margin-right: auto; color: var(--text); font-size: 10px; white-space: nowrap; }
|
||||||
.working-panel .assistant-reply-toggle input { margin: 0; }
|
.working-panel .assistant-reply-toggle input { margin: 0; }
|
||||||
|
.working-panel .composer-controls { width: auto; min-width: 0; flex: 1 1 auto; justify-content: flex-end; padding: 0; overflow: hidden; }
|
||||||
|
.working-panel .reply-context { max-width: 100%; }
|
||||||
.working-panel details { position: relative; }
|
.working-panel details { position: relative; }
|
||||||
.working-panel summary { display: flex; height: 28px; align-items: center; border: 1px solid var(--line); border-radius: 9px; padding: 0 9px; background: var(--panel); color: var(--muted); font-size: 10px; cursor: pointer; list-style: none; }
|
.working-panel summary { display: flex; height: 28px; align-items: center; border: 1px solid var(--line); border-radius: 9px; padding: 0 9px; background: var(--panel); color: var(--muted); font-size: 10px; cursor: pointer; list-style: none; }
|
||||||
.working-panel summary::-webkit-details-marker { display: none; }
|
.working-panel summary::-webkit-details-marker { display: none; }
|
||||||
@@ -313,9 +364,7 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.provider-control select { max-width: 105px; }
|
.provider-control select { max-width: 105px; }
|
||||||
.model-picker > summary { min-width: 132px; max-width: 170px; }
|
.model-picker > summary { min-width: 132px; max-width: 170px; }
|
||||||
.picker-icons > svg:first-child { display: none; }
|
.picker-icons > svg:first-child { display: none; }
|
||||||
.model-menu { position: fixed; top: 64px; right: 12px; left: 12px; width: auto; max-height: calc(100dvh - 76px); grid-template-columns: minmax(0, 1fr); overflow-y: auto; }
|
.model-menu { position: fixed; top: 64px; right: 12px; left: 12px; width: auto; max-height: calc(100dvh - 76px); overflow-y: auto; }
|
||||||
.model-browser { flex: none; overflow: visible; }
|
|
||||||
.model-generation-settings { overflow: visible; border-top: 1px solid var(--line); border-left: 0; }
|
|
||||||
.history-sidebar { position: fixed; z-index: 40; top: 0; width: min(300px, 86vw); border-right: 1px solid var(--line); }
|
.history-sidebar { position: fixed; z-index: 40; top: 0; width: min(300px, 86vw); border-right: 1px solid var(--line); }
|
||||||
.history-heading { height: 58px; }
|
.history-heading { height: 58px; }
|
||||||
.history-close { display: grid !important; }
|
.history-close { display: grid !important; }
|
||||||
@@ -324,6 +373,16 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.session-location-help dl > div { grid-template-columns: 76px minmax(0, 1fr); }
|
.session-location-help dl > div { grid-template-columns: 76px minmax(0, 1fr); }
|
||||||
.session-title-template { grid-template-columns: 1fr; gap: 6px; }
|
.session-title-template { grid-template-columns: 1fr; gap: 6px; }
|
||||||
.session-import-actions { grid-template-columns: 1fr; }
|
.session-import-actions { grid-template-columns: 1fr; }
|
||||||
|
.settings-page-header { height: 58px; padding: 0 12px; }
|
||||||
|
.settings-layout { display: flex; flex-direction: column; gap: 12px; padding: 12px 0 0; }
|
||||||
|
.settings-nav { display: flex; flex: 0 0 auto; gap: 2px; overflow-x: auto; padding: 0 12px; }
|
||||||
|
.settings-nav button { flex: 0 0 auto; padding: 8px 10px; }
|
||||||
|
.settings-content { gap: 12px; padding: 0 12px max(28px, env(safe-area-inset-bottom)); }
|
||||||
|
.settings-card { border-radius: 13px; padding: 14px; }
|
||||||
|
.settings-model-group > div { grid-template-columns: 1fr; }
|
||||||
|
.settings-field-grid { grid-template-columns: 1fr; }
|
||||||
|
.local-key-entry { align-items: flex-start; flex-direction: column; }
|
||||||
|
.local-key-entry > div { width: 100%; }
|
||||||
.history-backdrop { position: fixed; z-index: 35; inset: 0; display: block; border: 0; background: rgba(0,0,0,.36); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
|
.history-backdrop { position: fixed; z-index: 35; inset: 0; display: block; border: 0; background: rgba(0,0,0,.36); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
|
||||||
.history-backdrop.open { opacity: 1; pointer-events: auto; }
|
.history-backdrop.open { opacity: 1; pointer-events: auto; }
|
||||||
.with-history.history-open .thread-root { padding-left: 0; }
|
.with-history.history-open .thread-root { padding-left: 0; }
|
||||||
@@ -342,7 +401,7 @@ button, a { -webkit-tap-highlight-color: transparent; }
|
|||||||
.composer-controls select { max-width: 170px; }
|
.composer-controls select { max-width: 170px; }
|
||||||
.composer > .model-picker > summary { width: auto; min-width: 0; max-width: 116px; }
|
.composer > .model-picker > summary { width: auto; min-width: 0; max-width: 116px; }
|
||||||
.composer > .model-picker > summary .picker-label { max-width: 86px; }
|
.composer > .model-picker > summary .picker-label { max-width: 86px; }
|
||||||
.composer > .model-picker .model-menu { top: auto; right: 12px; bottom: max(76px, calc(env(safe-area-inset-bottom) + 70px)); left: 12px; max-height: calc(100dvh - 100px); }
|
.composer > .model-picker .model-menu { top: auto; right: 12px; bottom: max(76px, calc(env(safe-area-inset-bottom) + 70px)); left: 12px; max-height: min(520px, calc(100dvh - 152px - env(safe-area-inset-bottom))); }
|
||||||
.branch-preview-notice { flex-direction: column; align-items: stretch; gap: 8px; margin-bottom: 6px; }
|
.branch-preview-notice { flex-direction: column; align-items: stretch; gap: 8px; margin-bottom: 6px; }
|
||||||
.branch-preview-notice > div { display: grid; width: 100%; grid-template-columns: auto minmax(0, 1fr); }
|
.branch-preview-notice > div { display: grid; width: 100%; grid-template-columns: auto minmax(0, 1fr); }
|
||||||
.branch-preview-notice button { min-width: 0; }
|
.branch-preview-notice button { min-width: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user