AI Agent
Deploy to Spaces
a0098d0
import { useState, useEffect, useRef } from 'react';
import {
Upload,
Cpu,
HardDrive,
Database,
CheckCircle,
AlertCircle,
Loader2,
Package,
Trash2,
Sparkles,
Clock,
Download
} from 'lucide-react';
import { useSystemStore } from '../store';
import { motion, AnimatePresence } from 'framer-motion';
/**
* ModelLoader page - load HuggingFace models with progress tracking
*/
export default function ModelLoader() {
const systemInfo = useSystemStore((state) => state.systemInfo);
const [modelName, setModelName] = useState('');
const [exampleModels, setExampleModels] = useState(null);
const [loadResult, setLoadResult] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [progress, setProgress] = useState(null);
const [cachedModels, setCachedModels] = useState([]);
const [modelInfo, setModelInfo] = useState(null);
const progressPollRef = useRef(null);
// Fetch example models and cache info on mount
useEffect(() => {
// Optimistic load from cache
const cachedExamples = localStorage.getItem('example_models');
if (cachedExamples) {
try {
setExampleModels(JSON.parse(cachedExamples));
} catch (e) { }
}
fetch('/api/models/examples')
.then(res => res.json())
.then(data => {
setExampleModels(data);
localStorage.setItem('example_models', JSON.stringify(data));
})
.catch(() => { });
fetchCacheInfo();
fetchModelInfo();
}, []);
const fetchCacheInfo = async () => {
try {
const res = await fetch('/api/models/cache');
const data = await res.json();
setCachedModels(data.models || []);
} catch (e) { }
};
const fetchModelInfo = async () => {
try {
const res = await fetch('/api/models/info');
const data = await res.json();
if (data.loaded) {
setModelInfo(data);
}
} catch (e) { }
};
const pollProgress = (name) => {
if (progressPollRef.current) {
clearInterval(progressPollRef.current);
}
progressPollRef.current = setInterval(async () => {
try {
const res = await fetch(`/api/models/progress/${encodeURIComponent(name)}`);
const data = await res.json();
if (data.downloading) {
setProgress(data);
}
} catch (e) { }
}, 500);
};
const stopPolling = () => {
if (progressPollRef.current) {
clearInterval(progressPollRef.current);
progressPollRef.current = null;
}
};
const handleLoadModel = async () => {
if (!modelName.trim() || isLoading) return;
setIsLoading(true);
setLoadResult(null);
setProgress({ status: 'starting', percent: 0, message: 'Starting download...' });
// Start polling for progress
pollProgress(modelName.trim());
try {
const response = await fetch('/api/models/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_name: modelName.trim(),
dtype: 'auto',
device: 'auto',
trust_remote_code: true
})
});
const data = await response.json();
setLoadResult(data);
if (data.success) {
setModelInfo(data.model_info);
setProgress({ status: 'complete', percent: 100, message: 'Model loaded!' });
fetchCacheInfo();
} else {
setProgress(null);
}
} catch (err) {
setLoadResult({ success: false, error: err.message });
setProgress(null);
} finally {
setIsLoading(false);
stopPolling();
}
};
const handleQuickLoad = (modelId) => {
setModelName(modelId);
};
const handleUnload = async () => {
try {
await fetch('/api/models/unload', { method: 'POST' });
setModelInfo(null);
setLoadResult(null);
setProgress(null);
} catch (e) { }
};
const handleDeleteFromCache = async (name) => {
try {
await fetch(`/api/models/cache/${encodeURIComponent(name)}`, { method: 'DELETE' });
fetchCacheInfo();
} catch (e) { }
};
const handleCleanup = async () => {
try {
const res = await fetch('/api/models/cache/cleanup', { method: 'POST' });
const data = await res.json();
fetchCacheInfo();
alert(`Cleaned up ${data.deleted_count} models`);
} catch (e) { }
};
return (
<div className="model-loader">
{/* Header */}
<div className="page-header">
<h1 className="page-title">Load HuggingFace Model</h1>
<p className="page-subtitle">
Download and analyze models directly from HuggingFace Hub
</p>
</div>
{/* Main Content */}
<div className="loader-grid">
{/* Load Model Card */}
<motion.div
className="glass-card load-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="card-header">
<Package size={24} />
<h2>Load Model</h2>
</div>
<div className="input-section">
<label className="input-label">Model ID</label>
<input
type="text"
className="input"
placeholder="e.g. gpt2, bert-base-uncased, prajjwal1/bert-tiny"
value={modelName}
onChange={(e) => setModelName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLoadModel()}
disabled={isLoading}
/>
<p className="input-hint">
Enter the HuggingFace model identifier (organization/model-name)
</p>
</div>
<button
className="btn btn-primary btn-lg w-full"
onClick={handleLoadModel}
disabled={isLoading || !modelName.trim()}
>
{isLoading ? (
<>
<Loader2 size={20} className="spinning" />
Loading...
</>
) : (
<>
<Download size={20} />
Download & Load Model
</>
)}
</button>
{/* Progress Bar */}
<AnimatePresence>
{progress && (
<motion.div
className="progress-container"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
>
<div className="progress-header">
<span className="progress-status">{progress.message || progress.status}</span>
<span className="progress-percent">{progress.percent || 0}%</span>
</div>
<div className="progress-bar">
<motion.div
className="progress-fill"
initial={{ width: 0 }}
animate={{ width: `${progress.percent || 0}%` }}
transition={{ duration: 0.3 }}
/>
</div>
{progress.speed_mbps && (
<div className="progress-details">
<span>{progress.speed_mbps} MB/s</span>
{progress.eta_seconds && <span>ETA: {progress.eta_seconds}s</span>}
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Result Message */}
<AnimatePresence>
{loadResult && !isLoading && (
<motion.div
className={`result-message ${loadResult.success ? 'success' : 'error'}`}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
>
{loadResult.success ? (
<>
<CheckCircle size={20} />
<div>
<strong>Model loaded successfully!</strong>
<p>{loadResult.model_info?.architecture} - {loadResult.model_info?.num_params_millions}M params</p>
</div>
</>
) : (
<>
<AlertCircle size={20} />
<div>
<strong>Failed to load model</strong>
<p>{loadResult.error}</p>
{loadResult.suggestion && <p className="suggestion">{loadResult.suggestion}</p>}
</div>
</>
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* Currently Loaded Model */}
{modelInfo && (
<motion.div
className="glass-card loaded-model-card"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
<div className="card-header">
<CheckCircle size={24} className="text-success" />
<h2>Loaded Model</h2>
<button className="btn btn-ghost btn-sm ml-auto" onClick={handleUnload}>
<Trash2 size={16} />
Unload
</button>
</div>
<div className="model-details">
<div className="detail-item">
<span className="label">Name</span>
<span className="value">{modelInfo.name}</span>
</div>
<div className="detail-item">
<span className="label">Parameters</span>
<span className="value">{modelInfo.num_params_millions}M</span>
</div>
<div className="detail-item">
<span className="label">Memory</span>
<span className="value">{modelInfo.memory_mb?.toFixed(1)} MB</span>
</div>
<div className="detail-item">
<span className="label">Device</span>
<span className="value">{modelInfo.device}</span>
</div>
<div className="detail-item">
<span className="label">Quantizable Layers</span>
<span className="value highlight">{modelInfo.num_quantizable_layers}</span>
</div>
</div>
</motion.div>
)}
{/* Quick Start */}
<motion.div
className="glass-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="card-header">
<Sparkles size={24} />
<h2>Quick Start</h2>
</div>
<p className="text-sm text-muted mb-md">Click to select a model:</p>
{exampleModels ? (
<>
{exampleModels.sample_models?.length > 0 && (
<div className="model-group">
<h4 className="group-title">⭐ Sample Models (Pre-cached)</h4>
<div className="model-list">
{exampleModels.sample_models.map((model) => (
<button
key={model.id}
className={`model-chip sample ${modelName === model.id ? 'selected' : ''}`}
onClick={() => handleQuickLoad(model.id)}
>
<span className="model-id">{model.id}</span>
<span className="model-desc">Instant load</span>
</button>
))}
</div>
</div>
)}
<div className="model-group">
<h4 className="group-title">Small Models</h4>
<div className="model-list">
{exampleModels.small_models?.map((model) => (
<button
key={model.id}
className={`model-chip ${modelName === model.id ? 'selected' : ''}`}
onClick={() => handleQuickLoad(model.id)}
>
<span className="model-id">{model.id}</span>
<span className="model-size">{model.size}</span>
</button>
))}
</div>
</div>
</>
) : (
<div className="loading-placeholder">
<Loader2 size={20} className="spinning" />
<span>Loading examples...</span>
</div>
)}
</motion.div>
{/* System Status */}
<motion.div
className="glass-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="card-header">
<Cpu size={24} />
<h2>System</h2>
</div>
{systemInfo ? (
<div className="status-list">
<div className="status-item">
<span className="status-label">Device</span>
<span className="status-value">
{systemInfo.cuda_available ? '🟢 CUDA GPU' :
systemInfo.mps_available ? '🟢 Apple MPS' : '🟡 CPU'}
</span>
</div>
{systemInfo.gpus?.length > 0 && (
<div className="status-item">
<span className="status-label">GPU</span>
<span className="status-value">{systemInfo.gpus[0].name}</span>
</div>
)}
<div className="status-item">
<span className="status-label">RAM</span>
<span className="status-value">{systemInfo.ram_available_gb?.toFixed(1)} GB</span>
</div>
</div>
) : (
<p className="text-muted">Loading...</p>
)}
</motion.div>
{/* Cached Models */}
<motion.div
className="glass-card cache-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="card-header">
<Database size={24} />
<h2>Model Cache</h2>
<button className="btn btn-ghost btn-sm ml-auto" onClick={handleCleanup}>
<Clock size={16} />
Cleanup
</button>
</div>
<p className="text-xs text-muted mb-sm">
Models auto-delete after 4 hours (except samples)
</p>
{cachedModels.length > 0 ? (
<div className="cache-list">
{cachedModels.map((model) => (
<div key={model.name} className={`cache-item ${model.is_sample ? 'sample' : ''}`}>
<div className="cache-info">
<span className="cache-name">
{model.is_sample && '⭐ '}
{model.name}
</span>
<span className="cache-size">{model.size_mb} MB</span>
</div>
{!model.is_sample && (
<button
className="btn btn-ghost btn-xs"
onClick={() => handleDeleteFromCache(model.name)}
>
<Trash2 size={14} />
</button>
)}
</div>
))}
</div>
) : (
<p className="text-muted text-sm">No models cached</p>
)}
</motion.div>
</div>
<style>{`
.loader-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-lg);
}
@media (max-width: 1024px) {
.loader-grid {
grid-template-columns: 1fr;
}
}
.load-card {
grid-column: span 2;
}
@media (max-width: 1024px) {
.load-card {
grid-column: span 1;
}
}
.loaded-model-card {
grid-column: span 2;
background: rgba(16, 185, 129, 0.05);
border-color: rgba(16, 185, 129, 0.3);
}
.cache-card {
grid-column: span 2;
}
.card-header {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
color: var(--text-primary);
}
.card-header h2 {
font-size: var(--text-lg);
font-weight: 600;
margin: 0;
}
.input-section {
margin-bottom: var(--space-lg);
}
.input-hint {
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-top: var(--space-xs);
}
/* Progress Bar */
.progress-container {
margin-top: var(--space-lg);
padding: var(--space-md);
background: var(--glass-bg);
border-radius: var(--radius-md);
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-sm);
font-size: var(--text-sm);
}
.progress-status {
color: var(--text-secondary);
}
.progress-percent {
color: var(--color-accent-primary);
font-weight: 600;
}
.progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-accent-primary), var(--color-accent-secondary));
border-radius: 4px;
}
.progress-details {
display: flex;
justify-content: space-between;
margin-top: var(--space-xs);
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.result-message {
display: flex;
align-items: flex-start;
gap: var(--space-md);
padding: var(--space-md);
border-radius: var(--radius-md);
margin-top: var(--space-md);
}
.result-message.success {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
color: var(--color-success);
}
.result-message.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--color-error);
}
.result-message strong {
display: block;
}
.result-message p {
margin: var(--space-xs) 0 0 0;
font-size: var(--text-sm);
opacity: 0.9;
}
.model-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--space-sm);
}
.detail-item {
display: flex;
flex-direction: column;
padding: var(--space-sm);
background: var(--glass-bg);
border-radius: var(--radius-md);
}
.detail-item .label {
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.detail-item .value {
font-size: var(--text-base);
font-weight: 500;
color: var(--text-primary);
}
.detail-item .value.highlight {
color: var(--color-accent-primary);
}
.model-group {
margin-bottom: var(--space-lg);
}
.group-title {
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: var(--space-sm);
}
.model-list {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.model-chip {
display: flex;
flex-direction: column;
padding: var(--space-sm) var(--space-md);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
text-align: left;
}
.model-chip:hover {
border-color: var(--glass-border-hover);
transform: translateY(-1px);
}
.model-chip.selected {
border-color: var(--color-accent-primary);
background: rgba(99, 102, 241, 0.1);
}
.model-chip.sample {
border-color: rgba(16, 185, 129, 0.4);
background: rgba(16, 185, 129, 0.1);
}
.model-id {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary);
}
.model-size, .model-desc {
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.status-list {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.status-item {
display: flex;
justify-content: space-between;
padding: var(--space-xs) 0;
border-bottom: 1px solid var(--glass-border);
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.status-value {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary);
}
.cache-list {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.cache-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-sm);
background: var(--glass-bg);
border-radius: var(--radius-md);
}
.cache-item.sample {
background: rgba(16, 185, 129, 0.05);
}
.cache-info {
display: flex;
flex-direction: column;
}
.cache-name {
font-size: var(--text-sm);
color: var(--text-primary);
}
.cache-size {
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.ml-auto {
margin-left: auto;
}
.text-success {
color: var(--color-success);
}
.spinning {
animation: spin 1s linear infinite;
}
.loading-placeholder {
display: flex;
align-items: center;
gap: var(--space-sm);
color: var(--text-tertiary);
}
`}</style>
</div>
);
}