All files / apps/web/src/components ModelSelector.jsx

0% Statements 0/22
0% Branches 0/21
0% Functions 0/10
0% Lines 0/17

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>
  );
}