feat(web): 命令面板 combobox+可见入口 + sticky 导航壳 + 焦点环统一 + 抽屉锁滚动/关闭按钮 + 主题首帧修复 + 设置徽标/datalist
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
Route,
|
||||
Save,
|
||||
XCircle,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
@@ -49,12 +50,35 @@ interface TestResult {
|
||||
|
||||
type SettingsSection = "routing" | "oauth" | "keys";
|
||||
|
||||
const SECTION_OPTIONS: Array<{ value: SettingsSection; label: string }> = [
|
||||
{ value: "routing", label: "路由" },
|
||||
{ value: "oauth", label: "OAuth" },
|
||||
{ value: "keys", label: "API Key" },
|
||||
// 分组的单一标签源:桌面 aside 与移动 SegmentedControl 共用,消除「档位路由 / 路由」漂移。
|
||||
const SETTINGS_SECTIONS: Array<{
|
||||
value: SettingsSection;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}> = [
|
||||
{ value: "routing", label: "档位路由", icon: Route },
|
||||
{ value: "oauth", label: "OAuth", icon: PlugZap },
|
||||
{ value: "keys", label: "API Key", icon: KeyRound },
|
||||
];
|
||||
|
||||
const SECTION_OPTIONS = SETTINGS_SECTIONS.map(({ value, label }) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
// 档位路由「模型」输入框的候选 model id(datalist 建议,仍可自由输入)。按 provider 分组。
|
||||
const MODEL_SUGGESTIONS: Record<string, readonly string[]> = {
|
||||
anthropic: ["claude-opus-4", "claude-sonnet-4", "claude-3-5-haiku"],
|
||||
deepseek: ["deepseek-chat", "deepseek-reasoner"],
|
||||
kimi: ["moonshot-v1-128k", "moonshot-v1-32k", "kimi-k2"],
|
||||
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
|
||||
qwen: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
||||
glm: ["glm-4-plus", "glm-4-air", "glm-4-flash"],
|
||||
gemini: ["gemini-2.0-flash", "gemini-1.5-pro"],
|
||||
"kimi-code-key": ["kimi-for-coding"],
|
||||
"kimi-code": ["kimi-for-coding"],
|
||||
};
|
||||
|
||||
// 设置页主体(UX §6.10):档位路由(可编辑)+ API-key 凭据行 + Kimi Code OAuth 连接区。
|
||||
export function ProvidersSettings({
|
||||
initial,
|
||||
@@ -76,6 +100,16 @@ export function ProvidersSettings({
|
||||
const maskedFor = (id: string): string | null =>
|
||||
providers.find((p) => p.provider === id)?.masked_key ?? null;
|
||||
|
||||
const sectionDetail = (value: SettingsSection): string => {
|
||||
if (value === "routing") {
|
||||
return `${routing.filter((r) => r.provider && r.model).length}/3 已配置`;
|
||||
}
|
||||
if (value === "oauth") {
|
||||
return kimiOauth.connected ? "Kimi 已连接" : "未连接";
|
||||
}
|
||||
return `${providers.length} 个凭据`;
|
||||
};
|
||||
|
||||
const updateRouting = (tier: string, providerId: string): void => {
|
||||
setRouting((prev) =>
|
||||
prev.map((d) =>
|
||||
@@ -163,28 +197,17 @@ export function ProvidersSettings({
|
||||
return (
|
||||
<div className="grid gap-6 lg:grid-cols-[12rem_1fr]">
|
||||
<aside className="hidden lg:block">
|
||||
<nav aria-label="设置分组" className={cardClass("sticky top-4 p-2")}>
|
||||
<SettingsNavButton
|
||||
active={activeSection === "routing"}
|
||||
icon={<Route className="h-4 w-4" aria-hidden="true" />}
|
||||
label="档位路由"
|
||||
detail={`${routing.filter((r) => r.provider && r.model).length}/3 已配置`}
|
||||
onClick={() => setActiveSection("routing")}
|
||||
/>
|
||||
<SettingsNavButton
|
||||
active={activeSection === "oauth"}
|
||||
icon={<PlugZap className="h-4 w-4" aria-hidden="true" />}
|
||||
label="OAuth"
|
||||
detail={kimiOauth.connected ? "Kimi 已连接" : "未连接"}
|
||||
onClick={() => setActiveSection("oauth")}
|
||||
/>
|
||||
<SettingsNavButton
|
||||
active={activeSection === "keys"}
|
||||
icon={<KeyRound className="h-4 w-4" aria-hidden="true" />}
|
||||
label="API Key"
|
||||
detail={`${providers.length} 个凭据`}
|
||||
onClick={() => setActiveSection("keys")}
|
||||
/>
|
||||
<nav aria-label="设置分组" className={cardClass("sticky top-20 p-2")}>
|
||||
{SETTINGS_SECTIONS.map((section) => (
|
||||
<SettingsNavButton
|
||||
key={section.value}
|
||||
active={activeSection === section.value}
|
||||
icon={<section.icon className="h-4 w-4" aria-hidden="true" />}
|
||||
label={section.label}
|
||||
detail={sectionDetail(section.value)}
|
||||
onClick={() => setActiveSection(section.value)}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -236,15 +259,23 @@ export function ProvidersSettings({
|
||||
<label className="sr-only" htmlFor={`route-model-${row.tier}`}>
|
||||
{TIER_LABELS[row.tier] ?? row.tier} 模型
|
||||
</label>
|
||||
<TextInput
|
||||
id={`route-model-${row.tier}`}
|
||||
value={row.model}
|
||||
onChange={(e) =>
|
||||
updateRoutingModel(row.tier, e.target.value)
|
||||
}
|
||||
placeholder="model"
|
||||
className="font-mono"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<TextInput
|
||||
id={`route-model-${row.tier}`}
|
||||
value={row.model}
|
||||
onChange={(e) =>
|
||||
updateRoutingModel(row.tier, e.target.value)
|
||||
}
|
||||
placeholder="model"
|
||||
className="w-full font-mono"
|
||||
list={`route-models-${row.tier}`}
|
||||
/>
|
||||
<datalist id={`route-models-${row.tier}`}>
|
||||
{(MODEL_SUGGESTIONS[row.provider] ?? []).map((m) => (
|
||||
<option key={m} value={m} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -445,16 +476,13 @@ function CapabilityBadges({ caps }: { caps: CapabilitiesView }) {
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
{badges.map((b) => (
|
||||
<span
|
||||
<Badge
|
||||
key={b.label}
|
||||
className={`rounded border px-2 py-0.5 text-[11px] ${
|
||||
b.on
|
||||
? "border-cinnabar/20 bg-[var(--color-cinnabar-wash)] text-cinnabar"
|
||||
: "border-line bg-bg text-ink-soft/50"
|
||||
}`}
|
||||
variant={b.on ? "accent" : "neutral"}
|
||||
className="text-2xs"
|
||||
>
|
||||
{b.label}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user