wiki-project / src /components /AIConfigModal.tsx
Nagi15's picture
Add codebase
fcb5a67
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;