Skip to content

feat live metrics and hyperparameter plots to models#432

Open
Felipedino wants to merge 12 commits intodevelopfrom
feat/models-live-metrics
Open

feat live metrics and hyperparameter plots to models#432
Felipedino wants to merge 12 commits intodevelopfrom
feat/models-live-metrics

Conversation

@Felipedino
Copy link
Collaborator

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]

- 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.
Copilot AI review requested due to automatic review settings January 27, 2026 02:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RunOperations accordion with a new RunResults tabbed panel on each RunCard, 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 a defaultMode prop, and removing logo/title elements from Footer and BarHeader.

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

  • PredictionCreationDialog currently defines the steps array twice (once at the module level and again inside the component) and still includes a "select mode" step in the inner steps even though ModeSelector has been removed from the imports. On top of that, renderStepContent now has two case 1 branches (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 single steps definition matching the new 2-step flow (configure input, confirm), remove the obsolete select-mode logic and ModeSelector usage, and ensure the switch cases align (e.g. case 0 for configuration, case 1 for 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.

Comment on lines +1 to +220
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) {
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@Felipedino
Copy link
Collaborator Author

aaaapq no pasa los precommit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant