Spaces:
Runtime error
Runtime error
| import React, { useState, useEffect } from 'react'; | |
| import { X, Settings, Check, AlertCircle, ExternalLink, Download, Key, Server, Zap } from 'lucide-react'; | |
| import { AI_PROVIDERS, AIProviderManager } from '../utils/ai-providers'; | |
| const token = import.meta.env.VITE_HF_TOKEN; | |
| interface AIConfigModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| } | |
| const AIConfigModal: React.FC<AIConfigModalProps> = ({ isOpen, onClose }) => { | |
| const [config, setConfig] = useState(AIProviderManager.getConfig()); | |
| const [connectionStatus, setConnectionStatus] = useState<Record<string, boolean>>({}); | |
| const [testing, setTesting] = useState<string | null>(null); | |
| const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({}); | |
| useEffect(() => { | |
| if (isOpen) { | |
| const currentConfig = AIProviderManager.getConfig(); | |
| setConfig(currentConfig); | |
| // Auto-configure Hugging Face if not already set | |
| if (!currentConfig.apiKeys?.huggingface) { | |
| const newConfig = AIProviderManager.updateConfig({ | |
| selectedProvider: 'huggingface', | |
| selectedModel: 'microsoft/DialoGPT-medium', | |
| apiKeys: { | |
| ...currentConfig.apiKeys, | |
| huggingface: token | |
| } | |
| }); | |
| setConfig(newConfig); | |
| // Test connection automatically | |
| setTimeout(() => { | |
| testConnection('huggingface'); | |
| }, 500); | |
| } | |
| } | |
| }, [isOpen]); | |
| const handleProviderChange = (providerId: string) => { | |
| const provider = AI_PROVIDERS.find(p => p.id === providerId); | |
| const newConfig = AIProviderManager.updateConfig({ | |
| selectedProvider: providerId, | |
| selectedModel: provider?.models[0]?.id || '' | |
| }); | |
| setConfig(newConfig); | |
| }; | |
| const handleModelChange = (modelId: string) => { | |
| const newConfig = AIProviderManager.updateConfig({ | |
| selectedModel: modelId | |
| }); | |
| setConfig(newConfig); | |
| }; | |
| const handleApiKeyChange = (providerId: string, apiKey: string) => { | |
| const newConfig = AIProviderManager.updateConfig({ | |
| apiKeys: { ...config.apiKeys, [providerId]: apiKey } | |
| }); | |
| setConfig(newConfig); | |
| }; | |
| const handleEndpointChange = (providerId: string, endpoint: string) => { | |
| const newConfig = AIProviderManager.updateConfig({ | |
| customEndpoints: { ...config.customEndpoints, [providerId]: endpoint } | |
| }); | |
| setConfig(newConfig); | |
| }; | |
| const testConnection = async (providerId: string) => { | |
| setTesting(providerId); | |
| try { | |
| const isConnected = await AIProviderManager.testConnection(providerId); | |
| setConnectionStatus(prev => ({ ...prev, [providerId]: isConnected })); | |
| if (isConnected && providerId === 'huggingface') { | |
| // Show success message | |
| console.log('✅ Hugging Face connected successfully!'); | |
| } | |
| } catch (error) { | |
| setConnectionStatus(prev => ({ ...prev, [providerId]: false })); | |
| console.error(`Connection test failed for ${providerId}:`, error); | |
| } finally { | |
| setTesting(null); | |
| } | |
| }; | |
| const selectedProvider = AI_PROVIDERS.find(p => p.id === config.selectedProvider); | |
| const selectedModel = selectedProvider?.models.find(m => m.id === config.selectedModel); | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> | |
| <div className="bg-white rounded-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"> | |
| <div className="p-6 border-b border-gray-200"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="w-10 h-10 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl flex items-center justify-center"> | |
| <Settings className="w-5 h-5 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="text-xl font-bold text-gray-900">AI Configuration</h2> | |
| <p className="text-gray-600">Configure your open-source AI providers</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-2 hover:bg-gray-100 rounded-lg transition-colors" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="p-6 space-y-8"> | |
| {/* Success Banner for Hugging Face */} | |
| {config.apiKeys?.huggingface && connectionStatus.huggingface === true && ( | |
| <div className="bg-green-50 border border-green-200 rounded-xl p-4"> | |
| <div className="flex items-center space-x-3"> | |
| <Check className="w-5 h-5 text-green-600" /> | |
| <div> | |
| <h4 className="font-medium text-green-900">🎉 Hugging Face Connected!</h4> | |
| <p className="text-green-700 text-sm">Your API key is working perfectly. AI enhancement is now available!</p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Provider Selection */} | |
| <div> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4">Select AI Provider</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {AI_PROVIDERS.map((provider) => ( | |
| <div | |
| key={provider.id} | |
| onClick={() => handleProviderChange(provider.id)} | |
| className={`p-4 border-2 rounded-xl cursor-pointer transition-all ${ | |
| config.selectedProvider === provider.id | |
| ? 'border-primary-500 bg-primary-50' | |
| : 'border-gray-200 hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <h4 className="font-semibold text-gray-900">{provider.name}</h4> | |
| {provider.isLocal && ( | |
| <span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded-full"> | |
| Local | |
| </span> | |
| )} | |
| {provider.requiresApiKey && ( | |
| <span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full"> | |
| API Key | |
| </span> | |
| )} | |
| {provider.id === 'huggingface' && config.apiKeys?.huggingface && ( | |
| <span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded-full"> | |
| ✓ Configured | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-sm text-gray-600 mb-3">{provider.description}</p> | |
| <div className="text-xs text-gray-500"> | |
| {provider.models.length} models available | |
| </div> | |
| </div> | |
| <div className="ml-4"> | |
| {connectionStatus[provider.id] === true && ( | |
| <Check className="w-5 h-5 text-green-600" /> | |
| )} | |
| {connectionStatus[provider.id] === false && ( | |
| <AlertCircle className="w-5 h-5 text-red-600" /> | |
| )} | |
| </div> | |
| </div> | |
| <div className="mt-3 flex items-center space-x-2"> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| testConnection(provider.id); | |
| }} | |
| disabled={testing === provider.id} | |
| className="flex items-center space-x-1 px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 transition-colors disabled:opacity-50" | |
| > | |
| <Zap className="w-3 h-3" /> | |
| <span>{testing === provider.id ? 'Testing...' : 'Test'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Provider Configuration */} | |
| {selectedProvider && ( | |
| <div className="space-y-6"> | |
| {/* API Key Configuration */} | |
| {selectedProvider.requiresApiKey && ( | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| API Key for {selectedProvider.name} | |
| </label> | |
| <div className="relative"> | |
| <input | |
| type={showApiKey[selectedProvider.id] ? 'text' : 'password'} | |
| value={config.apiKeys[selectedProvider.id] || ''} | |
| onChange={(e) => handleApiKeyChange(selectedProvider.id, e.target.value)} | |
| placeholder="Enter your API key..." | |
| className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent pr-12" | |
| /> | |
| <button | |
| onClick={() => setShowApiKey(prev => ({ ...prev, [selectedProvider.id]: !prev[selectedProvider.id] }))} | |
| className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded" | |
| > | |
| <Key className="w-4 h-4 text-gray-400" /> | |
| </button> | |
| </div> | |
| <p className="text-xs text-gray-500 mt-1"> | |
| Your API key is stored locally and never sent to our servers | |
| </p> | |
| {selectedProvider.id === 'huggingface' && config.apiKeys?.huggingface && ( | |
| <p className="text-xs text-green-600 mt-1"> | |
| ✓ API key configured and ready to use | |
| </p> | |
| )} | |
| </div> | |
| )} | |
| {/* Custom Endpoint Configuration */} | |
| {(selectedProvider.id === 'openai-compatible') && ( | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Custom Endpoint | |
| </label> | |
| <div className="relative"> | |
| <input | |
| type="text" | |
| value={config.customEndpoints[selectedProvider.id] || selectedProvider.apiUrl} | |
| onChange={(e) => handleEndpointChange(selectedProvider.id, e.target.value)} | |
| placeholder="http://localhost:8080" | |
| className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent pl-12" | |
| /> | |
| <Server className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" /> | |
| </div> | |
| </div> | |
| )} | |
| {/* Model Selection */} | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-3"> | |
| Select Model | |
| </label> | |
| <div className="grid grid-cols-1 gap-3"> | |
| {selectedProvider.models.map((model) => ( | |
| <div | |
| key={model.id} | |
| onClick={() => handleModelChange(model.id)} | |
| className={`p-4 border-2 rounded-xl cursor-pointer transition-all ${ | |
| config.selectedModel === model.id | |
| ? 'border-primary-500 bg-primary-50' | |
| : 'border-gray-200 hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <h4 className="font-medium text-gray-900">{model.name}</h4> | |
| <p className="text-sm text-gray-600 mt-1">{model.description}</p> | |
| <div className="flex items-center space-x-4 mt-2 text-xs text-gray-500"> | |
| <span>Max tokens: {model.maxTokens.toLocaleString()}</span> | |
| <span>Capabilities: {model.capabilities.join(', ')}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Installation Instructions */} | |
| {selectedProvider?.isLocal && ( | |
| <div className="bg-blue-50 border border-blue-200 rounded-xl p-4"> | |
| <h4 className="font-medium text-blue-900 mb-2">Installation Instructions</h4> | |
| {selectedProvider.id === 'ollama' && ( | |
| <div className="space-y-2 text-sm text-blue-800"> | |
| <p>1. Install Ollama from <a href="https://ollama.ai" target="_blank" rel="noopener noreferrer" className="underline">ollama.ai</a></p> | |
| <p>2. Run: <code className="bg-blue-100 px-2 py-1 rounded">ollama pull llama3.2</code></p> | |
| <p>3. Start Ollama service: <code className="bg-blue-100 px-2 py-1 rounded">ollama serve</code></p> | |
| </div> | |
| )} | |
| {selectedProvider.id === 'openai-compatible' && ( | |
| <div className="space-y-2 text-sm text-blue-800"> | |
| <p>Compatible with LocalAI, vLLM, FastChat, and other OpenAI-compatible APIs</p> | |
| <p>Set your custom endpoint URL above</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Current Configuration Summary */} | |
| {selectedProvider && selectedModel && ( | |
| <div className="bg-gray-50 border border-gray-200 rounded-xl p-4"> | |
| <h4 className="font-medium text-gray-900 mb-2">Current Configuration</h4> | |
| <div className="space-y-1 text-sm text-gray-600"> | |
| <p><strong>Provider:</strong> {selectedProvider.name}</p> | |
| <p><strong>Model:</strong> {selectedModel.name}</p> | |
| <p><strong>Max Tokens:</strong> {selectedModel.maxTokens.toLocaleString()}</p> | |
| <p><strong>Capabilities:</strong> {selectedModel.capabilities.join(', ')}</p> | |
| {config.apiKeys?.huggingface && ( | |
| <p><strong>Status:</strong> <span className="text-green-600">✓ Ready to use</span></p> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="p-6 border-t border-gray-200"> | |
| <div className="flex items-center justify-between"> | |
| <div className="text-sm text-gray-600"> | |
| All configurations are stored locally in your browser | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors" | |
| > | |
| Done | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default AIConfigModal; |