@@ -48,6 +48,7 @@ interface ILLMModels {
4848 value : string ;
4949 tags : string [ ] ;
5050 default ?: boolean ;
51+ provider : string ;
5152}
5253
5354interface ModelAgent {
@@ -110,6 +111,7 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
110111 value : model . entryId ,
111112 tags : model . tags ,
112113 default : model ?. default || false ,
114+ provider : model . provider || '' ,
113115 } ) ,
114116 ) ;
115117
@@ -128,13 +130,9 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
128130 }
129131 } ;
130132
131- if ( isDropdownOpen ) {
132- document . addEventListener ( 'mousedown' , handleClickOutside ) ;
133- }
133+ if ( isDropdownOpen ) document . addEventListener ( 'mousedown' , handleClickOutside ) ;
134134
135- return ( ) => {
136- document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
137- } ;
135+ return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
138136 } , [ isDropdownOpen ] ) ;
139137
140138 /**
@@ -165,6 +163,46 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
165163 setIsDropdownOpen ( ( prev ) => ! prev ) ;
166164 } , [ ] ) ;
167165
166+ // Badge priority order for sorting (lower number = higher priority)
167+ const BADGE_PRIORITY : Record < string , number > = {
168+ enterprise : 1 ,
169+ personal : 2 ,
170+ limited : 3 ,
171+ smythos : 999 , // SmythOS models come last
172+ } ;
173+
174+ /**
175+ * Get badge priority for sorting
176+ * @param tags - Array of model tags
177+ * @returns Priority number (lower = higher priority)
178+ */
179+ const getBadgePriority = ( tags : string [ ] ) => {
180+ return BADGE_PRIORITY [ getTempBadge ( tags ) . toLowerCase ( ) ] || 999 ;
181+ } ;
182+
183+ // Get unique providers and group models by provider
184+ const providers = Array . from ( new Set ( llmModels . map ( ( model ) => model . provider ) ) ) ;
185+ const modelsByProvider = providers . map ( ( provider ) => {
186+ const providerModels = llmModels . filter ( ( model ) => model . provider === provider ) ;
187+
188+ // Sort models with multiple criteria:
189+ // 1. Badge priority (Enterprise > Personal > Limited > SmythOS)
190+ // 2. Then by default flag (default models first)
191+ // 3. Finally alphabetically by label
192+ const sortedModels = [ ...providerModels ] . sort ( ( a , b ) => {
193+ // First priority: Badge priority (Enterprise > Personal > Limited > SmythOS)
194+ const priorityA = getBadgePriority ( a . tags ) ;
195+ const priorityB = getBadgePriority ( b . tags ) ;
196+ if ( priorityA !== priorityB ) return priorityA - priorityB ;
197+ // Second priority: Default models come first
198+ if ( a . default !== b . default ) return a . default ? 1 : - 1 ;
199+ // Third priority: Alphabetically by label
200+ return a . label . localeCompare ( b . label ) ;
201+ } ) ;
202+
203+ return { name : provider , models : sortedModels } ;
204+ } ) ;
205+
168206 return (
169207 < div className = "w-full bg-white border-b border-[#e5e5e5] h-14 flex justify-center absolute top-0 left-0 z-10 px-2.5 lg:px-0" >
170208 < div className = "w-full max-w-4xl flex justify-between items-center" >
@@ -182,7 +220,7 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
182220 ) }
183221 </ figure >
184222
185- < div className = "flex items-start justify-center flex-col" >
223+ < div className = "flex items-start justify-center flex-col w-full " >
186224 { isLoading . agent ? (
187225 < Skeleton
188226 className = { cn (
@@ -197,18 +235,16 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
197235 ) }
198236
199237 { /* Model selection */ }
200- < div className = "flex items-center group" >
238+ < div className = "flex items-center group w-full " >
201239 { isLoading . settings || isModelsLoading ? (
202240 < Skeleton
203241 className = { cn ( 'w-25 h-4 rounded ' , isLoading . settings && 'rounded-t-none' ) }
204242 />
205243 ) : (
206- < div ref = { dropdownRef } className = "relative leading-none" >
244+ < div ref = { dropdownRef } className = "relative leading-none w-full " >
207245 { /* Selected value display - clickable trigger */ }
208246 < Tooltip
209- content = {
210- isModelAgent ? 'Model selection is disabled for model agents' : 'Select model'
211- }
247+ content = { isModelAgent ? 'Default agents have a fixed model' : 'Select model' }
212248 placement = "bottom"
213249 >
214250 < button
@@ -247,27 +283,71 @@ export const ChatHeader: FC<ChatHeaderProps> = (props) => {
247283
248284 { /* Dropdown menu - only show if not a model agent */ }
249285 { isDropdownOpen && ! isModelAgent && (
250- < div className = "absolute top-full -left-3 mt-1 bg-slate-100 border border-slate-200 rounded-md shadow-lg z-50 min-w-[200px] max-h-[300px] overflow-y-auto divide-y divide-slate-200" >
251- { llmModels . map ( ( model ) => {
252- let badge = getTempBadge ( model . tags ) ;
253- badge = badge ? ' (' + badge + ')' : '' ;
254- const isSelected = model . value === currentModel ;
255-
256- return (
257- < button
258- key = { model . value }
259- type = "button"
260- onClick = { ( ) => handleModelChange ( model . value ) }
261- className = { `w-full text-left px-3 py-2 text-xs hover:bg-slate-200 transition-colors focus:outline-none ${
262- isSelected
263- ? 'bg-slate-300 text-slate-800 font-medium'
264- : 'text-slate-600'
265- } `}
266- >
267- { model . label + badge }
268- </ button >
269- ) ;
270- } ) }
286+ < div className = "absolute top-full -left-3 z-50 mt-1 bg-slate-100 rounded-md shadow-xl border-t border-slate-200 min-w-[250px] max-h-[500px] overflow-y-auto divide-y divide-slate-200" >
287+ < div className = "py-1" >
288+ { modelsByProvider . map ( ( provider , providerIndex ) => (
289+ < div key = { providerIndex } className = "mb-2 last:mb-1" >
290+ < div className = "px-4 py-1.5 flex items-center gap-2" >
291+ < span className = "font-semibold text-sm text-slate-900" >
292+ { provider . name }
293+ </ span >
294+ </ div >
295+
296+ < div className = "pl-2" >
297+ { provider . models . map ( ( model , modelIndex ) => {
298+ const badge = getTempBadge ( model . tags ) ;
299+ const isSelected = model . value === currentModel ;
300+
301+ return (
302+ < button
303+ key = { modelIndex }
304+ type = "button"
305+ onClick = { ( ) => handleModelChange ( model . value ) }
306+ className = { cn (
307+ 'w-full px-4 py-1.5 text-left hover:bg-slate-200 transition-colors flex items-center justify-between gap-2' ,
308+ isSelected
309+ ? 'font-semibold bg-slate-200/90 text-slate-900 border-l-2 border-slate-700'
310+ : 'text-slate-700' ,
311+ ) }
312+ >
313+ < span className = "text-sm flex items-center gap-1" >
314+ { model . label }
315+ { badge && (
316+ < span
317+ className = { cn (
318+ 'text-[10px] rounded-full px-1.5' ,
319+ badge === 'SmythOS'
320+ ? 'bg-primary-100/50 text-slate-700'
321+ : 'bg-primary-300 text-slate-700' ,
322+ ) }
323+ >
324+ { badge }
325+ </ span >
326+ ) }
327+ </ span >
328+
329+ { isSelected && (
330+ < svg
331+ className = "w-5 h-5 text-slate-700 shrink-0"
332+ fill = "none"
333+ stroke = "currentColor"
334+ viewBox = "0 0 24 24"
335+ >
336+ < path
337+ strokeLinecap = "round"
338+ strokeLinejoin = "round"
339+ strokeWidth = { 2.5 }
340+ d = "M5 13l4 4L19 7"
341+ />
342+ </ svg >
343+ ) }
344+ </ button >
345+ ) ;
346+ } ) }
347+ </ div >
348+ </ div >
349+ ) ) }
350+ </ div >
271351 </ div >
272352 ) }
273353 </ div >
0 commit comments