Major updates and improvements to BZZZ system
- Updated configuration and deployment files - Improved system architecture and components - Enhanced documentation and testing - Fixed various issues and added new features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user