Datavision / frontend /src /components /automl /ModelHistory.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
17.4 kB
/**
* 🗄️ Model History Component
*
* Displays all trained models for the current user with:
* - Version history
* - Metrics comparison
* - Rollback functionality
* - Delete options
*/
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
History,
Award,
Clock,
Target,
TrendingUp,
RotateCcw,
Trash2,
ChevronRight,
CheckCircle,
AlertCircle,
Loader,
Database,
Download,
RefreshCw,
} from 'lucide-react';
import { useConfirmModal } from '@/components/ui/ConfirmModal';
import { useToast } from '@/contexts/ToastContext';
import { getAuthHeadersSync } from '@/utils/userId';
interface ModelMetadata {
model_id: string;
user_id: string;
model_name: string;
task_type: string;
target_column: string;
feature_columns: string[];
metrics: Record<string, number>;
training_date: string;
dataset_info: {
n_features: number;
n_numeric: number;
n_categorical: number;
n_text: number;
is_nlp_task: boolean;
};
version: number;
is_active: boolean;
}
interface ModelHistoryProps {
userId?: string;
onModelChange?: () => void; // Callback when active model changes
}
const ModelHistory: React.FC<ModelHistoryProps> = ({ userId = 'default', onModelChange }) => {
const [models, setModels] = useState<ModelMetadata[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [rollingBack, setRollingBack] = useState<number | null>(null);
const [deleting, setDeleting] = useState<number | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Custom hooks for themed modals and toasts
const { confirm, ConfirmModal } = useConfirmModal();
const toast = useToast();
// Fetch models on mount
useEffect(() => {
fetchModels();
}, [userId]);
const fetchModels = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/v2/autonomous/models/${userId}`, {
headers: getAuthHeadersSync()
});
const data = await response.json();
if (data.success) {
setModels(data.models || []);
} else {
setError(data.message || 'Failed to load models');
}
} catch (err) {
setError('Failed to connect to API');
console.error('Fetch models error:', err);
} finally {
setLoading(false);
}
};
const handleRollback = async (version: number) => {
setRollingBack(version);
setSuccessMessage(null);
try {
const response = await fetch('/api/v2/autonomous/models/rollback', {
method: 'POST',
headers: getAuthHeadersSync(),
body: JSON.stringify({ user_id: userId, version })
});
const data = await response.json();
if (data.success) {
setSuccessMessage(`Rolled back to version ${version}`);
await fetchModels();
onModelChange?.(); // Notify parent to refresh overview
} else {
setError(data.detail || 'Rollback failed');
}
} catch (err) {
setError('Rollback failed');
} finally {
setRollingBack(null);
}
};
const handleDelete = async (version?: number) => {
const confirmed = await confirm({
title: version ? 'Delete Model Version' : 'Delete All Models',
message: version
? `Are you sure you want to delete version ${version}? This action cannot be undone.`
: 'Are you sure you want to delete all trained models? This will remove all model versions and cannot be undone.',
confirmText: 'Delete',
variant: 'danger',
icon: <Trash2 className="w-6 h-6" />
});
if (!confirmed) return;
setDeleting(version || -1);
try {
const url = version
? `/api/v2/autonomous/models/${userId}/version/${version}`
: `/api/v2/autonomous/models/${userId}`;
const response = await fetch(url, {
method: 'DELETE',
headers: getAuthHeadersSync()
});
const data = await response.json();
if (data.success) {
toast.deleted(version ? `Model version ${version} deleted` : 'All models deleted successfully');
await fetchModels();
} else {
toast.error(data.detail || 'Delete failed');
}
} catch (err) {
toast.error('Failed to delete model(s)');
} finally {
setDeleting(null);
}
};
const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateStr;
}
};
const getMetricColor = (value: number) => {
if (value >= 0.9) return '#10b981';
if (value >= 0.7) return '#f59e0b';
return '#ef4444';
};
if (loading) {
return (
<div className="flex flex-col items-center justify-center py-12">
<Loader className="w-8 h-8 animate-spin text-emerald-400 mb-4" />
<p style={{ color: 'var(--text-muted)' }}>Loading model history...</p>
</div>
);
}
if (error) {
return (
<div
className="p-6 rounded-2xl border bg-red-500/10 border-red-500/30 flex items-center gap-4"
>
<AlertCircle className="w-6 h-6 text-red-400" />
<div>
<p className="font-medium text-red-400">Error loading models</p>
<p style={{ color: 'var(--text-muted)' }}>{error}</p>
</div>
<button
onClick={fetchModels}
className="ml-auto px-4 py-2 bg-red-500/20 hover:bg-red-500/30 rounded-xl text-red-400 transition-colors"
>
Retry
</button>
</div>
);
}
if (models.length === 0) {
return (
<div
className="p-12 rounded-2xl border text-center"
style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}
>
<Database className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--text-muted)' }} />
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No Trained Models
</h3>
<p style={{ color: 'var(--text-muted)' }}>
Train a model in DataHub to see your model history here.
</p>
</div>
);
}
const activeModel = models.find(m => m.is_active);
const historyModels = models.filter(m => !m.is_active);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-xl bg-gradient-to-r from-emerald-500/20 to-green-500/20">
<History className="w-6 h-6 text-emerald-400" />
</div>
<div>
<h2 className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
Model History
</h2>
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>
{models.length} model version{models.length !== 1 ? 's' : ''} saved
</p>
</div>
</div>
<button
onClick={fetchModels}
className="p-2 rounded-xl hover:bg-white/10 transition-colors"
style={{ color: 'var(--text-muted)' }}
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
{/* Success Message */}
<AnimatePresence>
{successMessage && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="p-4 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center gap-3"
>
<CheckCircle className="w-5 h-5 text-emerald-400" />
<span className="text-emerald-400">{successMessage}</span>
</motion.div>
)}
</AnimatePresence>
{/* Active Model */}
{activeModel && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="p-6 rounded-2xl border-2 border-emerald-500/50 bg-gradient-to-br from-emerald-500/10 to-green-500/10"
>
<div className="flex items-center gap-3 mb-4">
<div className="px-3 py-1 bg-emerald-500 text-white text-xs font-bold rounded-full">
ACTIVE
</div>
<span className="text-sm" style={{ color: 'var(--text-muted)' }}>
Version {activeModel.version}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p className="text-sm mb-1" style={{ color: 'var(--text-muted)' }}>Model</p>
<p className="font-bold text-emerald-400 text-lg">{activeModel.model_name}</p>
</div>
<div>
<p className="text-sm mb-1" style={{ color: 'var(--text-muted)' }}>Task</p>
<p className="font-medium" style={{ color: 'var(--text-primary)' }}>{activeModel.task_type}</p>
</div>
<div>
<p className="text-sm mb-1" style={{ color: 'var(--text-muted)' }}>Target</p>
<p className="font-medium" style={{ color: 'var(--text-primary)' }}>{activeModel.target_column}</p>
</div>
<div>
<p className="text-sm mb-1" style={{ color: 'var(--text-muted)' }}>Trained</p>
<p className="font-medium" style={{ color: 'var(--text-primary)' }}>{formatDate(activeModel.training_date)}</p>
</div>
</div>
{/* Metrics */}
<div className="mt-4 pt-4 border-t border-emerald-500/20 flex flex-wrap gap-4">
{Object.entries(activeModel.metrics).slice(0, 4).map(([key, value]) => (
<div key={key} className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" style={{ color: getMetricColor(value) }} />
<span style={{ color: 'var(--text-muted)' }}>{key.toUpperCase()}:</span>
<span className="font-bold" style={{ color: getMetricColor(value) }}>
{key === 'mae' || key === 'rmse' ? value.toFixed(3) : `${(value * 100).toFixed(1)}%`}
</span>
</div>
))}
</div>
</motion.div>
)}
{/* Version History */}
{historyModels.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold flex items-center gap-2" style={{ color: 'var(--text-primary)' }}>
<Clock className="w-4 h-4" />
Previous Versions
</h3>
{historyModels.map((model, index) => (
<motion.div
key={model.model_id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="p-4 rounded-xl border flex items-center gap-4"
style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}
>
<div className="w-10 h-10 rounded-full bg-gray-500/20 flex items-center justify-center">
<span className="text-sm font-bold" style={{ color: 'var(--text-muted)' }}>
v{model.version}
</span>
</div>
<div className="flex-1">
<p className="font-medium" style={{ color: 'var(--text-primary)' }}>
{model.model_name}
</p>
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>
{formatDate(model.training_date)}
</p>
</div>
{/* Metrics preview */}
<div className="hidden md:flex items-center gap-4">
{Object.entries(model.metrics).slice(0, 2).map(([key, value]) => (
<span key={key} className="text-sm" style={{ color: 'var(--text-muted)' }}>
{key}: <span style={{ color: getMetricColor(value) }}>{(value * 100).toFixed(1)}%</span>
</span>
))}
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={() => handleRollback(model.version)}
disabled={rollingBack === model.version}
className="p-2 rounded-lg bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 transition-colors disabled:opacity-50"
title="Rollback to this version"
>
{rollingBack === model.version ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<RotateCcw className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleDelete(model.version)}
disabled={deleting === model.version}
className="p-2 rounded-lg bg-red-500/20 hover:bg-red-500/30 text-red-400 transition-colors disabled:opacity-50"
title="Delete this version"
>
{deleting === model.version ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</motion.div>
))}
</div>
)}
{/* Delete All */}
{models.length > 1 && (
<div className="pt-4 border-t" style={{ borderColor: 'var(--border-color)' }}>
<button
onClick={() => handleDelete()}
disabled={deleting === -1}
className="px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-xl transition-colors flex items-center gap-2 disabled:opacity-50"
>
{deleting === -1 ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
Delete All Models
</button>
</div>
)}
{/* Confirm Modal */}
<ConfirmModal />
</div>
);
};
export default ModelHistory;