11import { Check , ChevronDown , Star } from "lucide-react" ;
22import { useEffect , useRef , useState } from "react" ;
33import { 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" ;
55import { sponsorsByTier } from "@/content/sponsors" ;
66import { 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 */
1719export 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 >
0 commit comments