Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import { useState, useRef, useEffect } from 'react';
import { Search, ChevronDown, Brain, Loader2 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import api from '../services/api';
export default function ModelSelector({ value, onChange }) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const ref = useRef(null);
const { data: models = [], isLoading } = useQuery({
queryKey: ['llm-models'],
queryFn: async () => {
const res = await api.get('/llm/models');
return res.data.data || [];
},
staleTime: 300000,
});
const filtered = models.filter(m =>
m.toLowerCase().includes(search.toLowerCase())
);
useEffect(() => {
const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-3 py-2 border border-gray-300 rounded-lg bg-white hover:border-gray-400 text-sm"
>
<span className="truncate text-gray-900">{value || 'Sélectionner un modèle'}</span>
<ChevronDown className={`w-4 h-4 text-gray-400 ml-2 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div className="absolute z-50 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg">
<div className="flex items-center gap-2 px-3 py-2 border-b border-gray-100">
<Search className="w-4 h-4 text-gray-400 shrink-0" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Rechercher un modèle..."
className="w-full text-sm outline-none placeholder-gray-400"
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto">
{isLoading ? (
<div className="flex items-center justify-center gap-2 py-6 text-sm text-gray-400">
<Loader2 className="w-4 h-4 animate-spin" />
Chargement...
</div>
) : filtered.length === 0 ? (
<div className="py-6 text-center text-sm text-gray-400">
{search ? 'Aucun modèle trouvé' : 'Aucun modèle disponible'}
</div>
) : (
filtered.map((model) => (
<button
key={model}
type="button"
onClick={() => { onChange(model); setOpen(false); setSearch(''); }}
className={`w-full text-left px-3 py-2 text-sm hover:bg-indigo-50 flex items-center gap-2 ${
model === value ? 'bg-indigo-50 text-indigo-700 font-medium' : 'text-gray-700'
}`}
>
<Brain className="w-3.5 h-3.5 shrink-0 text-gray-400" />
<span className="truncate">{model}</span>
</button>
))
)}
</div>
</div>
)}
</div>
);
}
|