feat(ui): P3 页面级应用(作品库卡片/空态/加载/设置/次级页)
- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) + 元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷 - EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容) - loading 骨架化(Skeleton 拼作品库结构,motion-safe) - 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot - 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
This commit is contained in:
126
apps/web/components/settings/RoutingPanel.tsx
Normal file
126
apps/web/components/settings/RoutingPanel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { Save } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||
import { Select } from "@/components/ui/Select";
|
||||
import { StatusNote } from "@/components/ui/StatusNote";
|
||||
import { TextInput } from "@/components/ui/TextInput";
|
||||
import {
|
||||
KNOWN_PROVIDERS,
|
||||
TIER_LABELS,
|
||||
type RoutingDraft,
|
||||
} from "@/lib/settings/providers";
|
||||
|
||||
// 档位路由「模型」输入框的候选 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"],
|
||||
};
|
||||
|
||||
interface RoutingPanelProps {
|
||||
routing: RoutingDraft[];
|
||||
saving: boolean;
|
||||
onProviderChange: (tier: string, providerId: string) => void;
|
||||
onModelChange: (tier: string, model: string) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
// 能力档位路由面板:写手/分析/轻量三档各指向一个 provider+model(UX §6.10)。
|
||||
export function RoutingPanel({
|
||||
routing,
|
||||
saving,
|
||||
onProviderChange,
|
||||
onModelChange,
|
||||
onSave,
|
||||
}: RoutingPanelProps) {
|
||||
return (
|
||||
<section className="min-w-0">
|
||||
<SectionHeader
|
||||
eyebrow="路由"
|
||||
title="能力档位路由"
|
||||
description="写手、分析、轻量三类能力可分别指向不同模型。保存时只提交完整填写的行。"
|
||||
/>
|
||||
<StatusNote className="mt-3" variant="info">
|
||||
写章、审稿和摘要提炼会分别读取对应档位;未完整填写的行不会覆盖现有路由。
|
||||
</StatusNote>
|
||||
<ul className="mt-4 divide-y divide-line rounded border border-line bg-panel">
|
||||
{routing.map((row) => (
|
||||
<RoutingRow
|
||||
key={row.tier}
|
||||
row={row}
|
||||
onProviderChange={onProviderChange}
|
||||
onModelChange={onModelChange}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<Save className="h-4 w-4" aria-hidden="true" />
|
||||
{saving ? "保存中…" : "保存档位路由"}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface RoutingRowProps {
|
||||
row: RoutingDraft;
|
||||
onProviderChange: (tier: string, providerId: string) => void;
|
||||
onModelChange: (tier: string, model: string) => void;
|
||||
}
|
||||
|
||||
function RoutingRow({ row, onProviderChange, onModelChange }: RoutingRowProps) {
|
||||
const tierLabel = TIER_LABELS[row.tier] ?? row.tier;
|
||||
return (
|
||||
<li className="grid gap-3 px-4 py-3 text-sm md:grid-cols-[6rem_minmax(10rem,14rem)_1fr]">
|
||||
<span className="self-center text-ink">{tierLabel}</span>
|
||||
<label className="sr-only" htmlFor={`route-provider-${row.tier}`}>
|
||||
{tierLabel} 提供商
|
||||
</label>
|
||||
<Select
|
||||
id={`route-provider-${row.tier}`}
|
||||
value={row.provider}
|
||||
onChange={(e) => onProviderChange(row.tier, e.target.value)}
|
||||
>
|
||||
<option value="">(未配置)</option>
|
||||
{KNOWN_PROVIDERS.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<label className="sr-only" htmlFor={`route-model-${row.tier}`}>
|
||||
{tierLabel} 模型
|
||||
</label>
|
||||
<div className="min-w-0">
|
||||
<TextInput
|
||||
id={`route-model-${row.tier}`}
|
||||
value={row.model}
|
||||
onChange={(e) => onModelChange(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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user