feat live metrics and hyperparameter plots to models#432
feat live metrics and hyperparameter plots to models#432Felipedino wants to merge 12 commits intodevelopfrom
Conversation
- Implemented EditRunDialog to allow users to edit existing model run parameters, including model name, parameters, optimizer settings, and goal metric. - Integrated validation for model name uniqueness and required fields. - Added functionality to handle optimizer selection and parameter configuration. - Updated PredictionCard to persist expanded state in local storage. - Modified PredictionCreationDialog to remove mode selection and allow default mode configuration. - Enhanced RunCard with improved layout and action buttons for training and deleting runs. - Refactored RunOperations to include tabs for Explainability and Predictions, with separate sections for global and local explainers. - Updated ModelsContent to manage edit run functionality and confirmation dialog for run updates.
…anced model run visualization
There was a problem hiding this comment.
Pull request overview
This PR refactors the models “run” experience to consolidate run results (metrics, explainers, predictions, hyperparameters) into a tabbed view on each run card, adds live metrics and hyperparameter plots directly in the models page, and introduces editing capabilities for run parameters and optimizer configuration (partially wired). It also adjusts the models page training flow to account for existing operations and removes some branding elements from the three-section layout header/footer.
Changes:
- Replace the old
RunOperationsaccordion with a newRunResultstabbed panel on eachRunCard, including live metrics, explainers management, prediction creation (dataset/manual), and hyperparameter plots, plus persistence of expanded/selected tab state. - Extend the models page training flow to compute and pass per-run operation counts so retraining can warn about and clean up explainers/predictions, and add inline run-editing UI in
RunCard(with new dialogs for future edit-confirm flows). - Improve UX persistence and layout by storing explainer/prediction card expansion in
localStorage, simplifying the prediction creation wizard to be mode-driven via adefaultModeprop, and removing logo/title elements fromFooterandBarHeader.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
DashAI/front/src/pages/results/components/ResultsDetailsLayout.jsx |
Removes unused Parameters/Metrics tabs in favor of hyperparameter plots and info, matching the updated tabs configuration. |
DashAI/front/src/pages/models/ModelsContent.jsx |
Extends handleTrainRun to accept an optional operations count, integrates getRunOperationsCount/deleteRunOperations, wires retrain confirmation, and passes onRefreshRuns/updated onTrain into SessionVisualization. |
DashAI/front/src/components/threeSectionLayout/Footer.jsx |
Simplifies the footer to an empty flex container, removing the logo and divider while leaving the structural box in place. |
DashAI/front/src/components/threeSectionLayout/BarHeader.jsx |
Simplifies the header bar to an empty container, removing the “Dash” title text and associated styling. |
DashAI/front/src/components/models/SessionVisualization.jsx |
Refactors run cards to use the new RunResults component instead of RunOperations, and passes onRefreshRuns down so run operations can refresh the runs list. |
DashAI/front/src/components/models/RunResults.jsx |
New component that consolidates per-run live metrics, explainers, predictions, and hyperparameter plots into tabs, with persisted visibility/tab state and dialogs to create explainers and predictions. |
DashAI/front/src/components/models/RunOperations.jsx |
Legacy accordion-based operations view removed in favor of the new tabbed RunResults layout. |
DashAI/front/src/components/models/RunCard.jsx |
Adds inline edit mode for run parameters/optimizer, integrates operations count and retrain warnings into the train button, swaps in RunResults for per-run outputs, and introduces various UI/validation updates. |
DashAI/front/src/components/models/PredictionCreationDialog.jsx |
Refactors the prediction wizard to support a defaultMode prop and adjusts step logic, intending a 2-step (configure, confirm) flow instead of the prior 3-step mode-selection flow. |
DashAI/front/src/components/models/PredictionCard.jsx |
Persists each prediction card’s expanded/collapsed state to localStorage so expansion survives reloads. |
DashAI/front/src/components/models/LiveMetricsChart.jsx |
New live metrics chart for runs on the models page, mirroring the results page implementation with WebSocket updates and per-split/level metric selection. |
DashAI/front/src/components/models/HyperparameterPlots.jsx |
New hyperparameter plots component that fetches and renders historical, slice, contour, and importance plots based on how many parameters are optimizable for the run. |
DashAI/front/src/components/models/EditRunDialog.jsx |
New (currently unused) dialog that walks through editing run parameters and optimizer configuration via forms, preparing data for a confirmation flow. |
DashAI/front/src/components/models/EditConfirmationDialog.jsx |
New confirmation dialog component describing the consequences of updating run parameters (metrics reset, retraining, operations possibly invalid), intended to be used before applying edits. |
DashAI/front/src/components/explainers/ExplanainersCard.jsx |
Persists each explainer card’s expanded/collapsed state to localStorage and improves layout by combining explainer display name and instance name into a single, wrapped typography block. |
Comments suppressed due to low confidence (1)
DashAI/front/src/components/models/PredictionCreationDialog.jsx:265
PredictionCreationDialogcurrently defines thestepsarray twice (once at the module level and again inside the component) and still includes a "select mode" step in the innerstepseven thoughModeSelectorhas been removed from the imports. On top of that,renderStepContentnow has twocase 1branches (one for configuring input and one for confirmation), which is invalid and will either fail linting/compilation or make the confirmation step unreachable. Please consolidate to a singlestepsdefinition matching the new 2-step flow (configure input, confirm), remove the obsolete select-mode logic andModeSelectorusage, and ensure theswitchcases align (e.g.case 0for configuration,case 1for confirmation).
const steps = [
t("prediction:label.selectMode"),
t("prediction:label.configureInput"),
t("prediction:label.confirm"),
];
useEffect(() => {
if (!open) {
setActiveStep(0);
setPredictionMode(defaultMode);
setDatasets([]);
setSelectedDataset(null);
setManualRows([]);
setIsLoading(false);
}
}, [open, defaultMode]);
useEffect(() => {
const fetchData = async () => {
if (!run || !open) return;
setLoadingExperiment(true);
try {
const availableDatasets = await filterDatasets({ run_id: run.id });
const availableDatasetsWithInfo = await Promise.all(
availableDatasets.map(async (dataset) => {
const datasetInfo = await getDatasetInfo(dataset.id);
return { ...dataset, ...datasetInfo };
}),
);
setDatasets(availableDatasetsWithInfo);
if (run.model_session_id || session?.id) {
const sessionData = await getModelSessionById(
run.model_session_id || session.id,
);
setModelSession(sessionData);
const datasetTypes = await getDatasetTypes(sessionData.dataset_id);
setTypes(datasetTypes);
const datasetSample = await getDatasetSample(sessionData.dataset_id);
setSample(datasetSample);
}
} catch (error) {
console.error("Error fetching data:", error);
enqueueSnackbar(t("prediction:error.loadingPredictionData"), {
variant: "error",
});
} finally {
setLoadingExperiment(false);
}
};
fetchData();
}, [run, session, open, enqueueSnackbar]);
const canProceed = () => {
if (activeStep === 0) return true;
if (activeStep === 1) {
if (predictionMode === "dataset") {
return selectedDataset !== null;
}
return manualRows && manualRows.length > 0;
}
return true;
};
const handleNext = () => {
if (activeStep < steps.length - 1) {
setActiveStep((prev) => prev + 1);
} else {
handleSubmit();
}
};
const handleBack = () => {
setActiveStep((prev) => prev - 1);
};
const handleSubmit = async () => {
setIsLoading(true);
try {
const prediction = await createPrediction(
run.id,
predictionMode === "dataset" ? selectedDataset.id : null,
);
const jobResponse = await enqueuePredictionJob(
prediction.id,
predictionMode === "manual" ? manualRows : null,
);
if (!jobResponse || !jobResponse.id) {
throw new Error("Failed to enqueue prediction job");
}
enqueueSnackbar(t("prediction:message.predictionJobSubmitted"), {
variant: "success",
});
startJobPolling(
jobResponse.id,
async () => {
const updatedPredictions = await getPredictions(run.id);
const updatedPrediction = updatedPredictions.find(
(p) => p.id === prediction.id,
);
enqueueSnackbar(t("prediction:message.predictionCompleted"), {
variant: "success",
});
if (onPredictionCreated) {
onPredictionCreated(updatedPrediction || prediction);
}
},
(result) => {
// On failure
console.error("Prediction job failed:", result);
enqueueSnackbar(
t("prediction:error.predictionFailed", {
error: result.error || t("common:unknownError"),
}),
{ variant: "error" },
);
if (onPredictionCreated) {
onPredictionCreated();
}
},
);
if (onPredictionCreated) {
onPredictionCreated(prediction);
}
onClose();
} catch (error) {
console.error("Error creating prediction:", error);
enqueueSnackbar(t("prediction:error.creatingPrediction"), {
variant: "error",
});
} finally {
setIsLoading(false);
}
};
const renderStepContent = (step) => {
switch (step) {
case 0:
return (
<Box sx={{ py: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t("prediction:label.selectPredictionMode")}
</Typography>
<ModeSelector
predictionMode={predictionMode}
setPredictionMode={setPredictionMode}
/>
</Box>
);
case 1:
return (
<Box sx={{ py: 2 }}>
{predictionMode === "dataset" ? (
<>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 2 }}
>
{t("prediction:label.selectDataset")}
</Typography>
<DatasetSelector
experiment={modelSession}
datasets={datasets}
selectedDataset={selectedDataset}
setSelectedDataset={setSelectedDataset}
/>
</>
) : (
<>
<ManualInput
experiment={modelSession}
loading={loadingExperiment}
types={types}
sample={sample}
manualInputData={manualRows}
setManualInputData={setManualRows}
/>
</>
)}
</Box>
);
case 1:
return (
<Box sx={{ py: 2 }}>
<Typography variant="h6" gutterBottom>
{t("prediction:label.confirmPrediction")}
</Typography>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { | ||
| Box, | ||
| FormControl, | ||
| InputLabel, | ||
| MenuItem, | ||
| Select, | ||
| Tabs, | ||
| Tab, | ||
| Typography, | ||
| Button, | ||
| ButtonGroup, | ||
| } from "@mui/material"; | ||
| import { | ||
| LineChart, | ||
| Line, | ||
| XAxis, | ||
| YAxis, | ||
| Tooltip, | ||
| Legend, | ||
| ResponsiveContainer, | ||
| } from "recharts"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { getModelSessionById } from "../../api/modelSession"; | ||
|
|
||
| export function LiveMetricsChart({ run }) { | ||
| const [level, setLevel] = useState(null); | ||
| const [split, setSplit] = useState("TRAIN"); | ||
| const [data, setData] = useState({}); | ||
| const [selectedMetrics, setSelectedMetrics] = useState([]); | ||
| const [availableMetrics, setAvailableMetrics] = useState({ | ||
| TRAIN: [], | ||
| VALIDATION: [], | ||
| TEST: [], | ||
| }); | ||
|
|
||
| const selectedMetricsPerSplit = useRef({ | ||
| TRAIN: null, | ||
| VALIDATION: null, | ||
| TEST: null, | ||
| }); | ||
| const socketRef = useRef(null); | ||
|
|
||
| useEffect(() => { | ||
| if (run.status === 3 && run.test_metrics) { | ||
| setData((prev) => { | ||
| const next = structuredClone(prev); | ||
|
|
||
| const formattedTestMetrics = {}; | ||
| for (const metricName in run.test_metrics) { | ||
| const value = run.test_metrics[metricName]; | ||
| if (Array.isArray(value)) { | ||
| formattedTestMetrics[metricName] = value; | ||
| } else { | ||
| formattedTestMetrics[metricName] = [ | ||
| { step: 1, value: value, timestamp: new Date().toISOString() }, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| next.TEST = { | ||
| TRIAL: formattedTestMetrics, | ||
| STEP: formattedTestMetrics, | ||
| EPOCH: formattedTestMetrics, | ||
| }; | ||
| return next; | ||
| }); | ||
| } | ||
| }, [run.status, run.test_metrics]); | ||
|
|
||
| useEffect(() => { | ||
| if (socketRef.current) { | ||
| socketRef.current.close(); | ||
| } | ||
|
|
||
| const ws = new WebSocket(`ws://localhost:8000/api/v1/metrics/ws/${run.id}`); | ||
|
|
||
| ws.onmessage = (event) => { | ||
| const incoming = JSON.parse(event.data); | ||
|
|
||
| setData((prev) => { | ||
| const next = structuredClone(prev); | ||
|
|
||
| for (const splitKey in incoming) { | ||
| if (splitKey === "run_status") continue; | ||
| next[splitKey] ??= {}; | ||
|
|
||
| for (const levelKey in incoming[splitKey]) { | ||
| next[splitKey][levelKey] ??= {}; | ||
|
|
||
| for (const metricName in incoming[splitKey][levelKey]) { | ||
| const incomingPoints = incoming[splitKey][levelKey][metricName]; | ||
|
|
||
| if (!Array.isArray(next[splitKey][levelKey][metricName])) { | ||
| next[splitKey][levelKey][metricName] = [...incomingPoints]; | ||
| } else { | ||
| next[splitKey][levelKey][metricName].push(...incomingPoints); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return next; | ||
| }); | ||
| }; | ||
|
|
||
| ws.onclose = () => { | ||
| if (run.test_metrics) { | ||
| setData((prev) => { | ||
| const next = structuredClone(prev); | ||
|
|
||
| const formattedTestMetrics = {}; | ||
| for (const metricName in run.test_metrics) { | ||
| const value = run.test_metrics[metricName]; | ||
| if (Array.isArray(value)) { | ||
| formattedTestMetrics[metricName] = value; | ||
| } else { | ||
| formattedTestMetrics[metricName] = [ | ||
| { step: 1, value: value, timestamp: new Date().toISOString() }, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| next.TEST = { | ||
| TRIAL: formattedTestMetrics, | ||
| STEP: formattedTestMetrics, | ||
| EPOCH: formattedTestMetrics, | ||
| }; | ||
| return next; | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| ws.onerror = (error) => { | ||
| console.error("WebSocket error:", error); | ||
| }; | ||
|
|
||
| socketRef.current = ws; | ||
|
|
||
| return () => { | ||
| try { | ||
| ws.close(); | ||
| } catch (e) { | ||
| console.log("WebSocket already closed"); | ||
| } | ||
| }; | ||
| }, [run.id, run.test_metrics]); | ||
|
|
||
| useEffect(() => { | ||
| let mounted = true; | ||
|
|
||
| getModelSessionById(run.model_session_id.toString()).then((session) => { | ||
| if (!mounted) return; | ||
|
|
||
| setAvailableMetrics({ | ||
| TRAIN: session.train_metrics ?? [], | ||
| VALIDATION: session.validation_metrics ?? [], | ||
| TEST: session.test_metrics ?? [], | ||
| }); | ||
| }); | ||
|
|
||
| return () => { | ||
| mounted = false; | ||
| }; | ||
| }, [run.experiment_id]); | ||
|
|
||
| const metrics = data[split]?.[level] ?? {}; | ||
| const allowed = availableMetrics[split] ?? []; | ||
|
|
||
| const filteredMetrics = Object.fromEntries( | ||
| Object.entries(metrics).filter(([name]) => allowed.includes(name)), | ||
| ); | ||
|
|
||
| const chartData = (() => { | ||
| if (Object.keys(filteredMetrics).length === 0) return []; | ||
|
|
||
| const allSteps = new Set(); | ||
| for (const metricName in filteredMetrics) { | ||
| const metricData = filteredMetrics[metricName]; | ||
|
|
||
| if (Array.isArray(metricData)) { | ||
| metricData.forEach((point) => { | ||
| allSteps.add(point.step); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const sortedSteps = Array.from(allSteps).sort((a, b) => a - b); | ||
|
|
||
| return sortedSteps.map((step) => { | ||
| const point = { x: step }; | ||
|
|
||
| for (const metricName in filteredMetrics) { | ||
| const metricData = filteredMetrics[metricName]; | ||
|
|
||
| if (Array.isArray(metricData)) { | ||
| const dataPoint = metricData.find((p) => p.step === step); | ||
| point[metricName] = dataPoint?.value ?? null; | ||
| } else { | ||
| point[metricName] = null; | ||
| } | ||
| } | ||
|
|
||
| return point; | ||
| }); | ||
| })(); | ||
|
|
||
| const hasTrialData = | ||
| data[split]?.TRIAL && Object.keys(data[split].TRIAL).length > 0; | ||
| const hasStepData = | ||
| data[split]?.STEP && Object.keys(data[split].STEP).length > 0; | ||
| const hasEpochData = | ||
| data[split]?.EPOCH && Object.keys(data[split].EPOCH).length > 0; | ||
|
|
||
| useEffect(() => { | ||
| const currentLevelHasData = | ||
| (level === "TRIAL" && hasTrialData) || | ||
| (level === "STEP" && hasStepData) || | ||
| (level === "EPOCH" && hasEpochData); | ||
|
|
||
| if (currentLevelHasData) { |
There was a problem hiding this comment.
The newly added LiveMetricsChart under components/models duplicates almost all of the logic from pages/results/components/LiveMetricsChart but drops translation support and introduces a second source of truth for the live-metrics behavior. This duplication will make future fixes and enhancements error-prone; consider reusing the existing results LiveMetricsChart (possibly by moving it to a shared location) or extracting the common logic into a shared helper so both views stay in sync.
DashAI/front/src/components/models/PredictionCreationDialog.jsx
Outdated
Show resolved
Hide resolved
…en the optimizer is used
…ed imports in components
|
aaaapq no pasa los precommit |
This pull request introduces several improvements and new features to the model management and prediction flow in the frontend. The most significant changes are the addition of dialogs for editing model runs and confirming parameter updates, enhancements to user experience by persisting UI state, and refactoring the prediction creation wizard for clarity and flexibility.
Dialogs and Model Run Editing:
Added a new EditRunDialog component that provides a step-by-step wizard for editing existing model run parameters, including model configuration and optimizer setup. It validates input, prevents duplicate names, and supports dynamic optimizer configuration.
Added an EditConfirmationDialog component to warn users about the consequences of updating model run parameters, including data deletion and retraining, with a clear summary of affected data.
UI State Persistence:
Updated both ExplainersCard and PredictionCard components to persist their expanded/collapsed state in localStorage, ensuring consistent UI experience across page reloads. [1] [2]
Prediction Creation Flow Refactor:
Refactored the PredictionCreationDialog wizard to remove the mode selection step and allow the default input mode to be set via a prop, simplifying the user flow and making it more flexible for different use cases. [1] [2] [3] [4] [5] [6] [7]
UI/UX Improvements:
Improved layout and typography in ExplainersCard for better readability and alignment, especially when displaying explainer names and captions.
Miscellaneous:
Added import statements for new UI components and utilities in relevant files to support the new features. [1] [2] [3]