Skip to content

Commit 807facd

Browse files
committed
feat(studio): 顶部 provider 下拉分组展示 + 纳入远程清单赞助商
- 14+ 项平铺改为分组:聚合平台(赞助商在列)/模型公司/本地 CLI/本地模型/ 自定义,"想配某家模型公司的 API"一眼能找到 - 修复:远程清单上架的赞助商此前只出现在面板,不在顶部下拉里,无法切换; 现已纳入聚合平台组,赞助商标同步显示 - 注册表加 vendor 标区分模型公司官方 API 与聚合平台
1 parent 226b755 commit 807facd

3 files changed

Lines changed: 90 additions & 50 deletions

File tree

website/src/components/studio/ProviderSelect.tsx

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Check, ChevronDown, Star } from "lucide-react";
22
import { useEffect, useRef, useState } from "react";
33
import { useLanguage } from "@/i18n/LanguageProvider";
4-
import { api, API_PROVIDERS, PROVIDER_LABELS, PROVIDERS, type CustomProviderMeta } from "@/lib/studio";
4+
import { api, API_PROVIDERS, CLI_PROVIDER_IDS, PROVIDER_LABELS, type CustomProviderMeta, type RemoteProviderMeta } from "@/lib/studio";
55
import { sponsorsByTier } from "@/content/sponsors";
66
import { cn } from "@/lib/utils";
77

