import { useEffect, useState } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Slider from '@mui/material/Slider'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Alert from '@mui/material/Alert'; import CircularProgress from '@mui/material/CircularProgress'; import { Save, Play } from 'lucide-react'; import type { AnomalyConfig, EnsembleStrategy } from '../../types/labeleval'; import { getAnomalyConfig, postAnomalyConfig, runPipeline } from '../../services/labeleval'; import { SectionCard } from './shared'; import AnomalyDetectionConfig from './AnomalyDetectionConfig'; const DEFAULT_CONFIG: AnomalyConfig = { imbalance: { method: 'smote', minority_boost: 2 }, detectors: { knn: { enabled: true, k: 5 }, lof: { enabled: true, n_neighbors: 20 }, isolation_forest: { enabled: true, n_estimators: 100 }, ocsvm: { enabled: false, nu: 0.05 }, dbscan: { enabled: false, eps: 0.5, min_samples: 5 }, }, deep: { autoencoder: { enabled: true, latent_dim: 16, epochs: 20 }, vae: { enabled: false, latent_dim: 8 }, gan: { enabled: false }, reconstruction_threshold: 0.5, }, advanced: { few_shot: { enabled: false, support_per_class: 5 }, ensemble_strategy: 'weighted_average', decision_threshold: 0.6, }, }; function Column({ title, children }: { title: string; children: React.ReactNode }) { return ( {title} {children} ); } export default function RareEventConfig({ activeDatasetId, onRunStarted, }: { activeDatasetId: string | null; onRunStarted?: (runId: string) => void; }) { const [config, setConfig] = useState(DEFAULT_CONFIG); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState<'save' | 'run' | null>(null); const [notice, setNotice] = useState<{ severity: 'success' | 'error' | 'warning'; text: string } | null>(null); useEffect(() => { let cancelled = false; getAnomalyConfig() .then((c) => { if (!cancelled) setConfig(c); }) .catch(() => { /* backend offline — keep defaults, save will still POST */ }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); const save = async () => { setBusy('save'); setNotice(null); try { const saved = await postAnomalyConfig(config); setConfig(saved); setNotice({ severity: 'success', text: 'Configuration saved.' }); } catch (err) { setNotice({ severity: 'error', text: err instanceof Error ? err.message : String(err) }); } finally { setBusy(null); } }; const rerun = async () => { if (!activeDatasetId) { setNotice({ severity: 'warning', text: 'Select or generate a dataset first (Datasets page).' }); return; } setBusy('run'); setNotice(null); try { const res = await runPipeline(activeDatasetId); setNotice({ severity: 'success', text: `Evaluation run ${res.run_id} started (${res.status}).` }); onRunStarted?.(res.run_id); } catch (err) { setNotice({ severity: 'error', text: err instanceof Error ? err.message : String(err) }); } finally { setBusy(null); } }; return ( } > {notice ? ( setNotice(null)} sx={{ mb: 1 }}> {notice.text} ) : null} {/* Column 1: imbalanced data */} Method Minority boost ×{config.imbalance.minority_boost.toFixed(1)} setConfig({ ...config, imbalance: { ...config.imbalance, minority_boost: v as number } }) } /> {/* Column 2: classical anomaly detectors */} setConfig({ ...config, detectors })} /> {/* Column 3: deep learning */} Autoencoder setConfig({ ...config, deep: { ...config.deep, autoencoder: { ...config.deep.autoencoder, enabled: e.target.checked } }, }) } /> {config.deep.autoencoder.enabled ? ( setConfig({ ...config, deep: { ...config.deep, autoencoder: { ...config.deep.autoencoder, latent_dim: Number(e.target.value) }, }, }) } sx={{ flex: 1 }} /> setConfig({ ...config, deep: { ...config.deep, autoencoder: { ...config.deep.autoencoder, epochs: Number(e.target.value) }, }, }) } sx={{ flex: 1 }} /> ) : null} VAE setConfig({ ...config, deep: { ...config.deep, vae: { ...config.deep.vae, enabled: e.target.checked } }, }) } /> {config.deep.vae.enabled ? ( setConfig({ ...config, deep: { ...config.deep, vae: { ...config.deep.vae, latent_dim: Number(e.target.value) } }, }) } sx={{ mb: 1, width: '100%' }} /> ) : null} GAN setConfig({ ...config, deep: { ...config.deep, gan: { enabled: e.target.checked } } }) } /> Reconstruction threshold {config.deep.reconstruction_threshold.toFixed(2)} setConfig({ ...config, deep: { ...config.deep, reconstruction_threshold: v as number } }) } /> {/* Column 4: advanced / ensemble */} Few-shot setConfig({ ...config, advanced: { ...config.advanced, few_shot: { ...config.advanced.few_shot, enabled: e.target.checked }, }, }) } /> {config.advanced.few_shot.enabled ? ( setConfig({ ...config, advanced: { ...config.advanced, few_shot: { ...config.advanced.few_shot, support_per_class: Number(e.target.value) }, }, }) } sx={{ mb: 1, width: '100%' }} /> ) : null} Ensemble strategy Decision threshold {config.advanced.decision_threshold.toFixed(2)} setConfig({ ...config, advanced: { ...config.advanced, decision_threshold: v as number } }) } /> ); }