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 (
: }
disabled={busy !== null || loading}
onClick={() => void save()}
>
Save
: }
disabled={busy !== null}
onClick={() => void rerun()}
>
Re-run evaluation
}
>
{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 } })
}
/>
);
}