mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
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>
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
Settings2,
|
Settings2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useMemo, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import {
|
import {
|
||||||
ModelSelectorContent,
|
ModelSelectorContent,
|
||||||
ModelSelectorEmpty,
|
ModelSelectorEmpty,
|
||||||
@@ -114,134 +114,189 @@ export function ModelSelector({
|
|||||||
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
? `${selectedModel.modelId} ${dict.modelConfig.clickToChange}`
|
||||||
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
: `${dict.modelConfig.usingServerDefault} ${dict.modelConfig.clickToChange}`
|
||||||
|
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [showLabel, setShowLabel] = useState(true)
|
||||||
|
|
||||||
|
// Threshold (px) under which we hide the label (tweak as needed)
|
||||||
|
const HIDE_THRESHOLD = 240
|
||||||
|
const SHOW_THRESHOLD = 260
|
||||||
|
useEffect(() => {
|
||||||
|
const el = wrapperRef.current
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const target = el.parentElement ?? el
|
||||||
|
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const width = entry.contentRect.width
|
||||||
|
setShowLabel((prev) => {
|
||||||
|
// if currently showing and width dropped below hide threshold -> hide
|
||||||
|
if (prev && width <= HIDE_THRESHOLD) return false
|
||||||
|
// if currently hidden and width rose above show threshold -> show
|
||||||
|
if (!prev && width >= SHOW_THRESHOLD) return true
|
||||||
|
// otherwise keep previous state (hysteresis)
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ro.observe(target)
|
||||||
|
|
||||||
|
const initialWidth = target.getBoundingClientRect().width
|
||||||
|
setShowLabel(initialWidth >= SHOW_THRESHOLD)
|
||||||
|
|
||||||
|
return () => ro.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
<div ref={wrapperRef} className="inline-block">
|
||||||
<ModelSelectorTrigger asChild>
|
<ModelSelectorRoot open={open} onOpenChange={setOpen}>
|
||||||
<ButtonWithTooltip
|
<ModelSelectorTrigger asChild>
|
||||||
tooltipContent={tooltipContent}
|
<ButtonWithTooltip
|
||||||
variant="ghost"
|
tooltipContent={tooltipContent}
|
||||||
size="sm"
|
variant="ghost"
|
||||||
disabled={disabled}
|
size="sm"
|
||||||
className="hover:bg-accent gap-1.5 h-8 max-w-[180px] px-2"
|
disabled={disabled}
|
||||||
>
|
className={cn(
|
||||||
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
"hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
|
||||||
<span className="text-xs truncate">
|
!showLabel && "px-1.5 justify-center",
|
||||||
{selectedModel
|
)}
|
||||||
? selectedModel.modelId
|
// accessibility: expose label to screen readers
|
||||||
: dict.modelConfig.default}
|
aria-label={tooltipContent}
|
||||||
</span>
|
>
|
||||||
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
<Bot className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||||
</ButtonWithTooltip>
|
{/* show/hide visible label based on measured width */}
|
||||||
</ModelSelectorTrigger>
|
{showLabel ? (
|
||||||
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
<span className="text-xs truncate">
|
||||||
<ModelSelectorInput
|
{selectedModel
|
||||||
placeholder={dict.modelConfig.searchModels}
|
? selectedModel.modelId
|
||||||
/>
|
: dict.modelConfig.default}
|
||||||
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
</span>
|
||||||
<ModelSelectorEmpty>
|
) : (
|
||||||
{displayModels.length === 0 && models.length > 0
|
// Keep an sr-only label for screen readers when hidden
|
||||||
? dict.modelConfig.noVerifiedModels
|
<span className="sr-only">
|
||||||
: dict.modelConfig.noModelsFound}
|
{selectedModel
|
||||||
</ModelSelectorEmpty>
|
? selectedModel.modelId
|
||||||
|
: dict.modelConfig.default}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</ModelSelectorTrigger>
|
||||||
|
|
||||||
{/* Server Default Option */}
|
<ModelSelectorContent title={dict.modelConfig.selectModel}>
|
||||||
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
<ModelSelectorInput
|
||||||
<ModelSelectorItem
|
placeholder={dict.modelConfig.searchModels}
|
||||||
value="__server_default__"
|
/>
|
||||||
onSelect={handleSelect}
|
<ModelSelectorList className="[&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
className={cn(
|
<ModelSelectorEmpty>
|
||||||
"cursor-pointer",
|
{displayModels.length === 0 && models.length > 0
|
||||||
!selectedModelId && "bg-accent",
|
? dict.modelConfig.noVerifiedModels
|
||||||
)}
|
: dict.modelConfig.noModelsFound}
|
||||||
>
|
</ModelSelectorEmpty>
|
||||||
<Check
|
|
||||||
|
{/* Server Default Option */}
|
||||||
|
<ModelSelectorGroup heading={dict.modelConfig.default}>
|
||||||
|
<ModelSelectorItem
|
||||||
|
value="__server_default__"
|
||||||
|
onSelect={handleSelect}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"cursor-pointer",
|
||||||
!selectedModelId
|
!selectedModelId && "bg-accent",
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<ModelSelectorName>
|
|
||||||
{dict.modelConfig.serverDefault}
|
|
||||||
</ModelSelectorName>
|
|
||||||
</ModelSelectorItem>
|
|
||||||
</ModelSelectorGroup>
|
|
||||||
|
|
||||||
{/* Configured Models by Provider */}
|
|
||||||
{Array.from(groupedModels.entries()).map(
|
|
||||||
([
|
|
||||||
providerLabel,
|
|
||||||
{ provider, models: providerModels },
|
|
||||||
]) => (
|
|
||||||
<ModelSelectorGroup
|
|
||||||
key={providerLabel}
|
|
||||||
heading={providerLabel}
|
|
||||||
>
|
>
|
||||||
{providerModels.map((model) => (
|
<Check
|
||||||
<ModelSelectorItem
|
className={cn(
|
||||||
key={model.id}
|
"mr-2 h-4 w-4",
|
||||||
value={model.modelId}
|
!selectedModelId
|
||||||
onSelect={() => handleSelect(model.id)}
|
? "opacity-100"
|
||||||
className="cursor-pointer"
|
: "opacity-0",
|
||||||
>
|
)}
|
||||||
<Check
|
/>
|
||||||
className={cn(
|
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
"mr-2 h-4 w-4",
|
<ModelSelectorName>
|
||||||
selectedModelId === model.id
|
{dict.modelConfig.serverDefault}
|
||||||
? "opacity-100"
|
</ModelSelectorName>
|
||||||
: "opacity-0",
|
</ModelSelectorItem>
|
||||||
)}
|
</ModelSelectorGroup>
|
||||||
/>
|
|
||||||
<ModelSelectorLogo
|
|
||||||
provider={
|
|
||||||
PROVIDER_LOGO_MAP[provider] ||
|
|
||||||
provider
|
|
||||||
}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<ModelSelectorName>
|
|
||||||
{model.modelId}
|
|
||||||
</ModelSelectorName>
|
|
||||||
{model.validated !== true && (
|
|
||||||
<span
|
|
||||||
title={
|
|
||||||
dict.modelConfig
|
|
||||||
.unvalidatedModelWarning
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ModelSelectorItem>
|
|
||||||
))}
|
|
||||||
</ModelSelectorGroup>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Configure Option */}
|
{/* Configured Models by Provider */}
|
||||||
<ModelSelectorSeparator />
|
{Array.from(groupedModels.entries()).map(
|
||||||
<ModelSelectorGroup>
|
([
|
||||||
<ModelSelectorItem
|
providerLabel,
|
||||||
value="__configure__"
|
{ provider, models: providerModels },
|
||||||
onSelect={handleSelect}
|
]) => (
|
||||||
className="cursor-pointer"
|
<ModelSelectorGroup
|
||||||
>
|
key={providerLabel}
|
||||||
<Settings2 className="mr-2 h-4 w-4" />
|
heading={providerLabel}
|
||||||
<ModelSelectorName>
|
>
|
||||||
{dict.modelConfig.configureModels}
|
{providerModels.map((model) => (
|
||||||
</ModelSelectorName>
|
<ModelSelectorItem
|
||||||
</ModelSelectorItem>
|
key={model.id}
|
||||||
</ModelSelectorGroup>
|
value={model.modelId}
|
||||||
{/* Info text */}
|
onSelect={() =>
|
||||||
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
handleSelect(model.id)
|
||||||
{showUnvalidatedModels
|
}
|
||||||
? dict.modelConfig.allModelsShown
|
className="cursor-pointer"
|
||||||
: dict.modelConfig.onlyVerifiedShown}
|
>
|
||||||
</div>
|
<Check
|
||||||
</ModelSelectorList>
|
className={cn(
|
||||||
</ModelSelectorContent>
|
"mr-2 h-4 w-4",
|
||||||
</ModelSelectorRoot>
|
selectedModelId === model.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ModelSelectorLogo
|
||||||
|
provider={
|
||||||
|
PROVIDER_LOGO_MAP[
|
||||||
|
provider
|
||||||
|
] || provider
|
||||||
|
}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<ModelSelectorName>
|
||||||
|
{model.modelId}
|
||||||
|
</ModelSelectorName>
|
||||||
|
{model.validated !== true && (
|
||||||
|
<span
|
||||||
|
title={
|
||||||
|
dict.modelConfig
|
||||||
|
.unvalidatedModelWarning
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ModelSelectorItem>
|
||||||
|
))}
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Configure Option */}
|
||||||
|
<ModelSelectorSeparator />
|
||||||
|
<ModelSelectorGroup>
|
||||||
|
<ModelSelectorItem
|
||||||
|
value="__configure__"
|
||||||
|
onSelect={handleSelect}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Settings2 className="mr-2 h-4 w-4" />
|
||||||
|
<ModelSelectorName>
|
||||||
|
{dict.modelConfig.configureModels}
|
||||||
|
</ModelSelectorName>
|
||||||
|
</ModelSelectorItem>
|
||||||
|
</ModelSelectorGroup>
|
||||||
|
{/* Info text */}
|
||||||
|
<div className="px-3 py-2 text-xs text-muted-foreground border-t">
|
||||||
|
{showUnvalidatedModels
|
||||||
|
? dict.modelConfig.allModelsShown
|
||||||
|
: dict.modelConfig.onlyVerifiedShown}
|
||||||
|
</div>
|
||||||
|
</ModelSelectorList>
|
||||||
|
</ModelSelectorContent>
|
||||||
|
</ModelSelectorRoot>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user