/* * Changes: * - Added a searchable select component with filtering capability for model selection * - Implemented keyboard navigation and accessibility for the custom dropdown * - Added search functionality that filters models based on user input * - Added keyboard event handlers to close dropdowns with Escape key * - Styling for both light and dark mode themes */ import { useEffect, useState, useRef, useCallback } from 'react'; import type { KeyboardEvent } from 'react'; import { Button } from '@extension/ui'; import { llmProviderStore, agentModelStore, AgentNameEnum, llmProviderModelNames, ProviderTypeEnum, getDefaultDisplayNameFromProviderId, getDefaultProviderConfig, getDefaultAgentModelParams, type ProviderConfig, } from '@extension/storage'; // Helper function to check if a model is an O-series model function isOpenAIOModel(modelName: string): boolean { if (modelName.startsWith('openai/')) { return modelName.startsWith('openai/o'); } return modelName.startsWith('o'); } interface ModelSettingsProps { isDarkMode?: boolean; // Controls dark/light theme styling } export const ModelSettings = ({ isDarkMode = false }: ModelSettingsProps) => { const [providers, setProviders] = useState>({}); const [modifiedProviders, setModifiedProviders] = useState>(new Set()); const [providersFromStorage, setProvidersFromStorage] = useState>(new Set()); const [selectedModels, setSelectedModels] = useState>({ [AgentNameEnum.Navigator]: '', [AgentNameEnum.Planner]: '', [AgentNameEnum.Validator]: '', }); const [modelParameters, setModelParameters] = useState>({ [AgentNameEnum.Navigator]: { temperature: 0, topP: 0 }, [AgentNameEnum.Planner]: { temperature: 0, topP: 0 }, [AgentNameEnum.Validator]: { temperature: 0, topP: 0 }, }); // State for reasoning effort for O-series models const [reasoningEffort, setReasoningEffort] = useState>({ [AgentNameEnum.Navigator]: undefined, [AgentNameEnum.Planner]: undefined, [AgentNameEnum.Validator]: undefined, }); const [newModelInputs, setNewModelInputs] = useState>({}); const [isProviderSelectorOpen, setIsProviderSelectorOpen] = useState(false); const newlyAddedProviderRef = useRef(null); const [nameErrors, setNameErrors] = useState>({}); // Add state for tracking API key visibility const [visibleApiKeys, setVisibleApiKeys] = useState>({}); // Create a non-async wrapper for use in render functions const [availableModels, setAvailableModels] = useState< Array<{ provider: string; providerName: string; model: string }> >([]); // State for model input handling useEffect(() => { const loadProviders = async () => { try { const allProviders = await llmProviderStore.getAllProviders(); console.log('allProviders', allProviders); // Track which providers are from storage const fromStorage = new Set(Object.keys(allProviders)); setProvidersFromStorage(fromStorage); // Only use providers from storage, don't add default ones setProviders(allProviders); } catch (error) { console.error('Error loading providers:', error); // Set empty providers on error setProviders({}); // No providers from storage on error setProvidersFromStorage(new Set()); } }; loadProviders(); }, []); // Load existing agent models and parameters on mount useEffect(() => { const loadAgentModels = async () => { try { const models: Record = { [AgentNameEnum.Planner]: '', [AgentNameEnum.Navigator]: '', [AgentNameEnum.Validator]: '', }; for (const agent of Object.values(AgentNameEnum)) { const config = await agentModelStore.getAgentModel(agent); if (config) { models[agent] = config.modelName; if (config.parameters?.temperature !== undefined || config.parameters?.topP !== undefined) { setModelParameters(prev => ({ ...prev, [agent]: { temperature: config.parameters?.temperature ?? prev[agent].temperature, topP: config.parameters?.topP ?? prev[agent].topP, }, })); } // Also load reasoningEffort if available if (config.reasoningEffort) { setReasoningEffort(prev => ({ ...prev, [agent]: config.reasoningEffort as 'low' | 'medium' | 'high', })); } } } setSelectedModels(models); } catch (error) { console.error('Error loading agent models:', error); } }; loadAgentModels(); }, []); // Auto-focus the input field when a new provider is added useEffect(() => { // Only focus if we have a newly added provider reference if (newlyAddedProviderRef.current && providers[newlyAddedProviderRef.current]) { const providerId = newlyAddedProviderRef.current; const config = providers[providerId]; // For custom providers, focus on the name input if (config.type === ProviderTypeEnum.CustomOpenAI) { const nameInput = document.getElementById(`${providerId}-name`); if (nameInput) { nameInput.focus(); } } else { // For default providers, focus on the API key input const apiKeyInput = document.getElementById(`${providerId}-api-key`); if (apiKeyInput) { apiKeyInput.focus(); } } // Clear the ref after focusing newlyAddedProviderRef.current = null; } }, [providers]); // Add a click outside handler to close the dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; if (isProviderSelectorOpen && !target.closest('.provider-selector-container')) { setIsProviderSelectorOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isProviderSelectorOpen]); // Create a memoized version of getAvailableModels const getAvailableModelsCallback = useCallback(async () => { const models: Array<{ provider: string; providerName: string; model: string }> = []; try { // Load providers directly from storage const storedProviders = await llmProviderStore.getAllProviders(); // Only use providers that are actually in storage for (const [provider, config] of Object.entries(storedProviders)) { if (config.type === ProviderTypeEnum.AzureOpenAI) { // Handle Azure providers specially - use deployment names as models const deploymentNames = config.azureDeploymentNames || []; models.push( ...deploymentNames.map(deployment => ({ provider, providerName: config.name || provider, model: deployment, })), ); } else { // Standard handling for non-Azure providers const providerModels = config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || []; models.push( ...providerModels.map(model => ({ provider, providerName: config.name || provider, model, })), ); } } } catch (error) { console.error('Error loading providers for model selection:', error); } return models; }, []); // Update available models whenever providers change useEffect(() => { const updateAvailableModels = async () => { const models = await getAvailableModelsCallback(); setAvailableModels(models); }; updateAvailableModels(); }, [getAvailableModelsCallback]); // Only depends on the callback const handleApiKeyChange = (provider: string, apiKey: string, baseUrl?: string) => { setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => ({ ...prev, [provider]: { ...prev[provider], apiKey: apiKey.trim(), baseUrl: baseUrl !== undefined ? baseUrl.trim() : prev[provider]?.baseUrl, }, })); }; // Add a toggle handler for API key visibility const toggleApiKeyVisibility = (provider: string) => { setVisibleApiKeys(prev => ({ ...prev, [provider]: !prev[provider], })); }; const handleNameChange = (provider: string, name: string) => { setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => { const updated = { ...prev, [provider]: { ...prev[provider], name: name.trim(), }, }; return updated; }); }; const handleModelsChange = (provider: string, modelsString: string) => { setNewModelInputs(prev => ({ ...prev, [provider]: modelsString, })); }; const addModel = (provider: string, model: string) => { if (!model.trim()) return; setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => { const providerData = prev[provider] || {}; // Get current models - either from provider config or default models let currentModels = providerData.modelNames; if (currentModels === undefined) { currentModels = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])]; } // Don't add duplicates if (currentModels.includes(model.trim())) return prev; return { ...prev, [provider]: { ...providerData, modelNames: [...currentModels, model.trim()], }, }; }); // Clear the input setNewModelInputs(prev => ({ ...prev, [provider]: '', })); }; const removeModel = (provider: string, modelToRemove: string) => { setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => { const providerData = prev[provider] || {}; // If modelNames doesn't exist in the provider data yet, we need to initialize it // with the default models from llmProviderModelNames first if (!providerData.modelNames) { const defaultModels = llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || []; const filteredModels = defaultModels.filter(model => model !== modelToRemove); return { ...prev, [provider]: { ...providerData, modelNames: filteredModels, }, }; } // If modelNames already exists, just filter out the model to remove return { ...prev, [provider]: { ...providerData, modelNames: providerData.modelNames.filter(model => model !== modelToRemove), }, }; }); }; const handleKeyDown = (e: KeyboardEvent, provider: string) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const value = newModelInputs[provider] || ''; addModel(provider, value); } }; const getButtonProps = (provider: string) => { const isInStorage = providersFromStorage.has(provider); const isModified = modifiedProviders.has(provider); // For deletion, we only care if it's in storage and not modified if (isInStorage && !isModified) { return { theme: isDarkMode ? 'dark' : 'light', variant: 'danger' as const, children: 'Delete', disabled: false, }; } // For saving, we need to check if it has the required inputs let hasInput = false; const providerType = providers[provider]?.type; const config = providers[provider]; if (providerType === ProviderTypeEnum.CustomOpenAI) { hasInput = Boolean(config?.baseUrl?.trim()); // Custom needs Base URL, name checked elsewhere } else if (providerType === ProviderTypeEnum.Ollama) { hasInput = Boolean(config?.baseUrl?.trim()); // Ollama needs Base URL } else if (providerType === ProviderTypeEnum.AzureOpenAI) { // Azure needs API Key, Endpoint, Deployment Names, and API Version hasInput = Boolean(config?.apiKey?.trim()) && Boolean(config?.baseUrl?.trim()) && Boolean(config?.azureDeploymentNames?.length) && Boolean(config?.azureApiVersion?.trim()); } else if (providerType === ProviderTypeEnum.OpenRouter) { // OpenRouter needs API Key and optionally Base URL (has default) hasInput = Boolean(config?.apiKey?.trim()) && Boolean(config?.baseUrl?.trim()); } else { // Other built-in providers just need API Key hasInput = Boolean(config?.apiKey?.trim()); } return { theme: isDarkMode ? 'dark' : 'light', variant: 'primary' as const, children: 'Save', disabled: !hasInput || !isModified, }; }; const handleSave = async (provider: string) => { try { // Check if name contains spaces for custom providers if (providers[provider].type === ProviderTypeEnum.CustomOpenAI && providers[provider].name?.includes(' ')) { setNameErrors(prev => ({ ...prev, [provider]: 'Spaces are not allowed in provider names. Please use underscores or other characters instead.', })); return; } // Check if base URL is required but missing for custom_openai, ollama, azure_openai or openrouter if ( (providers[provider].type === ProviderTypeEnum.CustomOpenAI || providers[provider].type === ProviderTypeEnum.Ollama || providers[provider].type === ProviderTypeEnum.AzureOpenAI || providers[provider].type === ProviderTypeEnum.OpenRouter) && (!providers[provider].baseUrl || !providers[provider].baseUrl.trim()) ) { alert(`Base URL is required for ${getDefaultDisplayNameFromProviderId(provider)}. Please enter it.`); return; } // Ensure modelNames is provided let modelNames = providers[provider].modelNames; if (!modelNames) { // Use default model names if not explicitly set modelNames = [...(llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || [])]; } // Prepare data for saving using the correctly typed config from state // We can directly pass the relevant parts of the state config // Create a copy to avoid modifying state directly if needed, though setProvider likely handles it const configToSave: Partial = { ...providers[provider] }; // Use Partial to allow deleting modelNames // Explicitly set required fields that might be missing in partial state updates (though unlikely now) configToSave.apiKey = providers[provider].apiKey || ''; configToSave.name = providers[provider].name || getDefaultDisplayNameFromProviderId(provider); configToSave.type = providers[provider].type; configToSave.createdAt = providers[provider].createdAt || Date.now(); // baseUrl, azureDeploymentName, azureApiVersion should be correctly set by handlers if (providers[provider].type === ProviderTypeEnum.AzureOpenAI) { // Ensure modelNames is NOT included for Azure configToSave.modelNames = undefined; } else { // Ensure modelNames IS included for non-Azure // Use existing modelNames from state, or default if somehow missing configToSave.modelNames = providers[provider].modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || []; } // Pass the cleaned config to setProvider // Cast to ProviderConfig as we've ensured necessary fields based on type await llmProviderStore.setProvider(provider, configToSave as ProviderConfig); // Clear any name errors on successful save setNameErrors(prev => { const newErrors = { ...prev }; delete newErrors[provider]; return newErrors; }); // Add to providersFromStorage since it's now saved setProvidersFromStorage(prev => new Set(prev).add(provider)); setModifiedProviders(prev => { const next = new Set(prev); next.delete(provider); return next; }); // Refresh available models const models = await getAvailableModelsCallback(); setAvailableModels(models); } catch (error) { console.error('Error saving API key:', error); } }; const handleDelete = async (provider: string) => { try { // Delete the provider from storage regardless of its API key value await llmProviderStore.removeProvider(provider); // Remove from providersFromStorage setProvidersFromStorage(prev => { const next = new Set(prev); next.delete(provider); return next; }); // Remove from providers state setProviders(prev => { const next = { ...prev }; delete next[provider]; return next; }); // Also remove from modifiedProviders if it's there setModifiedProviders(prev => { const next = new Set(prev); next.delete(provider); return next; }); // Refresh available models const models = await getAvailableModelsCallback(); setAvailableModels(models); } catch (error) { console.error('Error deleting provider:', error); } }; const handleCancelProvider = (providerId: string) => { // Remove the provider from the state setProviders(prev => { const next = { ...prev }; delete next[providerId]; return next; }); // Remove from modified providers setModifiedProviders(prev => { const next = new Set(prev); next.delete(providerId); return next; }); }; const handleModelChange = async (agentName: AgentNameEnum, modelValue: string) => { // modelValue will be in format "provider>model" const [provider, model] = modelValue.split('>'); console.log(`[handleModelChange] Setting ${agentName} model: provider=${provider}, model=${model}`); // Set parameters based on provider type const newParameters = getDefaultAgentModelParams(provider, agentName); setModelParameters(prev => ({ ...prev, [agentName]: newParameters, })); setSelectedModels(prev => ({ ...prev, [agentName]: model, })); try { if (model) { const providerConfig = providers[provider]; // For Azure, verify the model is in the deployment names list if (providerConfig && providerConfig.type === ProviderTypeEnum.AzureOpenAI) { console.log(`[handleModelChange] Azure model selected: ${model}`); } // Reset reasoning effort if switching models if (isOpenAIOModel(model)) { // Keep existing reasoning effort if already set for O-series models setReasoningEffort(prev => ({ ...prev, [agentName]: prev[agentName] || 'medium', // Default to medium if not set })); } else { // Clear reasoning effort for non-O-series models setReasoningEffort(prev => ({ ...prev, [agentName]: undefined, })); } await agentModelStore.setAgentModel(agentName, { provider, modelName: model, parameters: newParameters, reasoningEffort: isOpenAIOModel(model) ? reasoningEffort[agentName] || 'medium' : undefined, }); } else { // Reset storage if no model is selected await agentModelStore.resetAgentModel(agentName); } } catch (error) { console.error('Error saving agent model:', error); } }; const handleReasoningEffortChange = async (agentName: AgentNameEnum, value: 'low' | 'medium' | 'high') => { setReasoningEffort(prev => ({ ...prev, [agentName]: value, })); // Only update if we have a selected model if (selectedModels[agentName] && isOpenAIOModel(selectedModels[agentName])) { try { // Find provider const provider = getProviderForModel(selectedModels[agentName]); if (provider) { await agentModelStore.setAgentModel(agentName, { provider, modelName: selectedModels[agentName], parameters: modelParameters[agentName], reasoningEffort: value, }); } } catch (error) { console.error('Error saving reasoning effort:', error); } } }; const handleParameterChange = async (agentName: AgentNameEnum, paramName: 'temperature' | 'topP', value: number) => { const newParameters = { ...modelParameters[agentName], [paramName]: value, }; setModelParameters(prev => ({ ...prev, [agentName]: newParameters, })); // Only update if we have a selected model if (selectedModels[agentName]) { try { // Find provider let provider: string | undefined; for (const [providerKey, providerConfig] of Object.entries(providers)) { if (providerConfig.type === ProviderTypeEnum.AzureOpenAI) { // Check Azure deployment names const deploymentNames = providerConfig.azureDeploymentNames || []; if (deploymentNames.includes(selectedModels[agentName])) { provider = providerKey; break; } } else { // Check standard model names for non-Azure providers const modelNames = providerConfig.modelNames || llmProviderModelNames[providerKey as keyof typeof llmProviderModelNames] || []; if (modelNames.includes(selectedModels[agentName])) { provider = providerKey; break; } } } if (provider) { await agentModelStore.setAgentModel(agentName, { provider, modelName: selectedModels[agentName], parameters: newParameters, }); } } catch (error) { console.error('Error saving agent parameters:', error); } } }; const renderModelSelect = (agentName: AgentNameEnum) => (

