kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
// src/components/EngineManager.tsx
import { useState, useEffect } from 'react';
import apiClient from '../services/api';
interface ModelConfig {
model_id: string;
display_name: string;
provider?: string;
model_name?: string;
max_tokens?: number;
temperature?: number;
timeout?: number;
is_default?: boolean;
supported_domains?: string[];
description?: string;
}
interface EngineStatus extends ModelConfig {
status: 'master' | 'apprentice' | 'available' | 'retired';
unique_id?: string; // For apprentice engines
}
const EngineManager = () => {
const [allEngines, setAllEngines] = useState<EngineStatus[]>([]);
const [currentMasterId, setCurrentMasterId] = useState('');
const [currentApprenticeId, setCurrentApprenticeId] = useState('none');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [newModelProvider, setNewModelProvider] = useState('ollama');
const [newModelId, setNewModelId] = useState('');
const [newModelDisplayName, setNewModelDisplayName] = useState('');
const [newModelModelName, setNewModelModelName] = useState('');
const [newModelSupportedDomains, setNewModelSupportedDomains] = useState(''); // Comma separated
const [newModelDescription, setNewModelDescription] = useState('');
const [newModelFile, setNewModelFile] = useState<File | null>(null); // For GGUF file upload
const fetchModelsAndActiveEngines = async () => {
try {
const [allEnginesResponse, activeEnginesResponse] = await Promise.all([
apiClient.get('/config/engines'), // Fetch all engines with their statuses
apiClient.get('/config/engines/active') // Fetch current active master/apprentice
]);
setAllEngines(allEnginesResponse.data);
const { master_engine_id, apprentice_engine_id } = activeEnginesResponse.data;
if (master_engine_id) setCurrentMasterId(master_engine_id);
if (apprentice_engine_id) setCurrentApprenticeId(apprentice_engine_id);
} catch (err) {
setError('Failed to load engines or active engines.');
console.error(err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchModelsAndActiveEngines();
}, []);
const handleSetActiveEngines = async () => {
try {
await apiClient.post('/config/engines/active', {
master_engine_id: currentMasterId,
apprentice_engine_id: currentApprenticeId === 'none' ? null : currentApprenticeId,
});
alert('Active engines updated successfully!');
fetchModelsAndActiveEngines(); // Re-fetch to confirm and update UI
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to update active engines: ${errorMessage}`);
console.error(err);
}
};
const handleSubmitNewModel = async () => {
if (!newModelId || !newModelDisplayName || !newModelModelName) {
alert('Please fill in Model ID, Display Name, and Model Name.');
return;
}
let actualModelName = newModelModelName; // Default to user-provided model name
// Handle GGUF file upload first if applicable
if (newModelProvider === 'gguf') {
if (!newModelFile) {
alert('Please select a GGUF file for the GGUF provider.');
return;
}
try {
const formData = new FormData();
formData.append('file', newModelFile);
const uploadResponse = await apiClient.post('/config/upload-gguf', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
actualModelName = uploadResponse.data.path; // Use the path returned by the backend
alert(`GGUF file uploaded successfully to: ${actualModelName}`);
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to upload GGUF file: ${errorMessage}`);
console.error(err);
return; // Stop processing if file upload fails
}
}
const modelData: ModelConfig = {
model_id: newModelId,
display_name: newModelDisplayName,
provider: newModelProvider,
model_name: actualModelName, // Use the actualModelName (might be local file path)
max_tokens: 4096, // Default values
temperature: 0.7,
timeout: 120,
is_default: false,
supported_domains: newModelSupportedDomains.split(',').map(s => s.trim()).filter(s => s),
description: newModelDescription,
};
try {
await apiClient.post('/config/models', modelData);
alert('Model registered successfully!');
fetchModelsAndActiveEngines(); // Refresh the model list
// Clear form
setNewModelId('');
setNewModelDisplayName('');
setNewModelModelName('');
setNewModelSupportedDomains('');
setNewModelDescription('');
setNewModelFile(null);
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to register model: ${errorMessage}`);
console.error(err);
}
};
if (isLoading) {
return <div className="panel-section"><h3>Engine Management</h3><p>Loading models...</p></div>;
}
if (error) {
return <div className="panel-section"><h3>Engine Management</h3><p style={{color: 'red'}}>{error}</p></div>;
}
const handleSwapEngines = async (apprenticeModelId: string) => {
try {
await apiClient.post('/config/engines/swap', { apprentice_model_id: apprenticeModelId });
alert('Engines swapped successfully!');
fetchModelsAndActiveEngines(); // Refresh UI
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to swap engines: ${errorMessage}`);
console.error(err);
}
};
const handlePromoteApprentice = async (apprenticeModelId: string) => {
try {
await apiClient.post('/config/engines/promote', { apprentice_model_id: apprenticeModelId });
alert('Apprentice promoted to Master successfully!');
fetchModelsAndActiveEngines(); // Refresh UI
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to promote apprentice: ${errorMessage}`);
console.error(err);
}
};
const handleCreateNewApprentice = async () => {
try {
const response = await apiClient.post('/config/engines/apprentice/new');
alert(response.data.message);
fetchModelsAndActiveEngines(); // Refresh UI
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message;
alert(`Failed to create new apprentice: ${errorMessage}`);
console.error(err);
}
};
return (
<div className="panel-section">
<h3>Engine Management</h3>
{/* Current Active Engines Display */}
<div className="mb-4 p-3 border rounded">
<p className="text-sm font-medium">
Master Engine:{' '}
<span className="font-semibold text-blue-600">
{allEngines.find(e => e.model_id === currentMasterId)?.display_name || 'Not Set'}
</span>
</p>
<p className="text-sm font-medium">
Apprentice Engine:{' '}
<span className="font-semibold text-green-600">
{allEngines.find(e => e.model_id === currentApprenticeId)?.display_name || 'None'}
</span>
</p>
</div>
{/* Set Active Engines (existing functionality, but updated) */}
<div className="mb-4">
<h4>Set Current Master/Apprentice</h4>
<div className="form-group">
<label>Master (Teacher) Engine</label>
<select value={currentMasterId} onChange={(e) => setCurrentMasterId(e.target.value)}>
<option value="">Select Master</option>
{allEngines.filter(e => e.status !== 'retired').map(engine => (
<option key={engine.model_id} value={engine.model_id}>
{engine.display_name} ({engine.status === 'master' ? 'Current Master' : engine.status === 'apprentice' ? 'Current Apprentice' : engine.status})
</option>
))}
</select>
</div>
<div className="form-group">
<label>Apprentice (Student) Engine</label>
<select value={currentApprenticeId} onChange={(e) => setCurrentApprenticeId(e.target.value)}>
<option value="none">None</option>
{allEngines.filter(e => e.status !== 'master' && e.status !== 'retired').map(engine => (
<option key={engine.model_id} value={engine.model_id}>
{engine.display_name} ({engine.status === 'apprentice' ? 'Current Apprentice' : engine.status}) {engine.unique_id ? `[${engine.unique_id.substring(0, 4)}]` : ''}
</option>
))}
</select>
</div>
<button onClick={handleSetActiveEngines} className="btn btn-primary">
Set Active Engines
</button>
</div>
{/* New: Advanced Engine Management */}
<div style={{ marginTop: '1.5rem', borderTop: '1px solid #eee', paddingTop: '1.5rem' }}>
<h4>Advanced Engine Operations</h4>
{/* Swap Master and Apprentice */}
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700">Swap Master and Apprentice</label>
<select
value={""} // Temporary value
onChange={(e) => {
const selectedApprenticeId = e.target.value;
if (selectedApprenticeId) handleSwapEngines(selectedApprenticeId);
e.target.value = ""; // Reset dropdown
}}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="">Select Apprentice to Swap with Master</option>
{allEngines.filter(e => e.status === 'apprentice' || (e.status === 'available' && e.unique_id)).map(engine => (
<option key={engine.model_id} value={engine.model_id}>
{engine.display_name} {engine.unique_id ? `[${engine.unique_id.substring(0, 4)}]` : ''}
</option>
))}
</select>
</div>
{/* Promote Apprentice */}
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700">Promote Apprentice to Master</label>
<select
value={""} // Temporary value
onChange={(e) => {
const selectedApprenticeId = e.target.value;
if (selectedApprenticeId) handlePromoteApprentice(selectedApprenticeId);
e.target.value = ""; // Reset dropdown
}}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="">Select Apprentice to Promote</option>
{allEngines.filter(e => e.status === 'apprentice' || (e.status === 'available' && e.unique_id)).map(engine => (
<option key={engine.model_id} value={engine.model_id}>
{engine.display_name} {engine.unique_id ? `[${engine.unique_id.substring(0, 4)}]` : ''}
</option>
))}
</select>
</div>
{/* Create New Apprentice */}
<div className="mb-6">
<button onClick={handleCreateNewApprentice} className="btn btn-secondary">
Create New Empty Apprentice
</button>
</div>
</div>
{/* Register New Model Section (existing functionality) */}
<div style={{marginTop: '1.5rem', borderTop: '1px solid #eee', paddingTop: '1.5rem'}}>
<h4>Register New Model</h4>
{/* Existing new model registration form content */}
<div className="form-group">
<label>Provider</label>
<select value={newModelProvider} onChange={(e) => setNewModelProvider(e.target.value)}>
<option value="ollama">Ollama</option>
<option value="huggingface">HuggingFace (Transformers)</option>
<option value="mlx">MLX (Apple Silicon)</option>
<option value="gguf">GGUF (Local File)</option>
</select>
</div>
<div className="form-group">
<label>Model ID (Unique)</label>
<input type="text" value={newModelId} onChange={(e) => setNewModelId(e.target.value)} placeholder="e.g., 'my-custom-model'" />
</div>
<div className="form-group">
<label>Display Name</label>
<input type="text" value={newModelDisplayName} onChange={(e) => setNewModelDisplayName(e.target.value)} placeholder="e.g., 'My Custom Model'" />
</div>
<div className="form-group">
<label>Model Name (for Provider)</label>
<input type="text" value={newModelModelName} onChange={(e) => setNewModelModelName(e.target.value)} placeholder="e.g., 'gemma2:9b' or 'kofdai/my-hf-model'" />
</div>
<div className="form-group">
<label>Supported Domains (comma-separated)</label>
<input type="text" value={newModelSupportedDomains} onChange={(e) => setNewModelSupportedDomains(e.target.value)} placeholder="e.g., 'medical,general'" />
</div>
<div className="form-group">
<label>Description</label>
<textarea value={newModelDescription} onChange={(e) => setNewModelDescription(e.target.value)} placeholder="Optional description"></textarea>
</div>
{newModelProvider === 'gguf' && (
<div className="form-group">
<label>GGUF File (Select local .gguf file)</label>
<input type="file" accept=".gguf" onChange={(e) => setNewModelFile(e.target.files ? e.target.files[0] : null)} />
</div>
)}
<button onClick={handleSubmitNewModel} className="btn btn-secondary">
Register Model
</button>
</div>
</div>
);
};
export default EngineManager;