Files
writer-work-flow/apps/web/components/settings/RoutingPanel.tsx
Yaojia Wang 67e30a6863 feat(ui): P3 页面级应用(作品库卡片/空态/加载/设置/次级页)
- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) +
  元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷
- EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容)
- loading 骨架化(Skeleton 拼作品库结构,motion-safe)
- 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot
- 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
2026-07-11 07:36:32 +02:00

127 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 iddatalist 建议,仍可自由输入)。按 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+modelUX §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>
);
}