🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
615 lines
22 KiB
TypeScript
615 lines
22 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
CpuChipIcon,
|
|
SparklesIcon,
|
|
CurrencyDollarIcon,
|
|
ServerIcon,
|
|
CheckCircleIcon,
|
|
ExclamationTriangleIcon,
|
|
InformationCircleIcon,
|
|
EyeIcon,
|
|
EyeSlashIcon,
|
|
ArrowPathIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
interface GPUInfo {
|
|
name: string
|
|
memory: string
|
|
type: string
|
|
driver: string
|
|
}
|
|
|
|
interface AIConfig {
|
|
// OpenAI Configuration
|
|
openaiEnabled: boolean
|
|
openaiApiKey: string
|
|
openaiOrganization: string
|
|
openaiDefaultModel: string
|
|
|
|
// Cost Management
|
|
dailyCostLimit: number
|
|
monthlyCostLimit: number
|
|
costAlerts: boolean
|
|
|
|
// Local AI (Ollama/Parallama)
|
|
localAIEnabled: boolean
|
|
localAIType: 'ollama' | 'parallama'
|
|
localAIEndpoint: string
|
|
localAIModels: string[]
|
|
|
|
// GPU Configuration
|
|
gpuAcceleration: boolean
|
|
preferredGPU: string
|
|
maxGPUMemory: number
|
|
|
|
// Model Selection
|
|
preferredProvider: 'openai' | 'local' | 'hybrid'
|
|
fallbackEnabled: boolean
|
|
}
|
|
|
|
interface AIConfigurationProps {
|
|
systemInfo: any
|
|
configData: any
|
|
onComplete: (data: any) => void
|
|
onBack?: () => void
|
|
isCompleted: boolean
|
|
}
|
|
|
|
export default function AIConfiguration({
|
|
systemInfo,
|
|
configData,
|
|
onComplete,
|
|
onBack,
|
|
isCompleted
|
|
}: AIConfigurationProps) {
|
|
const [config, setConfig] = useState<AIConfig>({
|
|
openaiEnabled: false,
|
|
openaiApiKey: '',
|
|
openaiOrganization: '',
|
|
openaiDefaultModel: 'gpt-4',
|
|
|
|
dailyCostLimit: 50,
|
|
monthlyCostLimit: 500,
|
|
costAlerts: true,
|
|
|
|
localAIEnabled: true,
|
|
localAIType: 'ollama',
|
|
localAIEndpoint: 'http://localhost:11434',
|
|
localAIModels: ['llama2', 'codellama'],
|
|
|
|
gpuAcceleration: false,
|
|
preferredGPU: '',
|
|
maxGPUMemory: 8,
|
|
|
|
preferredProvider: 'local',
|
|
fallbackEnabled: true
|
|
})
|
|
|
|
const [showApiKey, setShowApiKey] = useState(false)
|
|
const [validatingOpenAI, setValidatingOpenAI] = useState(false)
|
|
const [validatingLocal, setValidatingLocal] = useState(false)
|
|
const [openaiValid, setOpenaiValid] = useState<boolean | null>(null)
|
|
const [localAIValid, setLocalAIValid] = useState<boolean | null>(null)
|
|
|
|
// Initialize configuration from existing data
|
|
useEffect(() => {
|
|
if (configData.ai) {
|
|
setConfig(prev => ({ ...prev, ...configData.ai }))
|
|
}
|
|
|
|
// Auto-detect GPU capabilities
|
|
if (systemInfo?.gpus?.length > 0) {
|
|
const hasNVIDIA = systemInfo.gpus.some((gpu: GPUInfo) => gpu.type === 'nvidia')
|
|
const hasAMD = systemInfo.gpus.some((gpu: GPUInfo) => gpu.type === 'amd')
|
|
|
|
if (hasNVIDIA) {
|
|
setConfig(prev => ({
|
|
...prev,
|
|
gpuAcceleration: true,
|
|
localAIType: 'parallama', // Parallama typically better for NVIDIA
|
|
preferredGPU: systemInfo.gpus.find((gpu: GPUInfo) => gpu.type === 'nvidia')?.name || ''
|
|
}))
|
|
} else if (hasAMD) {
|
|
setConfig(prev => ({
|
|
...prev,
|
|
gpuAcceleration: true,
|
|
localAIType: 'ollama', // Ollama works well with AMD
|
|
preferredGPU: systemInfo.gpus.find((gpu: GPUInfo) => gpu.type === 'amd')?.name || ''
|
|
}))
|
|
}
|
|
}
|
|
}, [systemInfo, configData])
|
|
|
|
const validateOpenAI = async () => {
|
|
if (!config.openaiApiKey) {
|
|
setOpenaiValid(false)
|
|
return
|
|
}
|
|
|
|
setValidatingOpenAI(true)
|
|
try {
|
|
// This would be a real API validation in production
|
|
// For now, just simulate validation
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
setOpenaiValid(true)
|
|
} catch (error) {
|
|
setOpenaiValid(false)
|
|
} finally {
|
|
setValidatingOpenAI(false)
|
|
}
|
|
}
|
|
|
|
const validateLocalAI = async () => {
|
|
if (!config.localAIEndpoint) {
|
|
setLocalAIValid(false)
|
|
return
|
|
}
|
|
|
|
setValidatingLocal(true)
|
|
try {
|
|
const response = await fetch(`${config.localAIEndpoint}/api/tags`)
|
|
setLocalAIValid(response.ok)
|
|
} catch (error) {
|
|
setLocalAIValid(false)
|
|
} finally {
|
|
setValidatingLocal(false)
|
|
}
|
|
}
|
|
|
|
const getGPURecommendations = () => {
|
|
if (!systemInfo?.gpus?.length) {
|
|
return {
|
|
recommendation: 'No GPU detected. CPU-only processing will be used.',
|
|
type: 'info',
|
|
details: 'Consider adding a GPU for better AI performance.'
|
|
}
|
|
}
|
|
|
|
const gpus = systemInfo.gpus
|
|
const nvidiaGPUs = gpus.filter((gpu: GPUInfo) => gpu.type === 'nvidia')
|
|
const amdGPUs = gpus.filter((gpu: GPUInfo) => gpu.type === 'amd')
|
|
|
|
if (nvidiaGPUs.length > 0) {
|
|
return {
|
|
recommendation: 'NVIDIA GPU detected - Parallama recommended for optimal performance',
|
|
type: 'success',
|
|
details: `${nvidiaGPUs[0].name} with ${nvidiaGPUs[0].memory} VRAM detected. Parallama provides excellent NVIDIA GPU acceleration.`
|
|
}
|
|
}
|
|
|
|
if (amdGPUs.length > 0) {
|
|
return {
|
|
recommendation: 'AMD GPU detected - Ollama with ROCm support recommended',
|
|
type: 'warning',
|
|
details: `${amdGPUs[0].name} detected. Ollama provides good AMD GPU support through ROCm.`
|
|
}
|
|
}
|
|
|
|
return {
|
|
recommendation: 'Integrated GPU detected - Limited AI acceleration available',
|
|
type: 'warning',
|
|
details: 'Integrated GPUs provide limited AI acceleration. Consider a dedicated GPU for better performance.'
|
|
}
|
|
}
|
|
|
|
const getRecommendedModels = () => {
|
|
const memoryGB = systemInfo?.memory_mb ? Math.round(systemInfo.memory_mb / 1024) : 8
|
|
|
|
if (memoryGB >= 32) {
|
|
return ['llama2:70b', 'codellama:34b', 'mixtral:8x7b']
|
|
} else if (memoryGB >= 16) {
|
|
return ['llama2:13b', 'codellama:13b', 'llama2:7b']
|
|
} else {
|
|
return ['llama2:7b', 'codellama:7b', 'phi2']
|
|
}
|
|
}
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
// Validate that at least one AI provider is configured
|
|
if (!config.openaiEnabled && !config.localAIEnabled) {
|
|
alert('Please enable at least one AI provider (OpenAI or Local AI)')
|
|
return
|
|
}
|
|
|
|
onComplete({ ai: config })
|
|
}
|
|
|
|
const gpuRecommendation = getGPURecommendations()
|
|
const recommendedModels = getRecommendedModels()
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* GPU Detection & Recommendations */}
|
|
{systemInfo?.gpus && (
|
|
<div className="bg-gray-50 rounded-lg p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
|
<CpuChipIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
GPU Configuration
|
|
</h3>
|
|
|
|
<div className={`p-4 rounded-lg border mb-4 ${
|
|
gpuRecommendation.type === 'success' ? 'bg-green-50 border-green-200' :
|
|
gpuRecommendation.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
|
|
'bg-blue-50 border-blue-200'
|
|
}`}>
|
|
<div className="flex items-start">
|
|
<InformationCircleIcon className={`h-5 w-5 mt-0.5 mr-2 ${
|
|
gpuRecommendation.type === 'success' ? 'text-green-600' :
|
|
gpuRecommendation.type === 'warning' ? 'text-yellow-600' :
|
|
'text-blue-600'
|
|
}`} />
|
|
<div>
|
|
<div className={`font-medium ${
|
|
gpuRecommendation.type === 'success' ? 'text-green-800' :
|
|
gpuRecommendation.type === 'warning' ? 'text-yellow-800' :
|
|
'text-blue-800'
|
|
}`}>
|
|
{gpuRecommendation.recommendation}
|
|
</div>
|
|
<div className={`text-sm mt-1 ${
|
|
gpuRecommendation.type === 'success' ? 'text-green-700' :
|
|
gpuRecommendation.type === 'warning' ? 'text-yellow-700' :
|
|
'text-blue-700'
|
|
}`}>
|
|
{gpuRecommendation.details}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{systemInfo.gpus.length > 0 && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="gpuAcceleration"
|
|
checked={config.gpuAcceleration}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, gpuAcceleration: e.target.checked }))}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="gpuAcceleration" className="ml-2 text-sm font-medium text-gray-700">
|
|
Enable GPU acceleration for AI processing
|
|
</label>
|
|
</div>
|
|
|
|
{config.gpuAcceleration && (
|
|
<div>
|
|
<label className="label">Preferred GPU</label>
|
|
<select
|
|
value={config.preferredGPU}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, preferredGPU: e.target.value }))}
|
|
className="input-field"
|
|
>
|
|
<option value="">Auto-select</option>
|
|
{systemInfo.gpus.map((gpu: GPUInfo, index: number) => (
|
|
<option key={index} value={gpu.name}>
|
|
{gpu.name} ({gpu.type.toUpperCase()}) - {gpu.memory}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Local AI Configuration */}
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
|
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
Local AI (Ollama/Parallama)
|
|
</h3>
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="localAIEnabled"
|
|
checked={config.localAIEnabled}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, localAIEnabled: e.target.checked }))}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="localAIEnabled" className="ml-2 text-sm font-medium text-gray-700">
|
|
Enable Local AI
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{config.localAIEnabled && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="label">Local AI Provider</label>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div
|
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
|
config.localAIType === 'ollama'
|
|
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
onClick={() => setConfig(prev => ({ ...prev, localAIType: 'ollama' }))}
|
|
>
|
|
<div className="font-medium text-gray-900">Ollama</div>
|
|
<div className="text-sm text-gray-600">Open-source, self-hosted AI models</div>
|
|
<div className="text-xs text-gray-500 mt-1">Best for: AMD GPUs, CPU-only setups</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
|
config.localAIType === 'parallama'
|
|
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
onClick={() => setConfig(prev => ({ ...prev, localAIType: 'parallama' }))}
|
|
>
|
|
<div className="font-medium text-gray-900">Parallama</div>
|
|
<div className="text-sm text-gray-600">Optimized for parallel processing</div>
|
|
<div className="text-xs text-gray-500 mt-1">Best for: NVIDIA GPUs, high performance</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">API Endpoint</label>
|
|
<div className="flex space-x-2">
|
|
<input
|
|
type="url"
|
|
value={config.localAIEndpoint}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, localAIEndpoint: e.target.value }))}
|
|
placeholder="http://localhost:11434"
|
|
className="input-field flex-1"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={validateLocalAI}
|
|
disabled={validatingLocal}
|
|
className="btn-outline whitespace-nowrap"
|
|
>
|
|
{validatingLocal ? (
|
|
<ArrowPathIcon className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
'Test'
|
|
)}
|
|
</button>
|
|
</div>
|
|
{localAIValid === true && (
|
|
<div className="flex items-center mt-1 text-green-600 text-sm">
|
|
<CheckCircleIcon className="h-4 w-4 mr-1" />
|
|
Connection successful
|
|
</div>
|
|
)}
|
|
{localAIValid === false && (
|
|
<div className="flex items-center mt-1 text-red-600 text-sm">
|
|
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
|
|
Connection failed
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Recommended Models for your system</label>
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<div className="text-sm text-blue-800">
|
|
<p className="font-medium mb-2">Based on your system memory ({Math.round(systemInfo?.memory_mb / 1024 || 8)} GB):</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{recommendedModels.map((model, index) => (
|
|
<span key={index} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
|
|
{model}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* OpenAI Configuration */}
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
|
<SparklesIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
OpenAI API
|
|
</h3>
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="openaiEnabled"
|
|
checked={config.openaiEnabled}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, openaiEnabled: e.target.checked }))}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="openaiEnabled" className="ml-2 text-sm font-medium text-gray-700">
|
|
Enable OpenAI API
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{config.openaiEnabled && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="label">API Key</label>
|
|
<div className="flex space-x-2">
|
|
<div className="relative flex-1">
|
|
<input
|
|
type={showApiKey ? 'text' : 'password'}
|
|
value={config.openaiApiKey}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, openaiApiKey: e.target.value }))}
|
|
placeholder="sk-..."
|
|
className="input-field pr-10"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowApiKey(!showApiKey)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
>
|
|
{showApiKey ? (
|
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
|
) : (
|
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={validateOpenAI}
|
|
disabled={validatingOpenAI || !config.openaiApiKey}
|
|
className="btn-outline whitespace-nowrap"
|
|
>
|
|
{validatingOpenAI ? (
|
|
<ArrowPathIcon className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
'Validate'
|
|
)}
|
|
</button>
|
|
</div>
|
|
{openaiValid === true && (
|
|
<div className="flex items-center mt-1 text-green-600 text-sm">
|
|
<CheckCircleIcon className="h-4 w-4 mr-1" />
|
|
API key valid
|
|
</div>
|
|
)}
|
|
{openaiValid === false && (
|
|
<div className="flex items-center mt-1 text-red-600 text-sm">
|
|
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
|
|
Invalid API key
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Organization (Optional)</label>
|
|
<input
|
|
type="text"
|
|
value={config.openaiOrganization}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, openaiOrganization: e.target.value }))}
|
|
placeholder="org-..."
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Default Model</label>
|
|
<select
|
|
value={config.openaiDefaultModel}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, openaiDefaultModel: e.target.value }))}
|
|
className="input-field"
|
|
>
|
|
<option value="gpt-4">GPT-4</option>
|
|
<option value="gpt-4-turbo">GPT-4 Turbo</option>
|
|
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cost Management */}
|
|
{config.openaiEnabled && (
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
|
<CurrencyDollarIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
Cost Management
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Daily Cost Limit ($)</label>
|
|
<input
|
|
type="number"
|
|
value={config.dailyCostLimit}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, dailyCostLimit: parseFloat(e.target.value) || 0 }))}
|
|
min="0"
|
|
step="0.01"
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Monthly Cost Limit ($)</label>
|
|
<input
|
|
type="number"
|
|
value={config.monthlyCostLimit}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, monthlyCostLimit: parseFloat(e.target.value) || 0 }))}
|
|
min="0"
|
|
step="0.01"
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="costAlerts"
|
|
checked={config.costAlerts}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, costAlerts: e.target.checked }))}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="costAlerts" className="ml-2 text-sm font-medium text-gray-700">
|
|
Send alerts when approaching cost limits
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Provider Preference */}
|
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Provider Preference</h3>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="label">Preferred AI Provider</label>
|
|
<select
|
|
value={config.preferredProvider}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, preferredProvider: e.target.value as 'openai' | 'local' | 'hybrid' }))}
|
|
className="input-field"
|
|
>
|
|
<option value="local">Local AI Only</option>
|
|
<option value="openai">OpenAI Only</option>
|
|
<option value="hybrid">Hybrid (Local first, OpenAI fallback)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="fallbackEnabled"
|
|
checked={config.fallbackEnabled}
|
|
onChange={(e) => setConfig(prev => ({ ...prev, fallbackEnabled: e.target.checked }))}
|
|
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="fallbackEnabled" className="ml-2 text-sm font-medium text-gray-700">
|
|
Enable automatic fallback between providers
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-between pt-6 border-t border-gray-200">
|
|
<div>
|
|
{onBack && (
|
|
<button type="button" onClick={onBack} className="btn-outline">
|
|
Back
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="btn-primary"
|
|
disabled={!config.openaiEnabled && !config.localAIEnabled}
|
|
>
|
|
{isCompleted ? 'Continue' : 'Next: Resource Allocation'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
} |