Spaces:
Running
Running
| // 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; | |