Skip to content

Commit 49b086c

Browse files
fix: make model selector label responsive to panel width (#443)
* fix: make model selector label responsive to panel width * Apply suggestion from @DayuanJiang Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> --------- Co-authored-by: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com>
1 parent 27f26d8 commit 49b086c

File tree

1 file changed

+179
-124
lines changed

1 file changed

+179
-124
lines changed

components/model-selector.tsx

Lines changed: 179 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
Server,
99
Settings2,
1010
} from "lucide-react"
11-
import { useMemo, useState } from "react"
11+
import { useEffect, useMemo, useRef, useState } from "react"
1212
import {
1313
ModelSelectorContent,
1414
ModelSelectorEmpty,
@@ -114,134 +114,189 @@ export function ModelSelector({
114114
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
115115
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
116116

117+
const wrapperRef = useRef<HTMLDivElement | null>(null)
118+
const [showLabel, setShowLabel] = useState(true)
119+
120+
// Threshold (px) under which we hide the label (tweak as needed)
121+
const HIDE_THRESHOLD = 240
122+
const SHOW_THRESHOLD = 260
123+
useEffect(() => {
124+
const el = wrapperRef.current
125+
if (!el) return
126+
127+
const target = el.parentElement ?? el
128+
129+
const ro = new ResizeObserver((entries) => {
130+
for (const entry of entries) {
131+
const width = entry.contentRect.width
132+
setShowLabel((prev) => {
133+
// if currently showing and width dropped below hide threshold -> hide
134+
if (prev && width <= HIDE_THRESHOLD) return false
135+
// if currently hidden and width rose above show threshold -> show
136+
if (!prev && width >= SHOW_THRESHOLD) return true
137+
// otherwise keep previous state (hysteresis)
138+
return prev
139+
})
140+
}
141+
})
142+
143+
ro.observe(target)
144+
145+
const initialWidth = target.getBoundingClientRect().width
146+
setShowLabel(initialWidth >= SHOW_THRESHOLD)
147+
148+
return () => ro.disconnect()
149+
}, [])
150+
117151
return (
118-
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
119-
<ModelSelectorTrigger asChild>
120-
<ButtonWithTooltip
121-
tooltipContent={tooltipContent}
122-
variant="ghost"
123-
size="sm"
124-
disabled={disabled}
125-
className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
126-
>
127-
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
128-
<span className="text-xs truncate">
129-
{selectedModel
130-
? selectedModel.modelId
131-
: dict.modelConfig.default}
132-
</span>
133-
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
134-
</ButtonWithTooltip>
135-
</ModelSelectorTrigger>
136-
<ModelSelectorContent title={dict.modelConfig.selectModel}>
137-
<ModelSelectorInput
138-
placeholder={dict.modelConfig.searchModels}
139-
/>
140-
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
141-
<ModelSelectorEmpty>
142-
{displayModels.length === 0 && models.length > 0
143-
? dict.modelConfig.noVerifiedModels
144-
: dict.modelConfig.noModelsFound}
145-
</ModelSelectorEmpty>
146-
147-
{/* Server Default Option */}
148-
<ModelSelectorGroup heading={dict.modelConfig.default}>
149-
<ModelSelectorItem
150-
value="__server_default__"
151-
onSelect={handleSelect}
152-
className={cn(
153-
"cursor-pointer",
154-
!selectedModelId && "bg-accent",
155-
)}
156-
>
157-
<Check
152+
<div ref={wrapperRef} className="inline-block">
153+
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
154+
<ModelSelectorTrigger asChild>
155+
<ButtonWithTooltip
156+
tooltipContent={tooltipContent}
157+
variant="ghost"
158+
size="sm"
159+
disabled={disabled}
160+
className={cn(
161+
"hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
162+
!showLabel && "px-1.5 justify-center",
163+
)}
164+
// accessibility: expose label to screen readers
165+
aria-label={tooltipContent}
166+
>
167+
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
168+
{/* show/hide visible label based on measured width */}
169+
{showLabel ? (
170+
<span className="text-xs truncate">
171+
{selectedModel
172+
? selectedModel.modelId
173+
: dict.modelConfig.default}
174+
</span>
175+
) : (
176+
// Keep an sr-only label for screen readers when hidden
177+
<span className="sr-only">
178+
{selectedModel
179+
? selectedModel.modelId
180+
: dict.modelConfig.default}
181+
</span>
182+
)}
183+
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
184+
</ButtonWithTooltip>
185+
</ModelSelectorTrigger>
186+
187+
<ModelSelectorContent title={dict.modelConfig.selectModel}>
188+
<ModelSelectorInput
189+
placeholder={dict.modelConfig.searchModels}
190+
/>
191+
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
192+
<ModelSelectorEmpty>
193+
{displayModels.length === 0 && models.length > 0
194+
? dict.modelConfig.noVerifiedModels
195+
: dict.modelConfig.noModelsFound}
196+
</ModelSelectorEmpty>
197+
198+
{/* Server Default Option */}
199+
<ModelSelectorGroup heading={dict.modelConfig.default}>
200+
<ModelSelectorItem
201+
value="__server_default__"
202+
onSelect={handleSelect}
158203
className={cn(
159-
"mr-2 h-4 w-4",
160-
!selectedModelId
161-
? "opacity-100"
162-
: "opacity-0",
204+
"cursor-pointer",
205+
!selectedModelId && "bg-accent",
163206
)}
164-
/>
165-
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
166-
<ModelSelectorName>
167-
{dict.modelConfig.serverDefault}
168-
</ModelSelectorName>
169-
</ModelSelectorItem>
170-
</ModelSelectorGroup>
171-
172-
{/* Configured Models by Provider */}
173-
{Array.from(groupedModels.entries()).map(
174-
([
175-
providerLabel,
176-
{ provider, models: providerModels },
177-
]) => (
178-
<ModelSelectorGroup
179-
key={providerLabel}
180-
heading={providerLabel}
181207
>
182-
{providerModels.map((model) => (
183-
<ModelSelectorItem
184-
key={model.id}
185-
value={model.modelId}
186-
onSelect={() => handleSelect(model.id)}
187-
className="cursor-pointer"
188-
>
189-
<Check
190-
className={cn(
191-
"mr-2 h-4 w-4",
192-
selectedModelId === model.id
193-
? "opacity-100"
194-
: "opacity-0",
195-
)}
196-
/>
197-
<ModelSelectorLogo
198-
provider={
199-
PROVIDER_LOGO_MAP[provider] ||
200-
provider
208+
<Check
209+
className={cn(
210+
"mr-2 h-4 w-4",
211+
!selectedModelId
212+
? "opacity-100"
213+
: "opacity-0",
214+
)}
215+
/>
216+
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
217+
<ModelSelectorName>
218+
{dict.modelConfig.serverDefault}
219+
</ModelSelectorName>
220+
</ModelSelectorItem>
221+
</ModelSelectorGroup>
222+
223+
{/* Configured Models by Provider */}
224+
{Array.from(groupedModels.entries()).map(
225+
([
226+
providerLabel,
227+
{ provider, models: providerModels },
228+
]) => (
229+
<ModelSelectorGroup
230+
key={providerLabel}
231+
heading={providerLabel}
232+
>
233+
{providerModels.map((model) => (
234+
<ModelSelectorItem
235+
key={model.id}
236+
value={model.modelId}
237+
onSelect={() =>
238+
handleSelect(model.id)
201239
}
202-
className="mr-2"
203-
/>
204-
<ModelSelectorName>
205-
{model.modelId}
206-
</ModelSelectorName>
207-
{model.validated !== true && (
208-
<span
209-
title={
210-
dict.modelConfig
211-
.unvalidatedModelWarning
240+
className="cursor-pointer"
241+
>
242+
<Check
243+
className={cn(
244+
"mr-2 h-4 w-4",
245+
selectedModelId === model.id
246+
? "opacity-100"
247+
: "opacity-0",
248+
)}
249+
/>
250+
<ModelSelectorLogo
251+
provider={
252+
PROVIDER_LOGO_MAP[
253+
provider
254+
] || provider
212255
}
213-
>
214-
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
215-
</span>
216-
)}
217-
</ModelSelectorItem>
218-
))}
219-
</ModelSelectorGroup>
220-
),
221-
)}
222-
223-
{/* Configure Option */}
224-
<ModelSelectorSeparator />
225-
<ModelSelectorGroup>
226-
<ModelSelectorItem
227-
value="__configure__"
228-
onSelect={handleSelect}
229-
className="cursor-pointer"
230-
>
231-
<Settings2 className="mr-2 h-4 w-4" />
232-
<ModelSelectorName>
233-
{dict.modelConfig.configureModels}
234-
</ModelSelectorName>
235-
</ModelSelectorItem>
236-
</ModelSelectorGroup>
237-
{/* Info text */}
238-
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
239-
{showUnvalidatedModels
240-
? dict.modelConfig.allModelsShown
241-
: dict.modelConfig.onlyVerifiedShown}
242-
</div>
243-
</ModelSelectorList>
244-
</ModelSelectorContent>
245-
</ModelSelectorRoot>
256+
className="mr-2"
257+
/>
258+
<ModelSelectorName>
259+
{model.modelId}
260+
</ModelSelectorName>
261+
{model.validated !== true && (
262+
<span
263+
title={
264+
dict.modelConfig
265+
.unvalidatedModelWarning
266+
}
267+
>
268+
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
269+
</span>
270+
)}
271+
</ModelSelectorItem>
272+
))}
273+
</ModelSelectorGroup>
274+
),
275+
)}
276+
277+
{/* Configure Option */}
278+
<ModelSelectorSeparator />
279+
<ModelSelectorGroup>
280+
<ModelSelectorItem
281+
value="__configure__"
282+
onSelect={handleSelect}
283+
className="cursor-pointer"
284+
>
285+
<Settings2 className="mr-2 h-4 w-4" />
286+
<ModelSelectorName>
287+
{dict.modelConfig.configureModels}
288+
</ModelSelectorName>
289+
</ModelSelectorItem>
290+
</ModelSelectorGroup>
291+
{/* Info text */}
292+
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
293+
{showUnvalidatedModels
294+
? dict.modelConfig.allModelsShown
295+
: dict.modelConfig.onlyVerifiedShown}
296+
</div>
297+
</ModelSelectorList>
298+
</ModelSelectorContent>
299+
</ModelSelectorRoot>
300+
</div>
246301
)
247302
}

0 commit comments

Comments
 (0)