Skip to main content
Glama
Models.tsx19.2 kB
import { useEffect, useState } from 'react'; import { Power, PowerOff, RefreshCw, Plus, Trash2, Edit2, Save, X, ArrowUp, ArrowDown } from 'lucide-react'; import api from '../lib/api'; import ModelFormModal, { type ModelFormData } from '../components/ModelFormModal'; interface Model { id: string; provider: string; apiModelName: string; layer?: string; relativeCost?: number; pricePer1kInputTokens?: number; pricePer1kOutputTokens?: number; contextWindow?: number; enabled: boolean; priority?: number; // Model priority within layer capabilities?: { code?: boolean; general?: boolean; reasoning?: boolean; vision?: boolean; }; } interface Layer { enabled: boolean; models: Model[]; providers: string[]; } interface LayersData { [key: string]: Layer; } export default function Models() { const [layers, setLayers] = useState<LayersData>({}); const [, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [editingLayer, setEditingLayer] = useState<string | null>(null); const [editingModel, setEditingModel] = useState<{ id: string; priority?: number; data: ModelFormData } | null>(null); const [saveStatus, setSaveStatus] = useState<string | null>(null); // Modal state for the reusable ModelFormModal const [isModalOpen, setIsModalOpen] = useState(false); const [modalMode, setModalMode] = useState<'create' | 'edit'>('edit'); const [addingToLayer, setAddingToLayer] = useState<string | null>(null); useEffect(() => { loadLayers(); }, []); async function loadLayers() { try { const response = await api.get(`/v1/models/layers`); if (response.data.layers) { setLayers(response.data.layers); } setError(null); setLoading(false); } catch (err) { console.error('Failed to load layers:', err); setError(err instanceof Error ? err.message : 'Failed to load layers'); setLoading(false); } } async function toggleLayer(layerName: string) { try { const newEnabled = !layers[layerName].enabled; await api.put(`/v1/layers/${layerName}/toggle`, { enabled: newEnabled }); // Update the state immediately for real-time UI feedback setLayers(prev => ({ ...prev, [layerName]: { ...prev[layerName], enabled: newEnabled, // If disabling layer, disable all models in that layer models: newEnabled ? prev[layerName].models : prev[layerName].models.map(m => ({...m, enabled: false})) } })); setSaveStatus(`Layer ${layerName} ${newEnabled ? 'enabled' : 'disabled'}`); setTimeout(() => setSaveStatus(null), 3000); // Reload data from server to ensure consistency setTimeout(() => loadLayers(), 500); } catch (err) { console.error('Failed to toggle layer:', err); setError(err instanceof Error ? err.message : 'Failed to toggle layer'); // Reload to revert any optimistic updates on error loadLayers(); } } // Open modal to add a new model to a specific layer function openAddModal(layerName: string) { setAddingToLayer(layerName); setEditingModel(null); setModalMode('create'); setIsModalOpen(true); } async function removeModel(layerName: string, modelId: string, modelName: string) { if (confirm(`Remove model "${modelName}" from ${layerName}?`)) { try { await api.delete(`/v1/models/${modelId}`); setSaveStatus(`Model ${modelName} removed from ${layerName}`); setTimeout(() => setSaveStatus(null), 3000); loadLayers(); // Reload to get updated data } catch (err) { console.error('Failed to remove model:', err); setError(err instanceof Error ? err.message : 'Failed to remove model'); } } } async function toggleModel(modelId: string, currentEnabled: boolean) { try { const newEnabled = !currentEnabled; // Optimistically update the state for immediate UI feedback setLayers(prev => { const newLayers = { ...prev }; for (const layerName in newLayers) { const layerData = newLayers[layerName]; layerData.models = layerData.models.map(model => model.id === modelId ? { ...model, enabled: newEnabled } : model ); } return newLayers; }); await api.put(`/v1/models/${modelId}`, { enabled: newEnabled }); setSaveStatus(`Model ${newEnabled ? 'enabled' : 'disabled'}`); setTimeout(() => setSaveStatus(null), 3000); // Reload data from server to ensure consistency setTimeout(() => loadLayers(), 500); } catch (err) { console.error('Failed to toggle model:', err); setError(err instanceof Error ? err.message : 'Failed to toggle model'); // Reload to revert any optimistic updates on error loadLayers(); } } function openEditModal(model: Model) { setAddingToLayer(null); setEditingModel({ id: model.id, priority: model.priority, data: { provider: model.provider, apiModelName: model.apiModelName, layer: model.layer || 'L0', relativeCost: model.relativeCost || 0, pricePer1kInputTokens: model.pricePer1kInputTokens || 0, pricePer1kOutputTokens: model.pricePer1kOutputTokens || 0, contextWindow: model.contextWindow || 8192, } }); setModalMode('edit'); setIsModalOpen(true); } async function handleModalSave(data: ModelFormData, modelId?: string) { if (modalMode === 'edit' && modelId) { // Update existing model await api.put(`/v1/models/${modelId}`, data); setSaveStatus(`Model ${modelId} updated`); } else { // Create new model - use addingToLayer if set, otherwise data.layer const targetLayer = addingToLayer || data.layer; const newModelId = `${data.provider}-${data.apiModelName.replace(/\//g, '-')}`; await api.post(`/v1/models`, { id: newModelId, ...data, layer: targetLayer, enabled: true, }); setSaveStatus(`Model added to ${targetLayer}`); } setTimeout(() => setSaveStatus(null), 3000); loadLayers(); } function closeModal() { setIsModalOpen(false); setEditingModel(null); setAddingToLayer(null); } // Model ordering functions - reorder all models in layer at once async function moveModel(layerName: string, modelId: string, direction: 'up' | 'down') { const layer = layers[layerName]; if (!layer) return; // Sort models by current priority first const models = [...layer.models].sort((a, b) => (a.priority || 0) - (b.priority || 0)); const currentIndex = models.findIndex(m => m.id === modelId); if (currentIndex === -1) return; const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (newIndex < 0 || newIndex >= models.length) return; // Swap models in the array [models[currentIndex], models[newIndex]] = [models[newIndex], models[currentIndex]]; // Update priorities based on new order models.forEach((model, index) => { model.priority = index; }); // Update local state immediately for responsive UI setLayers(prev => ({ ...prev, [layerName]: { ...prev[layerName], models: models } })); try { // Use the reorder API to update all priorities at once const modelIds = models.map(m => m.id); await api.put(`/v1/layers/${layerName}/reorder`, { modelIds }); setSaveStatus(`Model order updated in ${layerName}`); setTimeout(() => setSaveStatus(null), 2000); } catch (err) { console.error('Failed to update model order:', err); setError('Failed to update model order'); loadLayers(); // Reload to restore original order } } return ( <div className="space-y-6"> <div className="flex items-center justify-between"> <h1 className="text-3xl font-bold text-white">Model Layers</h1> <div className="flex items-center gap-3"> {saveStatus && ( <div className="flex items-center gap-2 text-green-400 text-sm"> <Save className="w-4 h-4" /> {saveStatus} </div> )} <button onClick={loadLayers} className="btn-secondary flex items-center gap-2"> <RefreshCw className="w-4 h-4" /> Refresh </button> </div> </div> {error && ( <div className="bg-red-500/10 border border-red-500/50 rounded-lg p-4"> <div className="flex items-center gap-3"> <div className="text-red-400 text-xl">⚠️</div> <div className="flex-1"> <h3 className="text-red-400 font-semibold">Error</h3> <p className="text-red-300 text-sm mt-1">{error}</p> </div> <button onClick={loadLayers} className="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-md transition-colors"> Retry </button> </div> </div> )} <div className="grid grid-cols-1 gap-6"> {Object.entries(layers).map(([layerName, layer]) => { const isEditing = editingLayer === layerName; return ( <div key={layerName} className="card p-6"> <div className="flex items-center justify-between mb-4"> <div className="flex items-center gap-3"> <h2 className="text-2xl font-bold text-white">{layerName}</h2> <span className={`badge ${layer.enabled ? 'badge-success' : 'badge-error'}`}> {layer.enabled ? ( <> <Power className="w-3 h-3 mr-1" /> Enabled </> ) : ( <> <PowerOff className="w-3 h-3 mr-1" /> Disabled </> )} </span> <span className="text-sm text-slate-400"> {layer.models.length} model(s) </span> </div> <div className="flex items-center gap-2"> <button onClick={() => toggleLayer(layerName)} className={`btn-secondary ${!layer.enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`} > {layer.enabled ? ( <> <PowerOff className="w-4 h-4 mr-2" /> Disable Layer </> ) : ( <> <Power className="w-4 h-4 mr-2" /> Enable Layer </> )} </button> <button onClick={() => setEditingLayer(isEditing ? null : layerName)} className="btn-secondary" > {isEditing ? ( <> <X className="w-4 h-4 mr-2" /> Close </> ) : ( <> <Edit2 className="w-4 h-4 mr-2" /> Edit Models </> )} </button> </div> </div> {/* Providers */} <div className="mb-4"> <h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide mb-2"> Providers </h3> <div className="flex flex-wrap gap-2"> {layer.providers.map((provider) => ( <span key={provider} className="badge badge-info"> {provider} </span> ))} </div> </div> {/* Add Model Button - opens full modal */} {isEditing && ( <div className="mb-4"> <button onClick={() => openAddModal(layerName)} className="btn-primary flex items-center gap-2" > <Plus className="w-4 h-4" /> Add New Model to {layerName} </button> </div> )} {/* Models */} {layer.models.length > 0 ? ( <div className="space-y-2"> <h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide"> Models ({layer.models.length}) </h3> <div className="grid grid-cols-1 gap-3"> {layer.models .sort((a, b) => (a.priority || 0) - (b.priority || 0)) .map((model, index) => ( <div key={model.id} className="flex items-center justify-between p-4 bg-slate-700/50 rounded-lg border border-slate-600 hover:border-slate-500 transition-colors group" > <div className="flex-1"> <div className="flex items-center gap-2 mb-1"> {isEditing && layer.models.length > 1 && ( <span className="inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-indigo-400 bg-indigo-500/20 rounded-full"> {index + 1} </span> )} <span className="text-white font-semibold">{model.apiModelName}</span> <span className={`badge ${model.enabled ? 'badge-success' : 'badge-error'} text-xs`}> {model.enabled ? 'Enabled' : 'Disabled'} </span> </div> <div className="flex items-center gap-2 text-xs text-slate-400"> <span className="badge badge-info">{model.provider}</span> <span>ID: {model.id}</span> {isEditing && layer.models.length > 1 && ( <span className="text-indigo-400">Priority: {model.priority || 0}</span> )} </div> </div> <div className="flex items-center gap-2"> {/* Model Ordering Controls */} {isEditing && layer.models.length > 1 && ( <> <button onClick={() => moveModel(layerName, model.id, 'up')} disabled={layer.models[0].id === model.id} className="btn-secondary text-sm bg-indigo-500/20 text-indigo-400 disabled:opacity-50 disabled:cursor-not-allowed" title="Move model up in priority" > <ArrowUp className="w-4 h-4" /> </button> <button onClick={() => moveModel(layerName, model.id, 'down')} disabled={layer.models[layer.models.length - 1].id === model.id} className="btn-secondary text-sm bg-indigo-500/20 text-indigo-400 disabled:opacity-50 disabled:cursor-not-allowed" title="Move model down in priority" > <ArrowDown className="w-4 h-4" /> </button> </> )} <button onClick={() => openEditModal(model)} className="btn-secondary text-sm bg-blue-500/20 text-blue-400" title="Edit model details" > <Edit2 className="w-4 h-4" /> </button> <button onClick={() => toggleModel(model.id, model.enabled)} className={`btn-secondary text-sm ${!model.enabled ? 'bg-green-500/20 text-green-400' : 'bg-orange-500/20 text-orange-400'}`} title={model.enabled ? 'Disable model' : 'Enable model'} > {model.enabled ? ( <PowerOff className="w-4 h-4" /> ) : ( <Power className="w-4 h-4" /> )} </button> {isEditing && ( <button onClick={() => removeModel(layerName, model.id, model.apiModelName)} className="p-2 text-red-400 hover:bg-red-500/20 rounded transition-opacity" title="Remove model" > <Trash2 className="w-4 h-4" /> </button> )} </div> </div> ))} </div> </div> ) : ( <div className="text-center py-8 text-slate-400"> No models configured for this layer {isEditing && ( <div className="mt-3"> <button onClick={() => openAddModal(layerName)} className="btn-primary flex items-center gap-2 mx-auto" > <Plus className="w-4 h-4" /> Add First Model to {layerName} </button> </div> )} </div> )} </div> ); })} </div> {Object.keys(layers).length === 0 && ( <div className="card p-12 text-center"> <div className="text-slate-400 text-lg">No layers configured</div> </div> )} {/* Model Modal - Using reusable ModelFormModal component */} <ModelFormModal isOpen={isModalOpen} mode={modalMode} initialData={modalMode === 'edit' ? editingModel?.data : (addingToLayer ? { layer: addingToLayer } : undefined)} modelId={editingModel?.id} currentPriority={editingModel?.priority} onSave={handleModalSave} onClose={closeModal} /> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server