import React, { useEffect, useState, useCallback } from 'react'; import { ArrowLeftRight, ExternalLink } from 'lucide-react'; import Modal from '../../../Modal'; import { Button } from '../../../ui/button'; import { QUICKSTART_GUIDE_URL } from '../../providers/modal/constants'; import { Input } from '../../../ui/input'; import { Select } from '../../../ui/Select'; import { useConfig } from '../../../ConfigContext'; import { changeModel } from '../index'; import type { View } from '../../../../App'; import Model, { getProviderMetadata } from '../modelInterface'; const ModalButtons = ({ onSubmit, onCancel, _isValid, _validationErrors }) => (
); type AddModelModalProps = { onClose: () => void; setView: (view: View) => void; }; export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => { const { getProviders, upsert, getExtensions, addExtension } = useConfig(); const [providerOptions, setProviderOptions] = useState([]); const [modelOptions, setModelOptions] = useState([]); const [provider, setProvider] = useState(null); const [model, setModel] = useState(''); const [isCustomModel, setIsCustomModel] = useState(false); const [validationErrors, setValidationErrors] = useState({ provider: '', model: '', }); const [isValid, setIsValid] = useState(true); const [attemptedSubmit, setAttemptedSubmit] = useState(false); // Validate form data const validateForm = useCallback(() => { const errors = { provider: '', model: '', }; let formIsValid = true; if (!provider) { errors.provider = 'Please select a provider'; formIsValid = false; } if (!model) { errors.model = 'Please select or enter a model'; formIsValid = false; } setValidationErrors(errors); setIsValid(formIsValid); return formIsValid; }, [model, provider]); const onSubmit = async () => { setAttemptedSubmit(true); const isFormValid = validateForm(); if (isFormValid) { const providerMetaData = await getProviderMetadata(provider, getProviders); const providerDisplayName = providerMetaData.display_name; await changeModel({ model: { name: model, provider: provider, subtext: providerDisplayName } as Model, // pass in a Model object writeToConfig: upsert, getExtensions, addExtension, }); onClose(); } }; // Re-validate when inputs change and after attempted submission useEffect(() => { if (attemptedSubmit) { validateForm(); } }, [attemptedSubmit, validateForm]); useEffect(() => { (async () => { try { const providersResponse = await getProviders(false); const activeProviders = providersResponse.filter((provider) => provider.is_configured); // Create provider options and add "Use other provider" option setProviderOptions([ ...activeProviders.map(({ metadata, name }) => ({ value: name, label: metadata.display_name, })), { value: 'configure_providers', label: 'Use other provider', }, ]); // Format model options by provider const formattedModelOptions = []; activeProviders.forEach(({ metadata, name }) => { if (metadata.known_models && metadata.known_models.length > 0) { formattedModelOptions.push({ options: metadata.known_models.map((modelName) => ({ value: modelName, label: modelName, provider: name, })), }); } }); // Add the "Custom model" option to each provider group formattedModelOptions.forEach((group) => { group.options.push({ value: 'custom', label: 'Use custom model', provider: group.options[0]?.provider, }); }); setModelOptions(formattedModelOptions); setOriginalModelOptions(formattedModelOptions); } catch (error) { console.error('Failed to load providers:', error); } })(); }, [getProviders]); // Filter model options based on selected provider const filteredModelOptions = provider ? modelOptions.filter((group) => group.options[0]?.provider === provider) : []; // Handle model selection change const handleModelChange = (selectedOption) => { if (selectedOption?.value === 'custom') { setIsCustomModel(true); setModel(''); } else { setIsCustomModel(false); setModel(selectedOption?.value || ''); } }; // Store the original model options in state, initialized from modelOptions const [originalModelOptions, setOriginalModelOptions] = useState(modelOptions); const handleInputChange = (inputValue: string) => { if (!provider) return; const trimmedInput = inputValue.trim(); if (trimmedInput === '') { // Reset to original model options when input is cleared setModelOptions([...originalModelOptions]); // Create new array to ensure state update return; } // Filter through the original model options to find matches const matchingOptions = originalModelOptions .map((group) => ({ options: group.options.filter( (option) => option.value.toLowerCase().includes(trimmedInput.toLowerCase()) && option.value !== 'custom' // Exclude the "Use custom model" option from search ), })) .filter((group) => group.options.length > 0); if (matchingOptions.length > 0) { // If we found matches in the existing options, show those setModelOptions(matchingOptions); } else { // If no matches, show the "Use: " option const customOption = [ { options: [ { value: trimmedInput, label: `Use: "${trimmedInput}"`, provider: provider, }, ], }, ]; setModelOptions(customOption); } }; return (
} >
Switch models
Configure your AI model providers by adding their API keys. Your keys are stored securely and encrypted locally.
{attemptedSubmit && validationErrors.model && (
{validationErrors.model}
)}
) : (
setModel(event.target.value)} value={model} /> {attemptedSubmit && validationErrors.model && (
{validationErrors.model}
)}
)} )}
); };