import React, { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { FiCheckCircle, FiAlertCircle, FiHelpCircle, FiExternalLink, } from 'react-icons/fi'; import { z } from 'zod'; import { useConfigStore } from '../../stores/configStore'; import { Input } from '../../components/ui'; import { Button } from '../../components/ui'; import Typography from '../../components/ui/Typography'; import { ModelSelectorWithOpenRouter } from '../../components/ui/ModelSelectorWithOpenRouter'; import { ApiKeyGuide } from '../../components/ui/ApiKeyGuide'; import { getModelInfo } from '../../lib/models'; type LLMSettingsProps = Record; const llmSettingsSchema = z.object({ provider: z.enum(['openai', 'anthropic']), openaiApiKey: z.string().optional(), openaiModel: z.string().optional(), anthropicApiKey: z.string().optional(), anthropicModel: z.string().optional(), }); type LLMSettingsForm = z.infer; export const LLMSettings: React.FC = () => { const { config, setLLMProvider, setOpenAIApiKey, setOpenAIModel, setAnthropicApiKey, setAnthropicModel, testOpenAIConnection, testAnthropicConnection, isLLMConfigValid, } = useConfigStore(); const [testResult, setTestResult] = useState<{ success: boolean; message: string; } | null>(null); const [isTesting, setIsTesting] = useState(false); const [showApiKeyGuide, setShowApiKeyGuide] = useState(false); const [isFirstTime, setIsFirstTime] = useState(false); const { register, handleSubmit: _handleSubmit, formState: { errors }, watch, setValue, reset, } = useForm({ resolver: zodResolver(llmSettingsSchema), defaultValues: { provider: config?.llmProvider || 'openai', openaiApiKey: config?.openai?.apiKey || '', openaiModel: config?.openai?.model || 'gpt-4o', anthropicApiKey: config?.anthropic?.apiKey || '', anthropicModel: config?.anthropic?.model || 'claude-3-7-sonnet-latest', }, }); React.useEffect(() => { if (config) { reset({ provider: config.llmProvider || 'openai', openaiApiKey: config.openai?.apiKey || '', openaiModel: config.openai?.model || 'gpt-4o', anthropicApiKey: config.anthropic?.apiKey || '', anthropicModel: config.anthropic?.model || 'claude-3-7-sonnet-latest', }); } }, [config, reset]); const watchProvider = watch('provider'); const watchOpenAIApiKey = watch('openaiApiKey'); const watchOpenAIModel = watch('openaiModel'); const watchAnthropicApiKey = watch('anthropicApiKey'); const watchAnthropicModel = watch('anthropicModel'); useEffect(() => { const hasAnyApiKey = config?.openai?.apiKey || config?.anthropic?.apiKey; setIsFirstTime(!hasAnyApiKey); }, [config]); useEffect(() => { if (config?.llmProvider !== watchProvider) { setLLMProvider(watchProvider); } }, [watchProvider, setLLMProvider, config?.llmProvider]); useEffect(() => { if (watchOpenAIApiKey !== undefined && config?.openai?.apiKey !== watchOpenAIApiKey) { setOpenAIApiKey(watchOpenAIApiKey); } }, [watchOpenAIApiKey, setOpenAIApiKey, config?.openai?.apiKey]); useEffect(() => { if (watchOpenAIModel && config?.openai?.model !== watchOpenAIModel) { setOpenAIModel(watchOpenAIModel as string); } }, [watchOpenAIModel, setOpenAIModel, config?.openai?.model]); useEffect(() => { if (watchAnthropicApiKey !== undefined && config?.anthropic?.apiKey !== watchAnthropicApiKey) { setAnthropicApiKey(watchAnthropicApiKey); } }, [watchAnthropicApiKey, setAnthropicApiKey, config?.anthropic?.apiKey]); useEffect(() => { if (watchAnthropicModel && config?.anthropic?.model !== watchAnthropicModel) { setAnthropicModel(watchAnthropicModel as string); } }, [watchAnthropicModel, setAnthropicModel, config?.anthropic?.model]); const handleTestConnection = async () => { setIsTesting(true); setTestResult(null); try { const result = watchProvider === 'openai' ? await testOpenAIConnection() : await testAnthropicConnection(); setTestResult({ success: result.success, message: result.success ? 'Connection successful! 🎉' : result.error || 'Connection failed', }); } catch (error) { setTestResult({ success: false, message: error instanceof Error ? error.message : 'Test failed', }); } finally { setIsTesting(false); } }; const currentApiKey = watchProvider === 'openai' ? watchOpenAIApiKey : watchAnthropicApiKey; const currentModel = watchProvider === 'openai' ? watchOpenAIModel : watchAnthropicModel; const selectedModelInfo = getModelInfo(currentModel || ''); if (showApiKeyGuide) { return ( setShowApiKeyGuide(false)} /> ); } return (
AI Language Model Configuration
Choose your AI provider and configure your API access. We support both OpenAI and Anthropic models.
{isFirstTime && (
👋
Welcome! Let's get you set up To use this conversational agent, you'll need an API key from either OpenAI or Anthropic. Don't worry - we'll guide you through the entire process!
)}
Choose Your AI Provider
setValue('provider', 'openai')} className={`cursor-pointer border rounded-lg p-4 transition-colors ${ watchProvider === 'openai' ? 'border-brand-blue/60 bg-brand-blue/5 dark:bg-brand-blue/10' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' }`} >
AI
OpenAI GPT-4.1, GPT-4o, and more
Popular models including the latest GPT-4.1 with 1M context window, efficient GPT-4o mini, and specialized reasoning models.
setValue('provider', 'anthropic')} className={`cursor-pointer border rounded-lg p-4 transition-colors ${ watchProvider === 'anthropic' ? 'border-brand-blue/60 bg-brand-blue/5 dark:bg-brand-blue/10' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' }`} >
C
Anthropic Claude 3.5 Sonnet, Haiku, and Opus
Advanced models known for safety, helpful reasoning, and excellent performance on coding and creative tasks.
{watchProvider === 'openai' ? 'OpenAI' : 'Anthropic'} API Configuration
API Key
{errors[`${watchProvider}ApiKey` as keyof LLMSettingsForm] && ( { errors[`${watchProvider}ApiKey` as keyof LLMSettingsForm] ?.message } )}
Your API key is encrypted and stored securely using the system keychain. {watchProvider === 'openai' ? ' OpenAI keys start with "sk-".' : ' Anthropic keys start with "sk-ant-".'} Get API Key
{currentApiKey && ( setValue( `${watchProvider}Model` as keyof LLMSettingsForm, model ) } useOpenRouter={true} /> )} {currentApiKey && (
)} {testResult && (
{testResult.success ? ( ) : ( )}
{testResult.message} {testResult.success && selectedModelInfo && (
Connected to {selectedModelInfo.name} •{' '} {selectedModelInfo.contextWindow} context
)}
)}
{isLLMConfigValid() && (
Configuration Complete! ✨ You're all set to use the conversational agent with{' '} {selectedModelInfo?.name || 'your selected model'}. {selectedModelInfo && (
• Model: {selectedModelInfo.name} • Context: {selectedModelInfo.contextWindow} • Input Cost: {selectedModelInfo.inputCost}
)}
)}
); }; export const OpenAISettings = LLMSettings;