"use client";
import { Check, ChevronDown } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { AUTO_MODEL, type ChatModel } from "@/lib/chat/models";
import type { ModelId } from "@/lib/chat/types";
type ModelSelectorProps = {
models: ChatModel[];
modelId: ModelId;
onChange: (id: ModelId) => void;
};
export function ModelSelector({ models, modelId, onChange }: ModelSelectorProps) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);
const selected =
models.find((model) => model.id === modelId) ??
models.find((model) => model.id === "auto") ??
models[0] ??
AUTO_MODEL;
useEffect(() => {
if (!open) return;
function handlePointerDown(event: PointerEvent) {
if (!containerRef.current?.contains(event.target as Node)) {
setOpen(false);
}
}
function handleKeyDown(event: globalThis.KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
setOpen(false);
triggerRef.current?.focus();
return;
}
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
event.preventDefault();
const items = itemRefs.current.filter((item): item is HTMLButtonElement => !!item);
if (!items.length) return;
const current = items.indexOf(document.activeElement as HTMLButtonElement);
const next =
event.key === "Home"
? 0
: event.key === "End"
? items.length - 1
: event.key === "ArrowDown"
? (current + 1) % items.length
: (current <= 0 ? items.length : current) - 1;
items[next]?.focus();
}
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [open]);
return (
<div className="relative" ref={containerRef}>
<Button
aria-expanded={open}
aria-haspopup="menu"
aria-label="Выбрать модель"
className="min-w-32 justify-between"
onClick={() => setOpen((current) => !current)}
ref={triggerRef}
type="button"
variant="ghost"
>
<span>{selected.name}</span>
<ChevronDown aria-hidden="true" className="size-4" />
</Button>
{open ? (
<div
aria-label="Доступные модели"
className="absolute left-0 top-full z-30 mt-2 w-72 rounded-xl border bg-popover p-1.5 text-popover-foreground shadow-lg"
role="menu"
>
{models.map((model, index) => (
<button
className="flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-left outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
key={model.id}
onClick={() => {
onChange(model.id);
setOpen(false);
}}
ref={(element) => {
itemRefs.current[index] = element;
}}
role="menuitem"
type="button"
>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">{model.name}</span>
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
{model.description}
</span>
</span>
{model.id === modelId ? (
<Check aria-hidden="true" className="mt-0.5 size-4" />
) : null}
</button>
))}
</div>
) : null}
</div>
);
}