import React, { useState, useEffect } from 'react'; import { Check, X, Loader2, Eye, EyeOff, AlertCircle } from 'lucide-react'; import { createStyles } from 'antd-style'; import { lobeChatColors, lobeChatSpacing } from '../styles/lobeChatTheme'; import { AI_PROVIDERS } from '../config/aiProviders'; const useStyles = createStyles(({ css }) => ({ container: css` width: 100%; height: 100%; `, title: css` font-size: 20px; font-weight: 600; color: white; margin-bottom: ${lobeChatSpacing.md}px; `, description: css` font-size: 14px; color: ${lobeChatColors.icon.default}; margin-bottom: ${lobeChatSpacing.xl}px; `, providersList: css` display: flex; flex-direction: column; gap: ${lobeChatSpacing.lg}px; `, providerCard: css` background: ${lobeChatColors.sidebar.background}; border: 1px solid ${lobeChatColors.sidebar.border}; border-radius: 12px; padding: ${lobeChatSpacing.lg}px; transition: all 0.2s; &:hover { border-color: ${lobeChatColors.input.focus}; } `, providerHeader: css` display: flex; align-items: center; justify-content: space-between; margin-bottom: ${lobeChatSpacing.md}px; `, providerInfo: css` display: flex; align-items: center; gap: ${lobeChatSpacing.md}px; `, providerIcon: css` font-size: 32px; `, providerName: css` font-size: 16px; font-weight: 600; color: white; `, providerStatus: css` display: flex; align-items: center; gap: ${lobeChatSpacing.xs}px; padding: 4px 12px; border-radius: 16px; font-size: 12px; font-weight: 500; `, providerStatusEnabled: css` background: rgba(34, 197, 94, 0.1); color: #22c55e; `, providerStatusDisabled: css` background: rgba(156, 163, 175, 0.1); color: #9ca3af; `, formGroup: css` margin-bottom: ${lobeChatSpacing.md}px; `, label: css` display: flex; align-items: center; gap: ${lobeChatSpacing.xs}px; font-size: 13px; font-weight: 500; color: ${lobeChatColors.icon.default}; margin-bottom: ${lobeChatSpacing.xs}px; `, required: css` color: #ef4444; `, inputWrapper: css` position: relative; display: flex; gap: ${lobeChatSpacing.xs}px; `, input: css` flex: 1; background: ${lobeChatColors.input.background}; border: 1px solid ${lobeChatColors.sidebar.border}; border-radius: 8px; padding: ${lobeChatSpacing.sm}px ${lobeChatSpacing.md}px; padding-right: 40px; color: white; font-size: 14px; outline: none; transition: all 0.2s; font-family: 'Monaco', 'Courier New', monospace; &:focus { border-color: ${lobeChatColors.input.focus}; background: rgba(102, 126, 234, 0.05); } &::placeholder { color: ${lobeChatColors.icon.default}; opacity: 0.5; } &[type="password"] { letter-spacing: 2px; } `, toggleButton: css` position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: transparent; border: none; border-radius: 4px; color: ${lobeChatColors.icon.default}; cursor: pointer; transition: all 0.2s; &:hover { background: ${lobeChatColors.sidebar.hover}; color: white; } `, testButton: css` display: flex; align-items: center; gap: ${lobeChatSpacing.xs}px; padding: ${lobeChatSpacing.sm}px ${lobeChatSpacing.md}px; background: ${lobeChatColors.sidebar.background}; border: 1px solid ${lobeChatColors.sidebar.border}; border-radius: 8px; color: white; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; white-space: nowrap; &:hover:not(:disabled) { border-color: ${lobeChatColors.input.focus}; background: ${lobeChatColors.sidebar.hover}; } &:disabled { opacity: 0.5; cursor: not-allowed; } `, testButtonTesting: css` border-color: #3b82f6; `, testButtonSuccess: css` border-color: #22c55e; color: #22c55e; `, testButtonError: css` border-color: #ef4444; color: #ef4444; `, checkboxWrapper: css` display: flex; align-items: center; gap: ${lobeChatSpacing.sm}px; margin-top: ${lobeChatSpacing.xs}px; `, checkbox: css` width: 18px; height: 18px; cursor: pointer; `, checkboxLabel: css` font-size: 13px; color: ${lobeChatColors.icon.default}; cursor: pointer; `, testResult: css` display: flex; align-items: flex-start; gap: ${lobeChatSpacing.sm}px; padding: ${lobeChatSpacing.sm}px ${lobeChatSpacing.md}px; border-radius: 8px; font-size: 13px; margin-top: ${lobeChatSpacing.sm}px; `, testResultSuccess: css` background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); color: #22c55e; `, testResultError: css` background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); color: #ef4444; `, modelsCount: css` font-size: 12px; color: ${lobeChatColors.icon.default}; margin-top: ${lobeChatSpacing.xs}px; `, saveButton: css` margin-top: ${lobeChatSpacing.lg}px; padding: ${lobeChatSpacing.md}px ${lobeChatSpacing.xl}px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 8px; color: white; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3); &:hover { box-shadow: 0 6px 24px rgba(102, 126, 234, 0.4); transform: translateY(-1px); } `, })); // URLs API por defecto const DEFAULT_API_URLS: Record = { openai: 'https://api.openai.com/v1', anthropic: 'https://api.anthropic.com/v1', google: 'https://generativelanguage.googleapis.com/v1', mistral: 'https://api.mistral.ai/v1', cohere: 'https://api.cohere.ai/v1', }; interface ProviderConfig { apiKey: string; apiUrl: string; useCustomUrl: boolean; enabled: boolean; } export const AIProviderSettings: React.FC = () => { const { styles, cx } = useStyles(); const [configs, setConfigs] = useState>({}); const [showKeys, setShowKeys] = useState>({}); const [testingStates, setTestingStates] = useState>({}); const [testMessages, setTestMessages] = useState>({}); // Cargar configuraciones guardadas useEffect(() => { const savedConfigs = localStorage.getItem('aiProviderConfigs'); if (savedConfigs) { setConfigs(JSON.parse(savedConfigs)); } else { // Inicializar con valores por defecto const initialConfigs: Record = {}; AI_PROVIDERS.forEach(provider => { initialConfigs[provider.id] = { apiKey: '', apiUrl: DEFAULT_API_URLS[provider.id] || '', useCustomUrl: false, enabled: false, }; }); setConfigs(initialConfigs); } }, []); const handleApiKeyChange = (providerId: string, apiKey: string) => { setConfigs(prev => ({ ...prev, [providerId]: { ...prev[providerId], apiKey }, })); }; const handleApiUrlChange = (providerId: string, apiUrl: string) => { setConfigs(prev => ({ ...prev, [providerId]: { ...prev[providerId], apiUrl }, })); }; const handleUseCustomUrlChange = (providerId: string, useCustomUrl: boolean) => { setConfigs(prev => ({ ...prev, [providerId]: { ...prev[providerId], useCustomUrl, apiUrl: useCustomUrl ? prev[providerId].apiUrl : DEFAULT_API_URLS[providerId], }, })); }; const toggleShowKey = (providerId: string) => { setShowKeys(prev => ({ ...prev, [providerId]: !prev[providerId] })); }; const testConnection = async (providerId: string) => { const config = configs[providerId]; if (!config?.apiKey) { setTestMessages(prev => ({ ...prev, [providerId]: 'API Key es requerido' })); setTestingStates(prev => ({ ...prev, [providerId]: 'error' })); return; } // Sanitizar API Key (eliminar espacios en blanco) const cleanApiKey = config.apiKey.trim(); if (!cleanApiKey) { setTestMessages(prev => ({ ...prev, [providerId]: 'API Key inválida' })); setTestingStates(prev => ({ ...prev, [providerId]: 'error' })); return; } setTestingStates(prev => ({ ...prev, [providerId]: 'testing' })); setTestMessages(prev => ({ ...prev, [providerId]: '' })); try { const apiUrl = config.useCustomUrl ? config.apiUrl : DEFAULT_API_URLS[providerId]; let testUrl = ''; let headers: Record = { 'Content-Type': 'application/json', }; // Configurar según el provider switch (providerId) { case 'openai': testUrl = `${apiUrl}/models`; headers['Authorization'] = `Bearer ${cleanApiKey}`; break; case 'anthropic': testUrl = `${apiUrl}/messages`; headers['x-api-key'] = cleanApiKey; headers['anthropic-version'] = '2023-06-01'; break; case 'google': // Para Google, el API key va en el query parameter testUrl = `${apiUrl}/models?key=${encodeURIComponent(cleanApiKey)}`; break; case 'mistral': testUrl = `${apiUrl}/models`; headers['Authorization'] = `Bearer ${cleanApiKey}`; break; case 'cohere': testUrl = `${apiUrl}/models`; headers['Authorization'] = `Bearer ${cleanApiKey}`; break; default: throw new Error(`Provider ${providerId} no soportado`); } // Instead of calling provider directly from browser (CORS issues), call our backend API const backendUrl = '/api/test-provider'; const response = await fetch(backendUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ providerId, apiKey: cleanApiKey, apiUrl: apiUrl }), }); if (response.ok) { const data = await response.json(); if (data.success) { const modelsCount = data.modelsCount || 0; setTestingStates(prev => ({ ...prev, [providerId]: 'success' })); setTestMessages(prev => ({ ...prev, [providerId]: modelsCount > 0 ? `✓ Connection successful. ${modelsCount} models available.` : `✓ Connection successful. API Key valid.` })); // Enable provider automatically setConfigs(prev => ({ ...prev, [providerId]: { ...prev[providerId], enabled: true }, })); } else { setTestingStates(prev => ({ ...prev, [providerId]: 'error' })); setTestMessages(prev => ({ ...prev, [providerId]: `✗ ${data.error || 'Unknown error'}` })); } } else { let msg = `HTTP ${response.status}`; try { const err = await response.json(); msg = err.error || err.message || msg; } catch (e) {} setTestingStates(prev => ({ ...prev, [providerId]: 'error' })); setTestMessages(prev => ({ ...prev, [providerId]: `✗ Error: ${msg}` })); } } catch (error: any) { let errorMessage = 'Error de red'; if (error.message?.includes('Failed to fetch')) { errorMessage = 'Error de CORS o red. Verifica tu API Key y URL.'; } else if (error.message) { errorMessage = error.message; } setTestingStates(prev => ({ ...prev, [providerId]: 'error' })); setTestMessages(prev => ({ ...prev, [providerId]: `✗ Error: ${errorMessage}` })); } // Resetear estado después de 5 segundos setTimeout(() => { setTestingStates(prev => ({ ...prev, [providerId]: 'idle' })); }, 5000); }; const handleSave = () => { localStorage.setItem('aiProviderConfigs', JSON.stringify(configs)); alert('Configuración guardada correctamente'); }; return (
AI Provider Settings
Configure sus API Keys y URLs para cada proveedor de IA. Los providers habilitados estarán disponibles en el selector de modelos.
{AI_PROVIDERS.map((provider) => { const config = configs[provider.id] || { apiKey: '', apiUrl: DEFAULT_API_URLS[provider.id] || '', useCustomUrl: false, enabled: false, }; const testState = testingStates[provider.id] || 'idle'; const testMessage = testMessages[provider.id]; return (
{provider.icon}
{provider.name}
{provider.models.length} modelos disponibles
{config.enabled ? '✓ Habilitado' : '○ Deshabilitado'}
{/* API Key */}
handleApiKeyChange(provider.id, e.target.value)} placeholder={`Ingrese su ${provider.name} API Key`} />
{/* Test Result Message */} {testMessage && (
{testMessage}
)} {/* API URL */}
handleApiUrlChange(provider.id, e.target.value)} placeholder={DEFAULT_API_URLS[provider.id]} disabled={!config.useCustomUrl} style={{ opacity: config.useCustomUrl ? 1 : 0.6 }} />
handleUseCustomUrlChange(provider.id, e.target.checked)} />
); })}
); };