{agentName.charAt(0).toUpperCase() + agentName.slice(1)}

{getAgentDescription(agentName)}

{/* Model Selection */}
{/* Temperature Slider */}
handleParameterChange(agentName, 'temperature', Number.parseFloat(e.target.value))} style={{ background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${(modelParameters[agentName].temperature / 2) * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`, }} className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} h-1 appearance-none rounded-full`} />
{modelParameters[agentName].temperature.toFixed(2)} { const value = Number.parseFloat(e.target.value); if (!Number.isNaN(value) && value >= 0 && value <= 2) { handleParameterChange(agentName, 'temperature', value); } }} className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} px-2 py-1 text-sm`} aria-label={`${agentName} temperature number input`} />
{/* Top P Slider */}
handleParameterChange(agentName, 'topP', Number.parseFloat(e.target.value))} style={{ background: `linear-gradient(to right, ${isDarkMode ? '#3b82f6' : '#60a5fa'} 0%, ${isDarkMode ? '#3b82f6' : '#60a5fa'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} ${modelParameters[agentName].topP * 100}%, ${isDarkMode ? '#475569' : '#cbd5e1'} 100%)`, }} className={`flex-1 ${isDarkMode ? 'accent-blue-500' : 'accent-blue-400'} h-1 appearance-none rounded-full`} />
{modelParameters[agentName].topP.toFixed(3)} { const value = Number.parseFloat(e.target.value); if (!Number.isNaN(value) && value >= 0 && value <= 1) { handleParameterChange(agentName, 'topP', value); } }} className={`w-20 rounded-md border ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} px-2 py-1 text-sm`} aria-label={`${agentName} top P number input`} />
{/* Reasoning Effort Selector (only for O-series models) */} {selectedModels[agentName] && isOpenAIOModel(selectedModels[agentName]) && (
)}
); const getAgentDescription = (agentName: AgentNameEnum) => { switch (agentName) { case AgentNameEnum.Navigator: return 'Navigates websites and performs actions'; case AgentNameEnum.Planner: return 'Develops and refines strategies to complete tasks'; case AgentNameEnum.Validator: return 'Checks if tasks are completed successfully'; default: return ''; } }; const getMaxCustomProviderNumber = () => { let maxNumber = 0; for (const providerId of Object.keys(providers)) { if (providerId.startsWith('custom_openai_')) { const match = providerId.match(/custom_openai_(\d+)/); if (match) { const number = Number.parseInt(match[1], 10); maxNumber = Math.max(maxNumber, number); } } } return maxNumber; }; const addCustomProvider = () => { const nextNumber = getMaxCustomProviderNumber() + 1; const providerId = `custom_openai_${nextNumber}`; setProviders(prev => ({ ...prev, [providerId]: { apiKey: '', name: `CustomProvider${nextNumber}`, type: ProviderTypeEnum.CustomOpenAI, baseUrl: '', modelNames: [], createdAt: Date.now(), }, })); setModifiedProviders(prev => new Set(prev).add(providerId)); // Set the newly added provider ref newlyAddedProviderRef.current = providerId; // Scroll to the newly added provider after render setTimeout(() => { const providerElement = document.getElementById(`provider-${providerId}`); if (providerElement) { providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 100); }; const addBuiltInProvider = (provider: string) => { // Get the default provider configuration const config = getDefaultProviderConfig(provider); // Add the provider to the state setProviders(prev => ({ ...prev, [provider]: config, })); // Mark as modified so it shows up in the UI setModifiedProviders(prev => new Set(prev).add(provider)); // Set the newly added provider ref newlyAddedProviderRef.current = provider; // Scroll to the newly added provider after render setTimeout(() => { const providerElement = document.getElementById(`provider-${provider}`); if (providerElement) { providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 100); }; // Sort providers to ensure newly added providers appear at the bottom const getSortedProviders = () => { // Filter providers to only include those from storage and newly added providers const filteredProviders = Object.entries(providers).filter(([providerId, config]) => { // ALSO filter out any provider missing a config or type, to satisfy TS if (!config || !config.type) { console.warn(`Filtering out provider ${providerId} with missing config or type.`); return false; } // Include if it's from storage if (providersFromStorage.has(providerId)) { return true; } // Include if it's a newly added provider (has been modified) if (modifiedProviders.has(providerId)) { return true; } // Exclude providers that aren't from storage and haven't been modified return false; }); // Sort the filtered providers return filteredProviders.sort(([keyA, configA], [keyB, configB]) => { // Separate newly added providers from stored providers const isNewA = !providersFromStorage.has(keyA) && modifiedProviders.has(keyA); const isNewB = !providersFromStorage.has(keyB) && modifiedProviders.has(keyB); // If one is new and one is stored, new ones go to the end if (isNewA && !isNewB) return 1; if (!isNewA && isNewB) return -1; // If both are new or both are stored, sort by createdAt if (configA.createdAt && configB.createdAt) { return configA.createdAt - configB.createdAt; // Sort in ascending order (oldest first) } // If only one has createdAt, put the one without createdAt at the end if (configA.createdAt) return -1; if (configB.createdAt) return 1; // If neither has createdAt, sort by type and then name const isCustomA = configA.type === ProviderTypeEnum.CustomOpenAI; const isCustomB = configB.type === ProviderTypeEnum.CustomOpenAI; if (isCustomA && !isCustomB) { return 1; // Custom providers come after non-custom } if (!isCustomA && isCustomB) { return -1; // Non-custom providers come before custom } // Sort alphabetically by name within each group return (configA.name || keyA).localeCompare(configB.name || keyB); }); }; const handleProviderSelection = (providerType: string) => { // Close the dropdown immediately setIsProviderSelectorOpen(false); // Handle custom provider if (providerType === ProviderTypeEnum.CustomOpenAI) { addCustomProvider(); return; } // Handle Azure OpenAI specially to allow multiple instances if (providerType === ProviderTypeEnum.AzureOpenAI) { addAzureProvider(); return; } // Handle built-in supported providers addBuiltInProvider(providerType); }; // New function to add Azure providers with unique IDs const addAzureProvider = () => { // Count existing Azure providers const azureProviders = Object.keys(providers).filter( key => key === ProviderTypeEnum.AzureOpenAI || key.startsWith(`${ProviderTypeEnum.AzureOpenAI}_`), ); const nextNumber = azureProviders.length + 1; // Create unique ID const providerId = nextNumber === 1 ? ProviderTypeEnum.AzureOpenAI : `${ProviderTypeEnum.AzureOpenAI}_${nextNumber}`; // Create config with appropriate name const config = getDefaultProviderConfig(ProviderTypeEnum.AzureOpenAI); config.name = `Azure OpenAI ${nextNumber}`; // Add to providers setProviders(prev => ({ ...prev, [providerId]: config, })); setModifiedProviders(prev => new Set(prev).add(providerId)); newlyAddedProviderRef.current = providerId; // Scroll to the newly added provider after render setTimeout(() => { const providerElement = document.getElementById(`provider-${providerId}`); if (providerElement) { providerElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 100); }; const getProviderForModel = (modelName: string): string => { for (const [provider, config] of Object.entries(providers)) { // Check Azure deployment names if (config.type === ProviderTypeEnum.AzureOpenAI) { const deploymentNames = config.azureDeploymentNames || []; if (deploymentNames.includes(modelName)) { return provider; } } else { // Check regular model names for non-Azure providers const modelNames = config.modelNames || llmProviderModelNames[provider as keyof typeof llmProviderModelNames] || []; if (modelNames.includes(modelName)) { return provider; } } } return ''; }; // Add and remove Azure deployments const addAzureDeployment = (provider: string, deploymentName: string) => { if (!deploymentName.trim()) return; setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => { const providerData = prev[provider] || {}; // Initialize or use existing deploymentNames array const deploymentNames = providerData.azureDeploymentNames || []; // Don't add duplicates if (deploymentNames.includes(deploymentName.trim())) return prev; return { ...prev, [provider]: { ...providerData, azureDeploymentNames: [...deploymentNames, deploymentName.trim()], }, }; }); // Clear the input setNewModelInputs(prev => ({ ...prev, [provider]: '', })); }; const removeAzureDeployment = (provider: string, deploymentToRemove: string) => { setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => { const providerData = prev[provider] || {}; // Get current deployments const deploymentNames = providerData.azureDeploymentNames || []; // Filter out the deployment to remove const filteredDeployments = deploymentNames.filter(name => name !== deploymentToRemove); return { ...prev, [provider]: { ...providerData, azureDeploymentNames: filteredDeployments, }, }; }); }; const handleAzureApiVersionChange = (provider: string, apiVersion: string) => { setModifiedProviders(prev => new Set(prev).add(provider)); setProviders(prev => ({ ...prev, [provider]: { ...prev[provider], azureApiVersion: apiVersion.trim(), }, })); }; return (
{/* LLM Providers Section */}

LLM Providers

{getSortedProviders().length === 0 ? (

No providers configured yet. Add a provider to get started.

) : ( getSortedProviders().map(([providerId, providerConfig]) => { // Add type guard to satisfy TypeScript if (!providerConfig || !providerConfig.type) { console.warn(`Skipping rendering for providerId ${providerId} due to missing config or type`); return null; // Skip rendering this item if config/type is somehow missing } return (

{providerConfig.name || providerId}

{/* Show Cancel button for newly added providers */} {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && ( )}
{/* Show message for newly added providers */} {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && (

This provider is newly added. Enter your API key and click Save to configure it.

)}
{/* Name input (only for custom_openai) - moved to top for prominence */} {providerConfig.type === ProviderTypeEnum.CustomOpenAI && (
{ console.log('Name input changed:', e.target.value); handleNameChange(providerId, e.target.value); }} className={`flex-1 rounded-md border p-2 text-sm ${ nameErrors[providerId] ? isDarkMode ? 'border-red-700 bg-slate-700 text-gray-200 focus:border-red-600 focus:ring-2 focus:ring-red-900' : 'border-red-300 bg-gray-50 focus:border-red-400 focus:ring-2 focus:ring-red-200' : isDarkMode ? 'border-blue-700 bg-slate-700 text-gray-200 focus:border-blue-600 focus:ring-2 focus:ring-blue-900' : 'border-blue-300 bg-gray-50 focus:border-blue-400 focus:ring-2 focus:ring-blue-200' } outline-none`} />
{nameErrors[providerId] ? (

{nameErrors[providerId]}

) : (

Provider name (spaces are not allowed when saving)

)}
)} {/* API Key input with label */}
handleApiKeyChange(providerId, e.target.value, providerConfig.baseUrl)} className={`w-full rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`} /> {/* Show eye button only for newly added providers */} {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && ( )}
{/* Display API key for newly added providers only when visible */} {modifiedProviders.has(providerId) && !providersFromStorage.has(providerId) && visibleApiKeys[providerId] && providerConfig.apiKey && (

{providerConfig.apiKey}

)} {/* Base URL input (for custom_openai, ollama, azure_openai, and openrouter) */} {(providerConfig.type === ProviderTypeEnum.CustomOpenAI || providerConfig.type === ProviderTypeEnum.Ollama || providerConfig.type === ProviderTypeEnum.AzureOpenAI || providerConfig.type === ProviderTypeEnum.OpenRouter) && (
handleApiKeyChange(providerId, providerConfig.apiKey || '', e.target.value)} className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`} />
)} {/* Azure Deployment Name input as tags/chips like OpenRouter models */} {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.AzureOpenAI && (
{/* Show azure deployments */} {(providerConfig.azureDeploymentNames || []).length > 0 ? (providerConfig.azureDeploymentNames || []).map((deploymentName: string) => (
{deploymentName}
)) : null} handleModelsChange(providerId, e.target.value)} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const value = newModelInputs[providerId] || ''; if (value.trim()) { addAzureDeployment(providerId, value.trim()); // Clear the input setNewModelInputs(prev => ({ ...prev, [providerId]: '', })); } } }} className={`min-w-[150px] flex-1 border-none text-sm ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`} />

Type model name and press Enter or Space to set. Deployment name should match OpenAI model name (e.g., gpt-4o) for best compatibility.

)} {/* NEW: Azure API Version input */} {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.AzureOpenAI && (
handleAzureApiVersionChange(providerId, e.target.value)} className={`flex-1 rounded-md border text-sm ${isDarkMode ? 'border-slate-600 bg-slate-700 text-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-800' : 'border-gray-300 bg-white text-gray-700 focus:border-blue-400 focus:ring-2 focus:ring-blue-200'} p-2 outline-none`} />
)} {/* Models input section (for non-Azure providers) */} {(providerConfig.type as ProviderTypeEnum) !== ProviderTypeEnum.AzureOpenAI && (
{/* Conditional UI for OpenRouter */} {(providerConfig.type as ProviderTypeEnum) === ProviderTypeEnum.OpenRouter ? ( <>
{providerConfig.modelNames && providerConfig.modelNames.length > 0 ? ( providerConfig.modelNames.map(model => (
{model}
)) ) : ( No models selected. Add model names manually if needed. )} handleModelsChange(providerId, e.target.value)} onKeyDown={e => handleKeyDown(e, providerId)} className={`min-w-[150px] flex-1 border-none text-sm ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`} />

Type and Press Enter or Space to add.

) : ( /* Default Tag Input for other providers */ <>
{(() => { const models = providerConfig.modelNames !== undefined ? providerConfig.modelNames : llmProviderModelNames[providerId as keyof typeof llmProviderModelNames] || []; return models.map(model => (
{model}
)); })()} handleModelsChange(providerId, e.target.value)} onKeyDown={e => handleKeyDown(e, providerId)} className={`min-w-[150px] flex-1 border-none text-sm ${isDarkMode ? 'bg-transparent text-gray-200' : 'bg-transparent text-gray-700'} p-1 outline-none`} />

Type and Press Enter or Space to add.

)} {/* === END: Conditional UI === */}
)} {/* Ollama reminder at the bottom of the section */} {providerConfig.type === ProviderTypeEnum.Ollama && (

Remember: Add{' '} OLLAMA_ORIGINS=chrome-extension://* {' '} environment variable for the Ollama server. Learn more

)}
{/* Add divider except for the last item */} {Object.keys(providers).indexOf(providerId) < Object.keys(providers).length - 1 && (
)}
); }) )} {/* Add Provider button and dropdown */}
{isProviderSelectorOpen && (
{/* Map through provider types to create buttons */} {Object.values(ProviderTypeEnum) // Allow Azure to appear multiple times, but filter out other already added providers .filter( type => type === ProviderTypeEnum.AzureOpenAI || // Always show Azure (type !== ProviderTypeEnum.CustomOpenAI && !providersFromStorage.has(type) && !modifiedProviders.has(type)), ) .map(type => ( ))} {/* Custom provider button (always shown) */}
)}
{/* Updated Agent Models Section */}

Model Selection

{[AgentNameEnum.Planner, AgentNameEnum.Navigator, AgentNameEnum.Validator].map(agentName => (
{renderModelSelect(agentName)}
))}
); };