@@ -13,17 +13,23 @@ const SPONSOR_IDS = API_PROVIDERS.filter((p) => p.sponsor).map((p) => p.id);
1313
/**
1414
* Studio 顶部 provider 选择器。原生 <select> 无法给单个选项上色/加徽章,
1515
* 这里用一个轻量自定义下拉,把旗舰赞助商(APINEBULA)金色高亮 + 星标 + 旗舰徽章并置顶。
16+
* 选项按「聚合平台 / 模型公司 / 本地 CLI / 本地模型 / 自定义」分组——14+ 项平铺看不懂,
17+
* 分组后"想配某家模型公司的 API"一眼能找到。
1618
*/
1719
export function ProviderSelect({ value, onChange }: { value: string; onChange: (p: string) => void }) {
1820
const { t } = useLanguage();
1921
const [open, setOpen] = useState(false);
20-
// 用户在供应商面板加的自定义供应商也要能在这里切换/显示名称,不能只认静态 PROVIDERS 列表。
22+
// 自定义/远程清单上架的供应商也要能在这里切换/显示名称,不能只认静态 PROVIDERS 列表。
2123
// 演示站没有引擎后端时拉不到 config —— 静默回退为空列表,下拉里只有内置 provider。
2224
const [customProviders, setCustomProviders] = useState<CustomProviderMeta[]>([]);
25+
const [remoteProviders, setRemoteProviders] = useState<RemoteProviderMeta[]>([]);
2326
const ref = useRef<HTMLDivElement>(null);
2427

2528
useEffect(() => {
26-
api.config().then((c) => setCustomProviders(c.customProviders ?? [])).catch(() => {});
29+
api.config().then((c) => {
30+
setCustomProviders(c.customProviders ?? []);
31+
setRemoteProviders(c.remoteProviders ?? []);
32+
}).catch(() => {});
2733
}, []);
2834

2935
useEffect(() => {
@@ -41,12 +47,25 @@ export function ProviderSelect({ value, onChange }: { value: string; onChange: (
4147
}, [open]);
4248

4349
const labelFor = (p: string) =>
44-
p === "" ? t.studio.shell.providerDefault : PROVIDER_LABELS[p] ?? customProviders.find((c) => c.id === p)?.name ?? p;
50+
p === "" ? t.studio.shell.providerDefault
51+
: PROVIDER_LABELS[p]
52+
?? customProviders.find((c) => c.id === p)?.name
53+
?? remoteProviders.find((r) => r.id === p)?.name
54+
?? p;
4555
const isFlagship = (p: string) => !!FLAGSHIP_ID && p === FLAGSHIP_ID;
46-
const isSponsor = (p: string) => SPONSOR_IDS.includes(p);
56+
const isSponsor = (p: string) => SPONSOR_IDS.includes(p) || !!remoteProviders.find((r) => r.id === p)?.sponsor;
4757
const selectedFlagship = isFlagship(value);
48-
// 自定义供应商排在内置列表之后(去重:万一自定义 id 撞了内置 id,以内置为准)
49-
const allProviders = [...PROVIDERS, ...customProviders.map((c) => c.id).filter((id) => !PROVIDERS.includes(id))];
58+
59+
const g = t.studio.providers;
60+
const groups: { label: string; ids: string[] }[] = [
61+
// 聚合平台:内置聚合商(旗舰/赞助商在前) + 远程清单上架的赞助商
62+
{ label: g.groupAggregators, ids: [...API_PROVIDERS.filter((p) => !p.vendor).map((p) => p.id), ...remoteProviders.map((r) => r.id)] },
63+
// 模型公司官方 API
64+
{ label: g.groupVendors, ids: API_PROVIDERS.filter((p) => p.vendor).map((p) => p.id) },
65+
{ label: g.groupCli, ids: [...CLI_PROVIDER_IDS] },
66+
{ label: g.groupLocal, ids: ["ollama"] },
67+
...(customProviders.length > 0 ? [{ label: g.groupCustom, ids: customProviders.map((c) => c.id) }] : []),
68+
];
5069

5170
return (
5271
<div className="relative" ref={ref}>
@@ -67,46 +86,55 @@ export function ProviderSelect({ value, onChange }: { value: string; onChange: (
6786
</button>
6887

6988
{open && (
70-
<div className="absolute right-0 z-50 mt-1.5 max-h-[70vh] w-60 overflow-auto rounded-xl border border-border/70 bg-card p-1 shadow-xl">
71-
{allProviders.map((p) => {
72-
const flag = isFlagship(p);
73-
const sponsor = isSponsor(p);
74-
const on = p === value;
75-
return (
76-
<button
77-
key={p}
78-
type="button"
79-
onClick={() => {
80-
onChange(p);
81-
setOpen(false);
82-
}}
83-
className={cn(
84-
"flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors",
85-
flag ? "font-semibold text-gold hover:bg-gold/10" : "text-foreground hover:bg-muted",
86-
on && !flag && "bg-muted",
87-
on && flag && "bg-gold/10",
88-
)}
89-
>
90-
{flag ? (
91-
<Star className="size-4 shrink-0 fill-gold text-gold" />
92-
) : (
93-
<span className="size-4 shrink-0" />
94-
)}
95-
<span className="min-w-0 flex-1 truncate">{labelFor(p)}</span>
96-
{flag && (
97-
<span className="shrink-0 rounded-full bg-gold/15 px-1.5 py-0.5 text-[10px] font-semibold text-gold">
98-
{t.studio.providers.flagshipTag}
99-
</span>
100-
)}
101-
{sponsor && (
102-
<span className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
103-
{t.studio.providers.sponsorTag}
104-
</span>
105-
)}
106-
{on && <Check className="size-4 shrink-0 text-gold" />}
107-
</button>
108-
);
109-
})}
89+
<div className="absolute right-0 z-50 mt-1.5 max-h-[70vh] w-64 overflow-auto rounded-xl border border-border/70 bg-card p-1 shadow-xl">
90+
{groups.map((group) =>
91+
group.ids.length === 0 ? null : (
92+
<div key={group.label}>
93+
<div className="px-2.5 pb-1 pt-2 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
94+
{group.label}
95+
</div>
96+
{group.ids.map((p) => {
97+
const flag = isFlagship(p);
98+
const sponsor = isSponsor(p);
99+
const on = p === value;
100+
return (
101+
<button
102+
key={p}
103+
type="button"
104+
onClick={() => {
105+
onChange(p);
106+
setOpen(false);
107+
}}
108+
className={cn(
109+
"flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors",
110+
flag ? "font-semibold text-gold hover:bg-gold/10" : "text-foreground hover:bg-muted",
111+
on && !flag && "bg-muted",
112+
on && flag && "bg-gold/10",
113+
)}
114+
>
115+
{flag ? (
116+
<Star className="size-4 shrink-0 fill-gold text-gold" />
117+
) : (
118+
<span className="size-4 shrink-0" />
119+
)}
120+
<span className="min-w-0 flex-1 truncate">{labelFor(p)}</span>
121+
{flag && (
122+
<span className="shrink-0 rounded-full bg-gold/15 px-1.5 py-0.5 text-[10px] font-semibold text-gold">
123+
{t.studio.providers.flagshipTag}
124+
</span>
125+
)}
126+
{sponsor && (
127+
<span className="shrink-0 rounded-full bg-gold/15 px-1.5 py-0.5 text-[10px] font-semibold text-gold">
128+
{t.studio.providers.sponsorTag}
129+
</span>
130+
)}
131+
{on && <Check className="size-4 shrink-0 text-gold" />}
132+
</button>
133+
);
134+
})}
135+
</div>
136+
),
137+
)}
110138
</div>
111139
)}
112140
</div>

website/src/i18n/translations.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ const zh = {
191191
registerCta: "注册领取 Key",
192192
modelSelectTitle: "切换模型(保存为该供应商的默认模型)",
193193
modelDefaultLabel: "默认模型",
194+
groupAggregators: "聚合平台",
195+
groupVendors: "模型公司",
196+
groupCli: "本地 CLI",
197+
groupLocal: "本地模型",
198+
groupCustom: "自定义",
194199
modelsLoading: "拉取模型列表…",
195200
modelsEmpty: "拉取失败或无可用模型,可去配置页手填",
196201
idImmutableHint: "标识不可修改(被已保存的 key 和工作流引用)",
@@ -724,6 +729,11 @@ const en: typeof zh = {
724729
registerCta: "Sign up & get a key",
725730
modelSelectTitle: "Switch model (saved as this provider's default)",
726731
modelDefaultLabel: "Default model",
732+
groupAggregators: "Aggregators",
733+
groupVendors: "Model vendors",
734+
groupCli: "Local CLI",
735+
groupLocal: "Local",
736+
groupCustom: "Custom",
727737
modelsLoading: "Fetching models…",
728738
modelsEmpty: "Fetch failed or no models; set one manually in Configure",
729739
idImmutableHint: "The identifier cannot be changed (referenced by saved keys and workflows)",

website/src/lib/studio.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,8 @@ export interface ApiProviderMeta {
517517
defaultBaseUrl?: string;
518518
/** 注册/领取 key 的直达链接(赞助商带推广参数) */
519519
signupUrl?: string;
520+
/** true=模型公司官方 API(如 DeepSeek/Anthropic/OpenAI),false/缺省=聚合平台 */
521+
vendor?: boolean;
520522
flagship?: boolean;
521523
sponsor?: boolean;
522524
modelSuggestions?: string[];
@@ -529,9 +531,9 @@ export const API_PROVIDERS: ApiProviderMeta[] = [
529531
{ id: "compshare", name: "CompShare", hint: "console.compshare.cn", defaultBaseUrl: "https://api.modelverse.cn/v1", signupUrl: "https://passport.compshare.cn/register?referral_code=ETD3L5JBM13CtKARkMORot&ytag=GPU_YY_YX_git_agency-agents", sponsor: true, modelSuggestions: ["deepseek-ai/DeepSeek-R1", "deepseek-ai/DeepSeek-V3"] },
530532
// 普通赞助商 RootFlowAI —— 前 3 位,紧跟两家旗舰/赞助商之后
531533
{ id: "rootflowai", name: "RootFlowAI", hint: "rootflowai.com", defaultBaseUrl: "https://api.rootflowai.com/v1", signupUrl: "https://rootflowai.com/register?utm_source=agency-agents-zh&utm_medium=sponsor&utm_campaign=studio", sponsor: true, modelSuggestions: ["claude-sonnet-4-6", "claude-opus-4-7", "gpt-5.5", "gemini-3.1-pro-preview"] },
532-
{ id: "deepseek", name: "DeepSeek", hint: "platform.deepseek.com", defaultBaseUrl: "https://api.deepseek.com/v1", modelSuggestions: ["deepseek-chat", "deepseek-reasoner"] },
533-
{ id: "claude", name: "Claude (Anthropic)", shortName: "Claude", hint: "console.anthropic.com", defaultBaseUrl: "https://api.anthropic.com/v1", modelSuggestions: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-5-sonnet-20241022"] },
534-
{ id: "openai", name: "OpenAI", hint: "gpt-4o {etc} · platform.openai.com", defaultBaseUrl: "https://api.openai.com/v1", modelSuggestions: ["gpt-4o", "gpt-4o-mini", "o1", "o3-mini", "gpt-4.1"] },
534+
{ id: "deepseek", name: "DeepSeek", hint: "platform.deepseek.com", defaultBaseUrl: "https://api.deepseek.com/v1", vendor: true, modelSuggestions: ["deepseek-chat", "deepseek-reasoner"] },
535+
{ id: "claude", name: "Claude (Anthropic)", shortName: "Claude", hint: "console.anthropic.com", defaultBaseUrl: "https://api.anthropic.com/v1", vendor: true, modelSuggestions: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-5-sonnet-20241022"] },
536+
{ id: "openai", name: "OpenAI", hint: "gpt-4o {etc} · platform.openai.com", defaultBaseUrl: "https://api.openai.com/v1", vendor: true, modelSuggestions: ["gpt-4o", "gpt-4o-mini", "o1", "o3-mini", "gpt-4.1"] },
535537
{ id: "agnes", name: "Agnes AI", hint: "agnes-2.0-flash · agnes-ai.com", defaultBaseUrl: "https://apihub.agnes-ai.com/v1", modelSuggestions: ["agnes-2.0-flash", "agnes-1.5-flash"] },
536538
];
537539

0 commit comments

Comments
 (0)