CoolFace
Apppublic

legends810/testingnew

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
ModelSelector.tsx313 linesDownload Raw Back to chat
1import type { ProviderInfo } from '~/types/model';2import { useEffect, useState, useRef } from 'react';3import type { KeyboardEvent } from 'react';4import type { ModelInfo } from '~/lib/modules/llm/types';5import { classNames } from '~/utils/classNames';6import * as React from 'react';7 8interface ModelSelectorProps {9  model?: string;10  setModel?: (model: string) => void;11  provider?: ProviderInfo;12  setProvider?: (provider: ProviderInfo) => void;13  modelList: ModelInfo[];14  providerList: ProviderInfo[];15  apiKeys: Record<string, string>;16  modelLoading?: string;17}18 19export const ModelSelector = ({20  model,21  setModel,22  provider,23  setProvider,24  modelList,25  providerList,26  modelLoading,27}: ModelSelectorProps) => {28  const [modelSearchQuery, setModelSearchQuery] = useState('');29  const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);30  const [focusedIndex, setFocusedIndex] = useState(-1);31  const searchInputRef = useRef<HTMLInputElement>(null);32  const optionsRef = useRef<(HTMLDivElement | null)[]>([]);33  const dropdownRef = useRef<HTMLDivElement>(null);34 35  useEffect(() => {36    const handleClickOutside = (event: MouseEvent) => {37      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {38        setIsModelDropdownOpen(false);39        setModelSearchQuery('');40      }41    };42 43    document.addEventListener('mousedown', handleClickOutside);44 45    return () => document.removeEventListener('mousedown', handleClickOutside);46  }, []);47 48  // Filter models based on search query49  const filteredModels = [...modelList]50    .filter((e) => e.provider === provider?.name && e.name)51    .filter(52      (model) =>53        model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||54        model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()),55    );56 57  // Reset focused index when search query changes or dropdown opens/closes58  useEffect(() => {59    setFocusedIndex(-1);60  }, [modelSearchQuery, isModelDropdownOpen]);61 62  // Focus search input when dropdown opens63  useEffect(() => {64    if (isModelDropdownOpen && searchInputRef.current) {65      searchInputRef.current.focus();66    }67  }, [isModelDropdownOpen]);68 69  // Handle keyboard navigation70  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {71    if (!isModelDropdownOpen) {72      return;73    }74 75    switch (e.key) {76      case 'ArrowDown':77        e.preventDefault();78        setFocusedIndex((prev) => {79          const next = prev + 1;80 81          if (next >= filteredModels.length) {82            return 0;83          }84 85          return next;86        });87        break;88 89      case 'ArrowUp':90        e.preventDefault();91        setFocusedIndex((prev) => {92          const next = prev - 1;93 94          if (next < 0) {95            return filteredModels.length - 1;96          }97 98          return next;99        });100        break;101 102      case 'Enter':103        e.preventDefault();104 105        if (focusedIndex >= 0 && focusedIndex < filteredModels.length) {106          const selectedModel = filteredModels[focusedIndex];107          setModel?.(selectedModel.name);108          setIsModelDropdownOpen(false);109          setModelSearchQuery('');110        }111 112        break;113 114      case 'Escape':115        e.preventDefault();116        setIsModelDropdownOpen(false);117        setModelSearchQuery('');118        break;119 120      case 'Tab':121        if (!e.shiftKey && focusedIndex === filteredModels.length - 1) {122          setIsModelDropdownOpen(false);123        }124 125        break;126    }127  };128 129  // Focus the selected option130  useEffect(() => {131    if (focusedIndex >= 0 && optionsRef.current[focusedIndex]) {132      optionsRef.current[focusedIndex]?.scrollIntoView({ block: 'nearest' });133    }134  }, [focusedIndex]);135 136  // Update enabled providers when cookies change137  useEffect(() => {138    // If current provider is disabled, switch to first enabled provider139    if (providerList.length === 0) {140      return;141    }142 143    if (provider && !providerList.map((p) => p.name).includes(provider.name)) {144      const firstEnabledProvider = providerList[0];145      setProvider?.(firstEnabledProvider);146 147      // Also update the model to the first available one for the new provider148      const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);149 150      if (firstModel) {151        setModel?.(firstModel.name);152      }153    }154  }, [providerList, provider, setProvider, modelList, setModel]);155 156  if (providerList.length === 0) {157    return (158      <div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">159        <p className="text-center">160          No providers are currently enabled. Please enable at least one provider in the settings to start using the161          chat.162        </p>163      </div>164    );165  }166 167  return (168    <div className="mb-2 flex gap-2 flex-col sm:flex-row">169      <select170        value={provider?.name ?? ''}171        onChange={(e) => {172          const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);173 174          if (newProvider && setProvider) {175            setProvider(newProvider);176          }177 178          const firstModel = [...modelList].find((m) => m.provider === e.target.value);179 180          if (firstModel && setModel) {181            setModel(firstModel.name);182          }183        }}184        className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"185      >186        {providerList.map((provider: ProviderInfo) => (187          <option key={provider.name} value={provider.name}>188            {provider.name}189          </option>190        ))}191      </select>192 193      <div className="relative flex-1 lg:max-w-[70%]" onKeyDown={handleKeyDown} ref={dropdownRef}>194        <div195          className={classNames(196            'w-full p-2 rounded-lg border border-bolt-elements-borderColor',197            'bg-bolt-elements-prompt-background text-bolt-elements-textPrimary',198            'focus-within:outline-none focus-within:ring-2 focus-within:ring-bolt-elements-focus',199            'transition-all cursor-pointer',200            isModelDropdownOpen ? 'ring-2 ring-bolt-elements-focus' : undefined,201          )}202          onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}203          onKeyDown={(e) => {204            if (e.key === 'Enter' || e.key === ' ') {205              e.preventDefault();206              setIsModelDropdownOpen(!isModelDropdownOpen);207            }208          }}209          role="combobox"210          aria-expanded={isModelDropdownOpen}211          aria-controls="model-listbox"212          aria-haspopup="listbox"213          tabIndex={0}214        >215          <div className="flex items-center justify-between">216            <div className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</div>217            <div218              className={classNames(219                'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75',220                isModelDropdownOpen ? 'rotate-180' : undefined,221              )}222            />223          </div>224        </div>225 226        {isModelDropdownOpen && (227          <div228            className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2  shadow-lg"229            role="listbox"230            id="model-listbox"231          >232            <div className="px-2 pb-2">233              <div className="relative">234                <input235                  ref={searchInputRef}236                  type="text"237                  value={modelSearchQuery}238                  onChange={(e) => setModelSearchQuery(e.target.value)}239                  placeholder="Search models..."240                  className={classNames(241                    'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',242                    'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',243                    'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',244                    'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',245                    'transition-all',246                  )}247                  onClick={(e) => e.stopPropagation()}248                  role="searchbox"249                  aria-label="Search models"250                />251                <div className="absolute left-2.5 top-1/2 -translate-y-1/2">252                  <span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />253                </div>254              </div>255            </div>256 257            <div258              className={classNames(259                'max-h-60 overflow-y-auto',260                'sm:scrollbar-none',261                '[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2',262                '[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor',263                '[&::-webkit-scrollbar-thumb]:hover:bg-bolt-elements-borderColorHover',264                '[&::-webkit-scrollbar-thumb]:rounded-full',265                '[&::-webkit-scrollbar-track]:bg-bolt-elements-background-depth-2',266                '[&::-webkit-scrollbar-track]:rounded-full',267                'sm:[&::-webkit-scrollbar]:w-1.5 sm:[&::-webkit-scrollbar]:h-1.5',268                'sm:hover:[&::-webkit-scrollbar-thumb]:bg-bolt-elements-borderColor/50',269                'sm:hover:[&::-webkit-scrollbar-thumb:hover]:bg-bolt-elements-borderColor',270                'sm:[&::-webkit-scrollbar-track]:bg-transparent',271              )}272            >273              {modelLoading === 'all' || modelLoading === provider?.name ? (274                <div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>275              ) : filteredModels.length === 0 ? (276                <div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No models found</div>277              ) : (278                filteredModels.map((modelOption, index) => (279                  <div280                    ref={(el) => (optionsRef.current[index] = el)}281                    key={index}282                    role="option"283                    aria-selected={model === modelOption.name}284                    className={classNames(285                      'px-3 py-2 text-sm cursor-pointer',286                      'hover:bg-bolt-elements-background-depth-3',287                      'text-bolt-elements-textPrimary',288                      'outline-none',289                      model === modelOption.name || focusedIndex === index290                        ? 'bg-bolt-elements-background-depth-2'291                        : undefined,292                      focusedIndex === index ? 'ring-1 ring-inset ring-bolt-elements-focus' : undefined,293                    )}294                    onClick={(e) => {295                      e.stopPropagation();296                      setModel?.(modelOption.name);297                      setIsModelDropdownOpen(false);298                      setModelSearchQuery('');299                    }}300                    tabIndex={focusedIndex === index ? 0 : -1}301                  >302                    {modelOption.label}303                  </div>304                ))305              )}306            </div>307          </div>308        )}309      </div>310    </div>311  );312};313