Files
bzzz/install/config-ui/app/setup/components/AIConfiguration.tsx
anthonyrawlins be761cfe20 Enhance deployment system with retry functionality and improved UX
Major Improvements:
- Added retry deployment buttons in machine list for failed deployments
- Added retry button in SSH console modal footer for enhanced UX
- Enhanced deployment process with comprehensive cleanup of existing services
- Improved binary installation with password-based sudo authentication
- Updated configuration generation to include all required sections (agent, ai, network, security)
- Fixed deployment verification and error handling

Security Enhancements:
- Enhanced verifiedStopExistingServices with thorough cleanup process
- Improved binary copying with proper sudo authentication
- Added comprehensive configuration validation

UX Improvements:
- Users can retry deployments without re-running machine discovery
- Retry buttons available from both machine list and console modal
- Real-time deployment progress with detailed console output
- Clear error states with actionable retry options

Technical Changes:
- Modified ServiceDeployment.tsx with retry button components
- Enhanced api/setup_manager.go with improved deployment functions
- Updated main.go with command line argument support (--config, --setup)
- Added comprehensive zero-trust security validation system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 10:23:27 +10:00

634 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('/api/setup/ollama/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: config.localAIEndpoint
})
})
const result = await response.json()
if (result.valid && result.models) {
setLocalAIValid(true)
// Update the local AI models list with discovered models
setConfig(prev => ({ ...prev, localAIModels: result.models }))
} else {
setLocalAIValid(false)
console.error('Ollama validation failed:', result.message)
}
} catch (error) {
setLocalAIValid(false)
console.error('Ollama validation error:', error)
} 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-eucalyptus-50 border-eucalyptus-950' :
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-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-600' :
'text-blue-600'
}`} />
<div>
<div className={`font-medium ${
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-800' :
'text-blue-800'
}`}>
{gpuRecommendation.recommendation}
</div>
<div className={`text-sm mt-1 ${
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
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-eucalyptus-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-eucalyptus-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>
)
}