diff --git a/DashAI/front/src/App.jsx b/DashAI/front/src/App.jsx
index 7f3ec8355..dc6f5911b 100644
--- a/DashAI/front/src/App.jsx
+++ b/DashAI/front/src/App.jsx
@@ -17,6 +17,7 @@ import NewPipeline from "./pages/pipelines/NewPipeline";
import PluginsDetails from "./pages/plugins/components/PluginsDetails";
import Generative from "./pages/generative/Generative";
import NewPipelineWrapper from "./pages/pipelines/newPipelineWrapper";
+import JobQueueWidget from "./components/jobs/JobQueueWidget";
function App() {
return (
@@ -53,6 +54,7 @@ function App() {
+
);
}
diff --git a/DashAI/front/src/api/run.ts b/DashAI/front/src/api/run.ts
index 72540f4ec..327860a2c 100644
--- a/DashAI/front/src/api/run.ts
+++ b/DashAI/front/src/api/run.ts
@@ -60,12 +60,14 @@ export const deleteRun = async (runId: string): Promise => {
export const updateRunParameters = async (
runId: string,
+ name?: string,
parameters?: object,
optimizer?: string,
optimizer_parameters?: object,
goal_metric?: string,
): Promise => {
const response = await api.patch(`/v1/run/${runId}`, {
+ run_name: name,
parameters,
optimizer,
optimizer_parameters,
diff --git a/DashAI/front/src/components/DatasetVisualization.jsx b/DashAI/front/src/components/DatasetVisualization.jsx
index be7976392..1bd026dbf 100644
--- a/DashAI/front/src/components/DatasetVisualization.jsx
+++ b/DashAI/front/src/components/DatasetVisualization.jsx
@@ -18,7 +18,6 @@ import {
getDatasetFileFiltered,
} from "../api/datasets";
import { useTourContext } from "./tour/TourProvider";
-import JobQueueWidget from "./jobs/JobQueueWidget";
import { formatDate } from "../pages/results/constants/formatDate";
import Header from "./notebooks/dataset/header/Header";
import Tooltip from "@mui/material/Tooltip";
@@ -415,8 +414,6 @@ export default function DatasetVisualization({
)}
-
-
>
);
}
diff --git a/DashAI/front/src/components/custom/CustomLayout.jsx b/DashAI/front/src/components/custom/CustomLayout.jsx
index c92388717..ff8d160cd 100644
--- a/DashAI/front/src/components/custom/CustomLayout.jsx
+++ b/DashAI/front/src/components/custom/CustomLayout.jsx
@@ -2,7 +2,6 @@ import React from "react";
import PropTypes from "prop-types";
import Container from "@mui/material/Container";
import { useMediaQuery, Typography, Box } from "@mui/material";
-import JobQueueWidget from "../jobs/JobQueueWidget";
import { useTheme } from "@mui/material/styles";
/**
@@ -21,26 +20,8 @@ function CustomLayout({
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up(xxl));
- const jobQueueWidgetElement = (
-
-
-
- );
-
if (disableContainer) {
- return (
-
- {children}
- {jobQueueWidgetElement}
-
- );
+ return {children};
}
return (
@@ -58,7 +39,6 @@ function CustomLayout({
)}
{children}
- {jobQueueWidgetElement}
);
}
diff --git a/DashAI/front/src/components/experiments/runButtons/EditRunDialog.jsx b/DashAI/front/src/components/experiments/runButtons/EditRunDialog.jsx
index 88f14d313..1e4cc092a 100644
--- a/DashAI/front/src/components/experiments/runButtons/EditRunDialog.jsx
+++ b/DashAI/front/src/components/experiments/runButtons/EditRunDialog.jsx
@@ -1,14 +1,12 @@
import React, { useState } from "react";
import { GridActionsCellItem } from "@mui/x-data-grid";
import { Edit } from "@mui/icons-material";
-import { updateRunParameters } from "../../../api/run";
-import { Box } from "@mui/system";
import RunInfoModal from "./RunInfoModal";
import { useTranslation } from "react-i18next";
export default function EditRunDialog({ experiment, run, setRun }) {
- const isRunning = run.status === 1 || run.status === 2; // Delivered or Started
+ const isRunning = run.status === 1 || run.status === 2;
if (isRunning) {
return null;
}
diff --git a/DashAI/front/src/components/explainers/ExplainersPlot.jsx b/DashAI/front/src/components/explainers/ExplainersPlot.jsx
index 557d04e9d..9eaf9084d 100644
--- a/DashAI/front/src/components/explainers/ExplainersPlot.jsx
+++ b/DashAI/front/src/components/explainers/ExplainersPlot.jsx
@@ -2,7 +2,6 @@ import { React, useEffect, useState } from "react";
import {
FormControl,
InputLabel,
- Grid,
MenuItem,
Select,
CircularProgress,
@@ -59,47 +58,49 @@ export default function ExplainersPlot({ explainer, scope }) {
}, [explainer.status]);
return (
-
-
- {!loading && isLocal && (
-
- Select an instance
-
-
- )}
-
-
- {!loading && explainer.status === 3 ? (
-
- ) : explainer.status === 4 ? (
- {t("explainers:error.explainerFailed")}
- ) : (
-
-
-
- )}
-
-
+
+ {!loading && isLocal && (
+
+ Select an instance
+
+
+ )}
+ {!loading && explainer.status === 3 ? (
+
+ ) : explainer.status === 4 ? (
+ {t("explainers:error.explainerFailed")}
+ ) : (
+
+
+
+ )}
+
);
}
diff --git a/DashAI/front/src/components/explainers/ExplanainersCard.jsx b/DashAI/front/src/components/explainers/ExplanainersCard.jsx
index 888bc243b..3a4d4bc98 100644
--- a/DashAI/front/src/components/explainers/ExplanainersCard.jsx
+++ b/DashAI/front/src/components/explainers/ExplanainersCard.jsx
@@ -36,7 +36,17 @@ export default function ExplainersCard({
compact = false,
}) {
const [open, setOpen] = useState(false);
- const [expanded, setExpanded] = useState(true);
+ const [expanded, setExpanded] = useState(() => {
+ const saved = localStorage.getItem(`explainer-${explainer.id}-expanded`);
+ return saved !== null ? JSON.parse(saved) : true;
+ });
+
+ useEffect(() => {
+ localStorage.setItem(
+ `explainer-${explainer.id}-expanded`,
+ JSON.stringify(expanded),
+ );
+ }, [expanded, explainer.id]);
const [componentData, setComponentData] = useState(null);
const { t } = useTranslation(["explainers"]);
@@ -77,7 +87,7 @@ export default function ExplainersCard({
if (compact) {
return (
<>
-
+
-
-
+
+
{componentData
? componentData.display_name
: plotName(explainer.explainer_name)}
-
-
- {explainer.name}
+
+ {explainer.name}
+
@@ -158,7 +182,7 @@ export default function ExplainersCard({
// Full mode for standalone page
return (
-
+
setSessionInfoVisible(false)}
/>
)}
-
-
);
}
diff --git a/DashAI/front/src/components/jobs/JobQueueWidget.jsx b/DashAI/front/src/components/jobs/JobQueueWidget.jsx
index 918fdbba1..888b11225 100644
--- a/DashAI/front/src/components/jobs/JobQueueWidget.jsx
+++ b/DashAI/front/src/components/jobs/JobQueueWidget.jsx
@@ -80,7 +80,6 @@ const JobQueueWidget = () => {
const [clearingAll, setClearingAll] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0);
const [isHovered, setIsHovered] = useState(false);
- const prevActiveJobsCount = useRef(0);
const handleClearAllJobs = () => {
setConfirmClearAll(true);
@@ -139,18 +138,38 @@ const JobQueueWidget = () => {
const finishedJobs = jobs.filter((job) => job.status === "finished");
const errorJobs = jobs.filter((job) => job.status === "error");
+ const hasInitializedRef = useRef(false);
+ const prevActiveCountRef = useRef(0);
+
+ // Snapshot del estado inicial al completar la primera carga (evita tratar
+ // jobs ya existentes como nuevos y disparar el expand al montar)
useEffect(() => {
- if (
- activeJobs.length > 0 &&
- activeJobs.length !== prevActiveJobsCount.current &&
- !expanded
- ) {
+ if (!loading && !hasInitializedRef.current) {
+ hasInitializedRef.current = true;
+ prevActiveCountRef.current = activeJobs.length;
+ }
+ }, [loading, activeJobs.length]);
+
+ // Auto expand/collapse por transiciones reales durante la sesión
+ useEffect(() => {
+ if (!hasInitializedRef.current) return;
+ const prev = prevActiveCountRef.current;
+ const curr = activeJobs.length;
+ if (prev === 0 && curr > 0) {
setExpanded(true);
- const toastTimeout = setTimeout(() => {}, 100);
- return () => clearTimeout(toastTimeout);
+ } else if (prev > 0 && curr === 0) {
+ setExpanded(false);
+ }
+ prevActiveCountRef.current = curr;
+ }, [activeJobs.length]);
+
+ useEffect(() => {
+ try {
+ localStorage.setItem("jobQueueWidgetExpanded", String(expanded));
+ } catch (e) {
+ // ignore
}
- prevActiveJobsCount.current = activeJobs.length;
- }, [activeJobs.length, expanded]);
+ }, [expanded]);
const handleToggleExpand = () => {
setExpanded(!expanded);
diff --git a/DashAI/front/src/components/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx
index 2c045ea81..912e2bede 100644
--- a/DashAI/front/src/components/models/AddModelDialog.jsx
+++ b/DashAI/front/src/components/models/AddModelDialog.jsx
@@ -29,7 +29,7 @@ import { useTourContext } from "../tour/TourProvider";
/**
* Dialog for adding a new model run to a session
* Step 1: Configure model name and parameters
- * Step 2: Configure optimizer
+ * Step 2: Configure optimizer for train
*/
function AddModelDialog({
open,
diff --git a/DashAI/front/src/components/models/CreateSessionSteps.jsx b/DashAI/front/src/components/models/CreateSessionSteps.jsx
index cdd7fd8e8..cb07d0199 100644
--- a/DashAI/front/src/components/models/CreateSessionSteps.jsx
+++ b/DashAI/front/src/components/models/CreateSessionSteps.jsx
@@ -7,7 +7,6 @@ import { useTourContext } from "../tour/TourProvider";
import SetNameAndDatasetStep from "./SetNameAndDatasetStep";
import PrepareDatasetStep from "../experiments/PrepareDatasetStep";
import FormSchemaButtonGroup from "../shared/FormSchemaButtonGroup";
-import JobQueueWidget from "../jobs/JobQueueWidget";
import { createModelSession } from "../../api/modelSession";
import { getComponents } from "../../api/component";
import { generateSequentialName } from "../../utils/nameGenerator";
@@ -309,9 +308,7 @@ function CreateSessionSteps({
right: "20px",
zIndex: 1000,
}}
- >
-
-
+ >
>
);
}
diff --git a/DashAI/front/src/components/models/HyperparameterPlots.jsx b/DashAI/front/src/components/models/HyperparameterPlots.jsx
new file mode 100644
index 000000000..48c4eaf5f
--- /dev/null
+++ b/DashAI/front/src/components/models/HyperparameterPlots.jsx
@@ -0,0 +1,213 @@
+import React, { useEffect, useState } from "react";
+import PropTypes from "prop-types";
+import Plot from "react-plotly.js";
+import { Grid, CircularProgress, Box, Typography } from "@mui/material";
+import { getHyperparameterPlot as getHyperparameterPlotRequest } from "../../api/run";
+import { enqueueSnackbar } from "notistack";
+import { checkHowManyOptimazers } from "../../utils/schema";
+
+function HyperparameterPlots({ run }) {
+ const [historicalPlot, setHistoricalPlot] = useState(null);
+ const [slicePlot, setSlicePlot] = useState(null);
+ const [contourPlot, setContourPlot] = useState(null);
+ const [importancePlot, setImportancePlot] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const parsePlot = (plot) => {
+ return JSON.parse(plot);
+ };
+
+ const optimizables = checkHowManyOptimazers({
+ params: run.parameters,
+ });
+
+ const getHyperparameterPlot = async () => {
+ try {
+ setLoading(true);
+ if (optimizables >= 2) {
+ const [historical, slice, contour, importance] = await Promise.all([
+ getHyperparameterPlotRequest(run.id, 1),
+ getHyperparameterPlotRequest(run.id, 2),
+ getHyperparameterPlotRequest(run.id, 3),
+ getHyperparameterPlotRequest(run.id, 4),
+ ]);
+
+ setHistoricalPlot(parsePlot(historical));
+ setSlicePlot(parsePlot(slice));
+ setContourPlot(parsePlot(contour));
+ setImportancePlot(parsePlot(importance));
+ } else if (optimizables === 1) {
+ const [historical, slice] = await Promise.all([
+ getHyperparameterPlotRequest(run.id, 1),
+ getHyperparameterPlotRequest(run.id, 2),
+ ]);
+
+ setHistoricalPlot(parsePlot(historical));
+ setSlicePlot(parsePlot(slice));
+ }
+ } catch (error) {
+ enqueueSnackbar("Error while trying to obtain hyperparameter plots", {
+ variant: "error",
+ });
+ console.error("Error loading hyperparameter plots:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (run.status === 3) {
+ getHyperparameterPlot();
+ } else {
+ setLoading(false);
+ }
+ }, [run.id, run.status, run.parameters]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (run.status === 2 || run.status === 1) {
+ return (
+
+
+ Training in progress. Hyperparameter plots will be available when
+ training completes.
+
+
+ );
+ }
+
+ if (run.status === 4) {
+ return (
+
+
+ Run failed. No hyperparameter plots available.
+
+
+ );
+ }
+
+ if (run.status === 0) {
+ return (
+
+
+ Run not started. No hyperparameter plots available.
+
+
+ );
+ }
+
+ if (!historicalPlot && !slicePlot) {
+ return (
+
+
+ No hyperparameter plots available.
+
+
+ );
+ }
+
+ return (
+
+
+ {historicalPlot && (
+
+
+
+ )}
+
+ {slicePlot && (
+
+
+
+ )}
+
+ {optimizables >= 2 && contourPlot && (
+
+
+
+ )}
+
+ {optimizables >= 2 && importancePlot && (
+
+
+
+ )}
+
+
+ );
+}
+
+HyperparameterPlots.propTypes = {
+ run: PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ status: PropTypes.number.isRequired,
+ parameters: PropTypes.object.isRequired,
+ }).isRequired,
+};
+
+export default HyperparameterPlots;
diff --git a/DashAI/front/src/components/models/LiveMetricsChart.jsx b/DashAI/front/src/components/models/LiveMetricsChart.jsx
new file mode 100644
index 000000000..0650db344
--- /dev/null
+++ b/DashAI/front/src/components/models/LiveMetricsChart.jsx
@@ -0,0 +1,364 @@
+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 protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ const host = window.location.host;
+ const ws = new WebSocket(
+ `${protocol}//${host}/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(() => {
+ if (!run.model_session_id) return;
+
+ 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.model_session_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) {
+ return;
+ }
+
+ if (hasEpochData) setLevel("EPOCH");
+ else if (hasStepData) setLevel("STEP");
+ else if (hasTrialData) setLevel("TRIAL");
+ else setLevel(null);
+ }, [split, hasEpochData, hasStepData, hasTrialData, level]);
+
+ useEffect(() => {
+ const metricNames = Object.keys(filteredMetrics);
+
+ if (metricNames.length === 0) {
+ setSelectedMetrics([]);
+ return;
+ }
+
+ const savedSelection = selectedMetricsPerSplit.current[split];
+
+ if (savedSelection !== null) {
+ const validSavedMetrics = savedSelection.filter((m) =>
+ metricNames.includes(m),
+ );
+ setSelectedMetrics(validSavedMetrics);
+ } else {
+ setSelectedMetrics(metricNames);
+ }
+ }, [split, level, filteredMetrics]);
+
+ const handleMetricChange = (e) => {
+ const newSelection = e.target.value;
+ setSelectedMetrics(newSelection);
+ selectedMetricsPerSplit.current[split] = newSelection;
+ };
+
+ const handleLevelChange = (newLevel) => {
+ setLevel(newLevel);
+ };
+
+ return (
+
+
+
+ Metrics
+
+
+
+
+ setSplit(v)} sx={{ mb: 2 }}>
+
+
+
+
+
+ {chartData.length === 0 || selectedMetrics.length === 0 ? (
+
+
+ No metrics available for this view
+
+
+ ) : (
+
+
+
+
+
+
+
+ {selectedMetrics.map((metric, idx) => (
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default LiveMetricsChart;
diff --git a/DashAI/front/src/components/models/PredictionCard.jsx b/DashAI/front/src/components/models/PredictionCard.jsx
index 40d3ac3d7..93b76b166 100644
--- a/DashAI/front/src/components/models/PredictionCard.jsx
+++ b/DashAI/front/src/components/models/PredictionCard.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useCallback } from "react";
+import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import {
Card,
@@ -38,11 +38,22 @@ const RUNNING_STATUSES = [1, 2]; // Delivered or Started
* PredictionCard - Displays a single prediction with results table
*/
export default function PredictionCard({ prediction, onDelete, onUpdate }) {
- const [expanded, setExpanded] = useState(true);
+ const [expanded, setExpanded] = useState(() => {
+ const saved = localStorage.getItem(`prediction-${prediction.id}-expanded`);
+ return saved !== null ? JSON.parse(saved) : true;
+ });
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const { t } = useTranslation(["prediction", "datasets", "common"]);
+ // Persist expanded state
+ useEffect(() => {
+ localStorage.setItem(
+ `prediction-${prediction.id}-expanded`,
+ JSON.stringify(expanded),
+ );
+ }, [expanded, prediction.id]);
+
const statusText = prediction.status;
// Status color mapping
diff --git a/DashAI/front/src/components/models/PredictionCreationDialog.jsx b/DashAI/front/src/components/models/PredictionCreationDialog.jsx
index ed6440696..5de4e2230 100644
--- a/DashAI/front/src/components/models/PredictionCreationDialog.jsx
+++ b/DashAI/front/src/components/models/PredictionCreationDialog.jsx
@@ -15,7 +15,6 @@ import {
StepLabel,
} from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material";
-import ModeSelector from "../predictions/ModeSelector";
import DatasetSelector from "../predictions/DatasetSelector";
import ManualInput from "../predictions/ManualInput";
import { createPrediction, filterDatasets } from "../../api/predict";
@@ -29,6 +28,7 @@ import { getModelSessionById } from "../../api/modelSession";
import { useSnackbar } from "notistack";
import { startJobPolling } from "../../utils/jobPoller";
import { getPredictions } from "../../api/predict";
+
import { useTranslation } from "react-i18next";
/**
@@ -40,9 +40,10 @@ export default function PredictionCreationDialog({
run,
session,
onPredictionCreated,
+ defaultMode = "dataset",
}) {
const [activeStep, setActiveStep] = useState(0);
- const [predictionMode, setPredictionMode] = useState("dataset");
+ const [predictionMode, setPredictionMode] = useState(defaultMode);
const [datasets, setDatasets] = useState([]);
const [selectedDataset, setSelectedDataset] = useState(null);
const [manualRows, setManualRows] = useState([]);
@@ -57,7 +58,6 @@ export default function PredictionCreationDialog({
const { t } = useTranslation(["prediction", "common"]);
const steps = [
- t("prediction:label.selectMode"),
t("prediction:label.configureInput"),
t("prediction:label.confirm"),
];
@@ -65,13 +65,13 @@ export default function PredictionCreationDialog({
useEffect(() => {
if (!open) {
setActiveStep(0);
- setPredictionMode("dataset");
+ setPredictionMode(defaultMode);
setDatasets([]);
setSelectedDataset(null);
setManualRows([]);
setIsLoading(false);
}
- }, [open]);
+ }, [open, defaultMode]);
useEffect(() => {
const fetchData = async () => {
@@ -209,19 +209,6 @@ export default function PredictionCreationDialog({
const renderStepContent = (step) => {
switch (step) {
case 0:
- return (
-
-
- {t("prediction:label.selectPredictionMode")}
-
-
-
- );
-
- case 1:
return (
{predictionMode === "dataset" ? (
@@ -255,7 +242,7 @@ export default function PredictionCreationDialog({
);
- case 2:
+ case 1:
return (
@@ -327,7 +314,7 @@ export default function PredictionCreationDialog({
))}
- {loadingExperiment && activeStep === 1 ? (
+ {loadingExperiment && activeStep === 0 ? (
{hasOperations && }
- {t("models:label.retrainModel")}
+
+ {mode === "save"
+ ? t("models:label.saveParameterChanges")
+ : t("models:label.retrainModel")}
+
@@ -43,13 +48,22 @@ export default function RetrainConfirmDialog({
{hasOperations ? (
<>
- {t("models:label.retrainWillDeleteOperations")}
+ {mode === "save"
+ ? t("models:message.saveWillDeleteOperations")
+ : t("models:label.retrainWillDeleteOperations")}
-
- Re-training run "{{ runName: run?.name }}" will
- delete:
-
+ {mode === "save" ? (
+
+ Saving "{{ runName: run?.name }}" will reset
+ the run. The following will be deleted when you train again:
+
+ ) : (
+
+ Re-training run "{{ runName: run?.name }}"
+ will delete:
+
+ )}
{operationsCount.explainers > 0 && (
@@ -98,9 +112,11 @@ export default function RetrainConfirmDialog({
color={hasOperations ? "warning" : "primary"}
autoFocus
>
- {hasOperations
- ? t("models:button.deleteAndRetrain")
- : t("models:button.retrain")}
+ {mode === "save"
+ ? t("common:saveChanges")
+ : hasOperations
+ ? t("models:button.deleteAndRetrain")
+ : t("models:button.retrain")}
@@ -111,6 +127,7 @@ RetrainConfirmDialog.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
+ mode: PropTypes.oneOf(["retrain", "save"]),
run: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx
index 4310d7c36..8f550186c 100644
--- a/DashAI/front/src/components/models/RunCard.jsx
+++ b/DashAI/front/src/components/models/RunCard.jsx
@@ -1,9 +1,8 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import {
Card,
CardContent,
- CardActions,
Box,
Typography,
Chip,
@@ -18,6 +17,9 @@ import {
TableRow,
Paper,
Divider,
+ Tooltip,
+ TextField,
+ Alert,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import {
@@ -25,14 +27,21 @@ import {
Stop,
Edit,
Delete,
+ Save,
+ Cancel,
ExpandMore,
ExpandLess,
- Settings,
- TrendingUp,
- QueryStats,
} from "@mui/icons-material";
+import { useSnackbar } from "notistack";
import { getRunStatus } from "../../utils/runStatus";
-import RunOperations from "./RunOperations";
+import RunResults from "./RunResults";
+import FormSchemaWithSelectedModel from "../shared/FormSchemaWithSelectedModel";
+import FormSchemaContainer from "../shared/FormSchemaContainer";
+import OptimizationTableSelectOptimizer from "../experiments/OptimizationTableSelectOptimizer";
+import ModelsTableSelectMetric from "../experiments/ModelsTableSelectMetric";
+import useSchema from "../../hooks/useSchema";
+import { updateRunParameters, getRunOperationsCount } from "../../api/run";
+import RetrainConfirmDialog from "./RetrainConfirmDialog";
import { useTranslation } from "react-i18next";
/**
@@ -43,49 +52,222 @@ function RunCard({
models = [],
session,
onTrain,
- onEdit,
- onExplainer,
onDelete,
onOperationsRefresh,
explainerRefreshTrigger,
isLastRun = false,
+ existingRuns = [],
+ onRefresh,
}) {
const theme = useTheme();
- const [expanded, setExpanded] = useState(false);
const { t } = useTranslation(["models", "common"]);
+ const { enqueueSnackbar } = useSnackbar();
+ const [expanded, setExpanded] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedName, setEditedName] = useState(run.name || "");
+ const [editedParameters, setEditedParameters] = useState(
+ run.parameters || {},
+ );
+ const [editedOptimizer, setEditedOptimizer] = useState(
+ run.optimizer_name || "",
+ );
+ const [editedOptimizerParams, setEditedOptimizerParams] = useState(
+ run.optimizer_parameters || {},
+ );
+ const [editedGoalMetric, setEditedGoalMetric] = useState(
+ run.goal_metric || "",
+ );
+ const [operationsCount, setOperationsCount] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveConfirmOpen, setSaveConfirmOpen] = useState(false);
+
+ const { defaultValues: defaultOptimizerParams } = useSchema({
+ modelName: editedOptimizer,
+ });
+
+ useEffect(() => {
+ if (!isEditing) {
+ setEditedName(run.name || "");
+ setEditedParameters(run.parameters || {});
+ setEditedOptimizer(run.optimizer_name || "");
+ setEditedOptimizerParams(run.optimizer_parameters || {});
+ setEditedGoalMetric(run.goal_metric || "");
+ }
+ }, [run, isEditing]);
+
+ useEffect(() => {
+ const fetchOperationsCount = async () => {
+ if (!run || !run.id) return;
+ try {
+ const count = await getRunOperationsCount(run.id.toString());
+ setOperationsCount(count);
+ } catch (error) {
+ console.error("Error fetching operations count:", error);
+ }
+ };
+ fetchOperationsCount();
+ }, [run, explainerRefreshTrigger]);
+
+ const hasOptimizableParams = useMemo(() => {
+ return Object.values(editedParameters).some(
+ (value) =>
+ value &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ value.optimize === true,
+ );
+ }, [editedParameters]);
+
+ useEffect(() => {
+ if (
+ editedOptimizer &&
+ defaultOptimizerParams &&
+ Object.keys(defaultOptimizerParams).length > 0
+ ) {
+ setEditedOptimizerParams((prev) => {
+ if (Object.keys(prev).length === 0) {
+ return defaultOptimizerParams;
+ }
+ return prev;
+ });
+ }
+ }, [editedOptimizer, defaultOptimizerParams]);
+
+ const handleStartEdit = () => {
+ setIsEditing(true);
+ setExpanded(true);
+ };
+
+ const handleCancelEdit = () => {
+ setIsEditing(false);
+ setEditedName(run.name || "");
+ setEditedParameters(run.parameters || {});
+ setEditedOptimizer(run.optimizer_name || "");
+ setEditedOptimizerParams(run.optimizer_parameters || {});
+ setEditedGoalMetric(run.goal_metric || "");
+ };
+
+ const doSave = async () => {
+ setSaveConfirmOpen(false);
+ setIsSaving(true);
+ try {
+ await updateRunParameters(
+ run.id.toString(),
+ editedName.trim(),
+ editedParameters,
+ editedOptimizer || "",
+ editedOptimizerParams || {},
+ editedGoalMetric || "",
+ );
+
+ enqueueSnackbar(
+ t("models:message.runUpdatedSuccess", { runName: editedName }),
+ { variant: "success" },
+ );
+
+ setIsEditing(false);
+
+ if (onRefresh) {
+ await onRefresh();
+ }
+ } catch (error) {
+ console.error("Error updating run:", error);
+ enqueueSnackbar(
+ t("models:error.failedToUpdateRun", {
+ error: error.message || t("common:unknownError"),
+ }),
+ { variant: "error" },
+ );
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleSaveEdit = async () => {
+ if (!editedName.trim()) {
+ enqueueSnackbar(t("models:error.runNameEmpty"), { variant: "warning" });
+ return;
+ }
+
+ const nameExists = existingRuns.some(
+ (r) =>
+ r.id !== run.id &&
+ r.name &&
+ r.name.toLowerCase() === editedName.trim().toLowerCase(),
+ );
+ if (nameExists) {
+ enqueueSnackbar(
+ t("models:error.runNameExists", { name: editedName.trim() }),
+ { variant: "error" },
+ );
+ return;
+ }
+
+ if (hasOptimizableParams) {
+ if (!editedOptimizer) {
+ enqueueSnackbar(t("models:error.selectOptimizerRequired"), {
+ variant: "warning",
+ });
+ return;
+ }
+ if (!editedGoalMetric) {
+ enqueueSnackbar(t("models:error.selectGoalMetricRequired"), {
+ variant: "warning",
+ });
+ return;
+ }
+ }
- // Get display status from numeric code
- const statusText = run.status;
+ // If operations exist, warn before saving (they will be deleted on next train)
+ if (
+ operationsCount &&
+ (operationsCount.explainers > 0 || operationsCount.predictions > 0)
+ ) {
+ setSaveConfirmOpen(true);
+ return;
+ }
+
+ await doSave();
+ };
+
+ const handleParametersChange = useCallback((values) => {
+ setEditedParameters(values);
+ }, []);
+
+ const handleOptimizerParamsChange = useCallback((values) => {
+ setEditedOptimizerParams((prev) => ({ ...prev, ...values }));
+ }, []);
+
+ const handleOptimizerSelected = (optimizerName, defaultValues) => {
+ setEditedOptimizer(optimizerName);
+ if (defaultValues && Object.keys(defaultValues).length > 0) {
+ setEditedOptimizerParams(defaultValues);
+ }
+ };
- // Get model display name
+ const statusText = getRunStatus(run.status, t);
const model = models.find((m) => m.name === run.model_name);
const modelDisplayName = model?.display_name || run.model_name;
- // Status color mapping
const getStatusColor = (status) => {
switch (status) {
- case 0: // Not Started
+ case 0:
return "default";
- case 1: // Delivered
- case 2: // Started
+ case 1:
+ case 2:
return "info";
- case 3: // Finished
+ case 3:
return "success";
- case 4: // Error
+ case 4:
return "error";
default:
return "default";
}
};
- // Check if run can be trained
- const canTrain =
- statusText === 0 || // Not Started
- statusText === 4 || // Error
- statusText === 3; // Finished
- const isRunning = statusText === 1 || statusText === 2; // Delivered or Started
+ const canTrain = run.status === 0 || run.status === 3 || run.status === 4; // Not Started, Finished, Error
+ const isRunning = run.status === 1 || run.status === 2; // Delivered, Started
- // Get metrics from run
const getMetrics = () => {
if (!run.trained_models || run.trained_models.length === 0) {
return null;
@@ -113,9 +295,9 @@ function RunCard({
mb: 2,
borderLeft: "4px solid",
borderLeftColor:
- statusText === 3 // Finished
+ run.status === 3 // Finished
? "success.main"
- : statusText === 4 // Error
+ : run.status === 4 // Error
? "error.main"
: isRunning
? "info.main"
@@ -123,37 +305,159 @@ function RunCard({
}}
>
- {/* Run Name and Status */}
-
- {run.name}
-
-
-
+
+
+ setExpanded(!expanded)}
+ color={expanded ? "primary" : "default"}
+ disabled={isEditing}
+ >
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {modelDisplayName}
+
+ ({run.name})
+
+
+
- {/* Model */}
-
-
-
- {modelDisplayName}
-
+
+ {isEditing && (
+ <>
+ }
+ onClick={handleCancelEdit}
+ disabled={isSaving}
+ >
+ {t("common:cancel")}
+
+ }
+ onClick={handleSaveEdit}
+ disabled={isSaving}
+ >
+ {isSaving ? t("common:saving") : t("common:save")}
+
+ >
+ )}
+
+ {!isEditing && run.status !== 1 && run.status !== 2 && (
+ }
+ onClick={handleStartEdit}
+ >
+ {t("common:edit")}
+
+ )}
+ {canTrain && (
+ 0 ||
+ operationsCount.predictions > 0)
+ ? t("models:message.retrainWillResetOperations", {
+ explainersCount: operationsCount.explainers,
+ predictionsCount: operationsCount.predictions,
+ })
+ : ""
+ }
+ >
+
+
+ )}
+ {isRunning && (
+ }
+ >
+ {t("common:running")}
+
+ )}
+
+
+
+
+ onDelete(run)}
+ disabled={isRunning}
+ >
+
+
+
+
- {/* Metrics Summary */}
{metrics && Object.keys(metrics).length > 0 && (
@@ -181,7 +485,6 @@ function RunCard({
)}
- {/* Description if present */}
{run.description && (
)}
- {/* Expandable Details */}
-
-
-
-
-
- {/* Model Parameters */}
- {run.parameters && Object.keys(run.parameters).length > 0 && (
-
-
- {t("common:modelParameters")}
-
-
-
-
-
- {t("common:parameter")}
- {t("common:value")}
-
-
-
- {Object.entries(run.parameters).map(([key, value]) => (
-
- {key}
-
- {typeof value === "object" && value !== null
- ? value.fixed_value !== undefined
- ? String(value.fixed_value)
- : JSON.stringify(value)
- : String(value)}
-
-
- ))}
-
-
-
-
- )}
-
- {/* Optimizer Configuration */}
- {run.optimizer_name && (
-
-
- {t("common:optimizer")}: {run.optimizer_name}
-
- {run.optimizer_parameters &&
- Object.keys(run.optimizer_parameters).length > 0 && (
-
-
-
-
- {t("common:parameter")}
- {t("common:value")}
-
-
-
- {Object.entries(run.optimizer_parameters).map(
- ([key, value]) => (
-
- {key}
-
- {typeof value === "object"
- ? JSON.stringify(value)
- : String(value)}
-
-
- ),
- )}
-
-
-
- )}
-
- )}
-
- {/* Goal Metric */}
- {run.goal_metric && (
-
-
+
+ {t("models:message.editingParametersWarning")}
+
+
+ setEditedName(e.target.value)}
+ fullWidth
+ required
+ size="small"
+ />
+
+ {run.model_name && (
+
+
+ {t("common:modelParameters")}
+
+
+ {}}
+ hideButtons
+ />
+
+
+ )}
+
+ {hasOptimizableParams && (
+
- {t("models:label.goalMetric")}:{" "}
- {run.goal_metric}
-
-
- )}
-
-
-
+
+
+ {t("models:label.hyperparameterOptimizerConfiguration")}
+
+
+ {t("models:message.parametersMarkedForOptimization")}
+
- {/* RunOperations - Separate section for finished runs */}
- {statusText === 3 && ( // Finished
-
-
-
- )}
-
+
+
+ {t("models:label.goalMetric")} *
+
+
+
-
+ {/* Optimizer Selection */}
+
-
- {/* Train/Stop button */}
- {isRunning && (
- } disabled color="warning">
- {t("common:running")}
-
- )}
- {canTrain && (
- }
- onClick={() => onTrain(run)}
- color="primary"
- variant="contained"
- >
- {t("common:trainVerb")}
-
- )}
+ {editedOptimizer && (
+
+
+ {t("common:optimizerParameters")}
+
+
+
+ setEditedOptimizerParams(values)
+ }
+ onValuesChange={handleOptimizerParamsChange}
+ onCancel={() => {}}
+ hideButtons
+ />
+
+
+ )}
+
+ )}
- {/* Edit button */}
- onEdit(run)}
- color="primary"
- disabled={isRunning}
- title={t("common:editParameters")}
- >
-
-
-
- {/* Prediction button */}
- {statusText === 3 && ( // Finished
- {
- // Scroll to operations and open prediction dialog
- const operationsElement = document.getElementById(
- `run-operations-${run.id}`,
- );
- if (operationsElement) {
- operationsElement.scrollIntoView({
- behavior: "smooth",
- block: "nearest",
- });
- // Trigger prediction dialog open via custom event
- const event = new CustomEvent("openPredictionDialog", {
- detail: { runId: run.id },
- });
- window.dispatchEvent(event);
- }
- }}
- color="primary"
- title={t("models:button.createPrediction")}
- >
-
-
- )}
+
+ }
+ onClick={handleCancelEdit}
+ disabled={isSaving}
+ >
+ {t("common:cancel")}
+
+ }
+ onClick={handleSaveEdit}
+ disabled={isSaving}
+ >
+ {isSaving ? t("common:saving") : t("common:saveChanges")}
+
+
+
+ ) : (
+
+ {run.parameters && Object.keys(run.parameters).length > 0 && (
+
+
+ {t("common:modelParameters")}
+
+
+
+
+
+ {t("common:parameter")}
+ {t("common:value")}
+
+
+
+ {Object.entries(run.parameters).map(
+ ([key, value]) => (
+
+ {key}
+
+ {typeof value === "object" && value !== null
+ ? value.fixed_value !== undefined
+ ? String(value.fixed_value)
+ : JSON.stringify(value)
+ : String(value)}
+
+
+ ),
+ )}
+
+
+
+
+ )}
- {/* Explainer button */}
- {onExplainer &&
- statusText === 3 && ( // Finished
- onExplainer(run)}
- color="primary"
- title={t("models:button.createExplainer")}
- >
-
-
- )}
-
- {/* Delete button */}
- onDelete(run)}
- color="error"
- disabled={isRunning}
- title={t("models:button.deleteRun")}
- >
-
-
-
+ {run.optimizer_name && run.goal_metric && (
+
+
+ {t("common:optimizer")}: {run.optimizer_name}
+
+ {run.optimizer_parameters &&
+ Object.keys(run.optimizer_parameters).length > 0 && (
+
+
+
+
+ {t("common:parameter")}
+ {t("common:value")}
+
+
+
+ {Object.entries(run.optimizer_parameters).map(
+ ([key, value]) => (
+
+ {key}
+
+ {typeof value === "object"
+ ? JSON.stringify(value)
+ : String(value)}
+
+
+ ),
+ )}
+
+
+
+ )}
+
+ )}
+
+ {run.goal_metric && (
+
+
+ {t("models:label.goalMetric")}:{" "}
+ {run.goal_metric}
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+ setSaveConfirmOpen(false)}
+ onConfirm={doSave}
+ run={run}
+ operationsCount={operationsCount}
+ />
+
);
}
@@ -415,11 +748,12 @@ RunCard.propTypes = {
task_name: PropTypes.string,
}),
onTrain: PropTypes.func.isRequired,
- onEdit: PropTypes.func.isRequired,
- onExplainer: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onOperationsRefresh: PropTypes.func,
explainerRefreshTrigger: PropTypes.number,
+ isLastRun: PropTypes.bool,
+ existingRuns: PropTypes.array,
+ onRefresh: PropTypes.func,
};
export default RunCard;
diff --git a/DashAI/front/src/components/models/RunOperations.jsx b/DashAI/front/src/components/models/RunOperations.jsx
deleted file mode 100644
index 560e5e7ad..000000000
--- a/DashAI/front/src/components/models/RunOperations.jsx
+++ /dev/null
@@ -1,359 +0,0 @@
-import React, { useState, useEffect, useCallback } from "react";
-import PropTypes from "prop-types";
-import {
- Box,
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Button,
- Chip,
- Stack,
- CircularProgress,
- Collapse,
-} from "@mui/material";
-import {
- ExpandMore as ExpandMoreIcon,
- ExpandLess as ExpandLessIcon,
- Add as AddIcon,
- TrendingUp as TrendingUpIcon,
-} from "@mui/icons-material";
-import ExplainersCard from "../explainers/ExplanainersCard";
-import PredictionCard from "./PredictionCard";
-import NewGlobalExplainerModal from "../explainers/NewGlobalExplainerModal";
-import NewLocalExplainerModal from "../explainers/NewLocalExplainerModal";
-import PredictionCreationDialog from "./PredictionCreationDialog";
-import { getExplainers } from "../../api/explainer";
-import { getPredictions } from "../../api/predict";
-import { useTranslation } from "react-i18next";
-
-/**
- * RunOperations component - Shows explainers and predictions for a finished run
- * Displays as expandable sections within a RunCard
- */
-export default function RunOperations({
- run,
- session,
- onRefresh,
- explainerRefreshTrigger,
-}) {
- const [globalExplainers, setGlobalExplainers] = useState([]);
- const [localExplainers, setLocalExplainers] = useState([]);
- const [predictions, setPredictions] = useState([]);
- const [loading, setLoading] = useState(true);
- const [operationsVisible, setOperationsVisible] = useState(false);
-
- const [globalDialogOpen, setGlobalDialogOpen] = useState(false);
- const [localDialogOpen, setLocalDialogOpen] = useState(false);
- const [predictionDialogOpen, setPredictionDialogOpen] = useState(false);
-
- const [expandedSections, setExpandedSections] = useState({
- globalExplainers: false,
- localExplainers: false,
- predictions: false,
- });
-
- const { t } = useTranslation(["models"]);
-
- const fetchOperations = useCallback(async () => {
- if (!run || !run.id) return;
-
- setLoading(true);
- try {
- const [globalExpls, localExpls, preds] = await Promise.all([
- getExplainers(run.id, "global").catch(() => []),
- getExplainers(run.id, "local").catch(() => []),
- getPredictions(run.id).catch(() => []),
- ]);
-
- setGlobalExplainers(globalExpls);
- setLocalExplainers(localExpls);
- setPredictions(preds);
- } catch (error) {
- console.error("Error fetching operations:", error);
- } finally {
- setLoading(false);
- }
- }, [run]);
-
- useEffect(() => {
- fetchOperations();
- }, [fetchOperations, explainerRefreshTrigger]);
-
- // Listen for prediction dialog open event
- useEffect(() => {
- const handleOpenDialog = (event) => {
- if (event.detail.runId === run.id) {
- setPredictionDialogOpen(true);
- }
- };
- window.addEventListener("openPredictionDialog", handleOpenDialog);
- return () =>
- window.removeEventListener("openPredictionDialog", handleOpenDialog);
- }, [run.id]);
-
- const handleAccordionChange = (section) => (event, isExpanded) => {
- setExpandedSections((prev) => ({
- ...prev,
- [section]: isExpanded,
- }));
- };
-
- const handleExplainerCreated = () => {
- fetchOperations();
- if (onRefresh) onRefresh();
- };
-
- const handlePredictionCreated = (prediction) => {
- if (prediction) {
- setPredictions((prev) => [prediction, ...prev]);
- }
- fetchOperations();
- if (onRefresh) onRefresh();
- };
-
- const handleExplainerDeleted = () => {
- fetchOperations();
- if (onRefresh) onRefresh();
- };
-
- const handlePredictionDeleted = () => {
- fetchOperations();
- if (onRefresh) onRefresh();
- };
-
- const totalOperations =
- globalExplainers.length + localExplainers.length + predictions.length;
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- return (
-
- {/* Header with Show/Hide button */}
-
-
-
-
-
- {/* Global Explainers Section */}
-
- }>
-
-
- {t("models:label.globalExplainers")}
-
-
-
-
-
-
- }
- onClick={() => setGlobalDialogOpen(true)}
- fullWidth
- >
- {t("models:button.createGlobalExplainer")}
-
- {globalExplainers.length === 0 ? (
-
- {t("models:label.noGlobalExplainersYet")}
-
- ) : (
- globalExplainers.map((explainer) => (
-
- ))
- )}
-
-
-
-
- {/* Local Explainers Section */}
-
- }>
-
-
- {t("models:label.localExplainers")}
-
-
-
-
-
-
- }
- onClick={() => setLocalDialogOpen(true)}
- fullWidth
- >
- {t("models:button.createLocalExplainer")}
-
- {localExplainers.length === 0 ? (
-
- {t("models:label.noLocalExplainersYet")}
-
- ) : (
- localExplainers.map((explainer) => (
-
- ))
- )}
-
-
-
-
- {/* Predictions Section */}
-
- }>
-
-
- {t("models:label.predictions")}
-
-
-
-
-
-
- }
- onClick={() => setPredictionDialogOpen(true)}
- fullWidth
- >
- {t("models:button.newPrediction")}
-
- {predictions.length === 0 ? (
-
- {t("models:label.noPredictionsYet")}
-
- ) : (
- predictions.map((prediction) => (
-
- ))
- )}
-
-
-
-
-
- {/* Dialogs */}
-
-
-
-
- setPredictionDialogOpen(false)}
- run={run}
- session={session}
- onPredictionCreated={handlePredictionCreated}
- />
-
- );
-}
-
-RunOperations.propTypes = {
- run: PropTypes.shape({
- id: PropTypes.number.isRequired,
- name: PropTypes.string,
- model_name: PropTypes.string,
- status: PropTypes.number,
- model_session_id: PropTypes.number,
- }).isRequired,
- session: PropTypes.shape({
- id: PropTypes.number,
- name: PropTypes.string,
- task_name: PropTypes.string,
- }),
- onRefresh: PropTypes.func,
- explainerRefreshTrigger: PropTypes.number,
-};
diff --git a/DashAI/front/src/components/models/RunResults.jsx b/DashAI/front/src/components/models/RunResults.jsx
new file mode 100644
index 000000000..36e086773
--- /dev/null
+++ b/DashAI/front/src/components/models/RunResults.jsx
@@ -0,0 +1,556 @@
+import React, { useState, useEffect, useCallback } from "react";
+import PropTypes from "prop-types";
+import {
+ Box,
+ Typography,
+ Button,
+ Chip,
+ Stack,
+ CircularProgress,
+ Collapse,
+ Tabs,
+ Tab,
+ Grid,
+} from "@mui/material";
+import {
+ ExpandMore as ExpandMoreIcon,
+ ExpandLess as ExpandLessIcon,
+ Add as AddIcon,
+ TrendingUp as TrendingUpIcon,
+} from "@mui/icons-material";
+import ExplainersCard from "../explainers/ExplanainersCard";
+import PredictionCard from "./PredictionCard";
+import NewGlobalExplainerModal from "../explainers/NewGlobalExplainerModal";
+import NewLocalExplainerModal from "../explainers/NewLocalExplainerModal";
+import PredictionCreationDialog from "./PredictionCreationDialog";
+import LiveMetricsChart from "./LiveMetricsChart";
+import HyperparameterPlots from "./HyperparameterPlots";
+import { getExplainers } from "../../api/explainer";
+import { getPredictions } from "../../api/predict";
+import { checkHowManyOptimazers } from "../../utils/schema";
+import { useTranslation } from "react-i18next";
+
+export default function RunResults({
+ run,
+ session,
+ onRefresh,
+ explainerRefreshTrigger,
+}) {
+ const [globalExplainers, setGlobalExplainers] = useState([]);
+ const [localExplainers, setLocalExplainers] = useState([]);
+ const [predictions, setPredictions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [resultsVisible, setResultsVisible] = useState(() => {
+ const saved = localStorage.getItem(`run-${run.id}-results-visible`);
+ return saved ? JSON.parse(saved) : false;
+ });
+ const [activeTab, setActiveTab] = useState(() => {
+ const saved = localStorage.getItem(`run-${run.id}-active-tab`);
+ return saved ? JSON.parse(saved) : 0;
+ });
+
+ const [globalDialogOpen, setGlobalDialogOpen] = useState(false);
+ const [localDialogOpen, setLocalDialogOpen] = useState(false);
+ const [datasetPredictionDialogOpen, setDatasetPredictionDialogOpen] =
+ useState(false);
+ const [manualPredictionDialogOpen, setManualPredictionDialogOpen] =
+ useState(false);
+
+ const optimizables = checkHowManyOptimazers({ params: run.parameters });
+ const isFinished = run.status === 3;
+ const isRunning = run.status === 1 || run.status === 2;
+ const { t } = useTranslation("models");
+
+ const fetchOperations = useCallback(async () => {
+ if (!run || !run.id) return;
+
+ setLoading(true);
+ try {
+ const [globalExpls, localExpls, preds] = await Promise.all([
+ getExplainers(run.id, "global").catch(() => []),
+ getExplainers(run.id, "local").catch(() => []),
+ getPredictions(run.id).catch(() => []),
+ ]);
+
+ setGlobalExplainers(globalExpls);
+ setLocalExplainers(localExpls);
+ setPredictions(preds);
+ } catch (error) {
+ console.error("Error fetching operations:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [run]);
+
+ useEffect(() => {
+ fetchOperations();
+ }, [fetchOperations, explainerRefreshTrigger]);
+
+ useEffect(() => {
+ const handleOpenDialog = (event) => {
+ if (event.detail.runId === run.id) {
+ setDatasetPredictionDialogOpen(true);
+ }
+ };
+ window.addEventListener("openPredictionDialog", handleOpenDialog);
+ return () =>
+ window.removeEventListener("openPredictionDialog", handleOpenDialog);
+ }, [run.id]);
+
+ useEffect(() => {
+ if (isRunning && !resultsVisible) {
+ setResultsVisible(true);
+ setActiveTab(0); // Live Metrics tab
+ }
+ }, [isRunning, resultsVisible]);
+
+ useEffect(() => {
+ localStorage.setItem(
+ `run-${run.id}-results-visible`,
+ JSON.stringify(resultsVisible),
+ );
+ }, [resultsVisible, run.id]);
+
+ useEffect(() => {
+ localStorage.setItem(`run-${run.id}-active-tab`, JSON.stringify(activeTab));
+ }, [activeTab, run.id]);
+
+ const handleExplainerCreated = () => {
+ fetchOperations();
+ if (onRefresh) onRefresh();
+ };
+
+ const handlePredictionCreated = (prediction) => {
+ if (prediction) {
+ setPredictions((prev) => [prediction, ...prev]);
+ }
+ fetchOperations();
+ if (onRefresh) onRefresh();
+ };
+
+ const handleExplainerDeleted = () => {
+ fetchOperations();
+ if (onRefresh) onRefresh();
+ };
+
+ const handlePredictionDeleted = () => {
+ fetchOperations();
+ if (onRefresh) onRefresh();
+ };
+
+ const totalOperations =
+ globalExplainers.length + localExplainers.length + predictions.length;
+
+ if (loading && isFinished) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ setActiveTab(newValue)}
+ aria-label="Results tabs"
+ >
+
+
+ {t("models:label.explainability")}
+ {isFinished && (
+
+ )}
+
+ }
+ disabled={!isFinished}
+ />
+
+ {t("models:label.predictions")}
+ {isFinished && (
+
+ )}
+
+ }
+ disabled={!isFinished}
+ />
+
+
+
+
+ {activeTab === 0 && (
+
+
+
+ )}
+
+ {activeTab === 1 && isFinished && (
+
+
+
+
+
+
+
+ {t("models:label.globalExplainers")}
+
+
+
+
+
+ }
+ onClick={() => setGlobalDialogOpen(true)}
+ fullWidth
+ >
+ {t("models:button.createGlobalExplainer")}
+
+ {globalExplainers.length === 0 ? (
+
+ {t("models:label.noGlobalExplainersYet")}
+
+ ) : (
+ globalExplainers.map((explainer) => (
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+ {t("models:label.localExplainers")}
+
+
+
+
+
+ }
+ onClick={() => setLocalDialogOpen(true)}
+ fullWidth
+ >
+ {t("models:button.createLocalExplainer")}
+
+ {localExplainers.length === 0 ? (
+
+ {t("models:label.noLocalExplainersYet")}
+
+ ) : (
+ localExplainers.map((explainer) => (
+
+ ))
+ )}
+
+
+
+
+
+ )}
+
+ {activeTab === 2 && isFinished && (
+
+
+
+
+
+
+
+ {t("models:label.datasetPredictions")}
+
+ p.dataset_id).length}
+ size="small"
+ color="primary"
+ />
+
+
+
+ }
+ onClick={() => setDatasetPredictionDialogOpen(true)}
+ fullWidth
+ >
+ {t("models:button.newDatasetPrediction")}
+
+ {predictions.filter((p) => p.dataset_id).length === 0 ? (
+
+ {t("models:label.noDatasetPredictionsYet")}
+
+ ) : (
+ predictions
+ .filter((p) => p.dataset_id)
+ .map((prediction) => (
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+ {t("models:label.manualPredictions")}
+
+ !p.dataset_id).length}
+ size="small"
+ color="primary"
+ />
+
+
+
+ }
+ onClick={() => setManualPredictionDialogOpen(true)}
+ fullWidth
+ >
+ {t("models:button.newManualPrediction")}
+
+ {predictions.filter((p) => !p.dataset_id).length === 0 ? (
+
+ {t("models:label.noManualPredictionsYet")}
+
+ ) : (
+ predictions
+ .filter((p) => !p.dataset_id)
+ .map((prediction) => (
+
+ ))
+ )}
+
+
+
+
+
+ )}
+
+ {activeTab === 3 && isFinished && optimizables > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+ setDatasetPredictionDialogOpen(false)}
+ run={run}
+ session={session}
+ onPredictionCreated={handlePredictionCreated}
+ defaultMode="dataset"
+ />
+
+ setManualPredictionDialogOpen(false)}
+ run={run}
+ session={session}
+ onPredictionCreated={handlePredictionCreated}
+ defaultMode="manual"
+ />
+
+ );
+}
+
+RunResults.propTypes = {
+ run: PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string,
+ model_name: PropTypes.string,
+ status: PropTypes.number,
+ experiment_id: PropTypes.number,
+ parameters: PropTypes.object,
+ model_session_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ test_metrics: PropTypes.object,
+ }).isRequired,
+ session: PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ task_name: PropTypes.string,
+ }),
+ onRefresh: PropTypes.func,
+ explainerRefreshTrigger: PropTypes.number,
+};
diff --git a/DashAI/front/src/components/models/SessionVisualization.jsx b/DashAI/front/src/components/models/SessionVisualization.jsx
index 08d5881ba..c3cccbafb 100644
--- a/DashAI/front/src/components/models/SessionVisualization.jsx
+++ b/DashAI/front/src/components/models/SessionVisualization.jsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
-import PropTypes from "prop-types";
import {
Box,
Typography,
@@ -8,49 +7,38 @@ import {
Divider,
Button,
ButtonGroup,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
ToggleButtonGroup,
ToggleButton,
} from "@mui/material";
import { PlayArrow, TableChart, BarChart } from "@mui/icons-material";
-import JobQueueWidget from "../jobs/JobQueueWidget";
import ModelComparisonTable from "./ModelComparisonTable";
import RunCard from "./RunCard";
import { getComponents } from "../../api/component";
import ResultsGraphs from "../../pages/results/components/ResultsGraphs";
-import NewGlobalExplainerModal from "../explainers/NewGlobalExplainerModal";
-import NewLocalExplainerModal from "../explainers/NewLocalExplainerModal";
import RetrainConfirmDialog from "./RetrainConfirmDialog";
import { useTranslation } from "react-i18next";
+
import { useModels } from "./ModelsContext";
import { useTourContext } from "../tour/TourProvider";
export default function SessionVisualization() {
- const sessionTourContext = useTourContext();
const [models, setModels] = useState([]);
const [selectedRunId, setSelectedRunId] = useState(null);
const [tableHeight, setTableHeight] = useState(280);
const [showTable, setShowTable] = useState(true);
const [previousTableHeight, setPreviousTableHeight] = useState(280);
const [metricSplit, setMetricSplit] = useState("test");
- const [selectedRunForExplainer, setSelectedRunForExplainer] = useState(null);
- const [explainerDialogOpen, setExplainerDialogOpen] = useState(false);
- const [globalExplainerModalOpen, setGlobalExplainerModalOpen] =
- useState(false);
- const [localExplainerModalOpen, setLocalExplainerModalOpen] = useState(false);
const [explainerRefreshTrigger, setExplainerRefreshTrigger] = useState(0);
const isResizing = React.useRef(false);
const { t } = useTranslation(["models", "common"]);
+ const sessionTourContext = useTourContext();
const {
selectedSession: session,
runs,
onTrainRun: onTrain,
- onEditRun,
onDeleteRun,
+ fetchRuns,
retrainDialogOpen,
runToRetrain,
operationsCount,
@@ -131,25 +119,6 @@ export default function SessionVisualization() {
}
}, []);
- const handleExplainer = React.useCallback((run) => {
- setSelectedRunForExplainer(run);
- setExplainerDialogOpen(true);
- }, []);
-
- const handleCloseExplainerDialog = () => {
- setExplainerDialogOpen(false);
- };
-
- const handleGlobalExplainer = () => {
- setGlobalExplainerModalOpen(true);
- setExplainerDialogOpen(false);
- };
-
- const handleLocalExplainer = () => {
- setLocalExplainerModalOpen(true);
- setExplainerDialogOpen(false);
- };
-
const sortedRuns = React.useMemo(
() => [...runs].sort((a, b) => new Date(a.created) - new Date(b.created)),
[runs],
@@ -228,7 +197,6 @@ export default function SessionVisualization() {
{t("models:label.selectSessionToViewModels")}
-
>
);
}
@@ -437,11 +405,14 @@ export default function SessionVisualization() {
models={models}
session={session}
onTrain={handleTrainWithTour}
- onEdit={onEditRun}
- onExplainer={handleExplainer}
onDelete={onDeleteRun}
explainerRefreshTrigger={explainerRefreshTrigger}
+ onOperationsRefresh={() =>
+ setExplainerRefreshTrigger((prev) => prev + 1)
+ }
isLastRun={index === sortedRuns.length - 1}
+ existingRuns={runs}
+ onRefresh={fetchRuns}
/>
))}
@@ -450,69 +421,6 @@ export default function SessionVisualization() {
- {/* Explainer Type Selection Dialog */}
-
-
- {/* Global Explainer Modal */}
- {
- setExplainerRefreshTrigger((prev) => prev + 1);
- }}
- />
-
- {/* Local Explainer Modal */}
- {
- setExplainerRefreshTrigger((prev) => prev + 1);
- }}
- />
-
- {/* Retrain Confirmation Dialog */}
-
-
>
);
}
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
index 88092e6e9..6d449f98a 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
@@ -4,6 +4,7 @@ import FormSchemaButtonGroup from "../../shared/FormSchemaButtonGroup";
import Upload from "./Upload";
import { useSnackbar } from "notistack";
import { enqueueDatasetJob as enqueueDatasetRequest } from "../../../api/job";
+import { forceRefreshNow } from "../../../utils/jobPoller";
import { useTourContext } from "../../tour/TourProvider";
import { createDataset } from "../../../api/datasets";
@@ -84,6 +85,7 @@ export default function ConfigureAndUploadDatasetStep({
try {
const job = await enqueueDatasetRequest(data.id, file, url, params);
+ forceRefreshNow();
handleDatasetCreated(data, job);
if (tourContext?.run) {
diff --git a/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx b/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx
index 0f06f84de..7d7c0042c 100644
--- a/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx
+++ b/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx
@@ -2,7 +2,6 @@ import React, { useState } from "react";
import { Box, Divider } from "@mui/material";
import NotebookView from "./NotebookView";
import DatasetPreviewNotebook from "./DatasetPreviewNotebook";
-import JobQueueWidget from "../../jobs/JobQueueWidget";
export default function NotebookVisualization({
notebook,
@@ -32,7 +31,6 @@ export default function NotebookVisualization({
-
>
);
}
diff --git a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx
index 1eb6c72d9..2551d3507 100644
--- a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx
@@ -1,8 +1,6 @@
-import { Box, Typography } from "@mui/material";
-import { useTheme } from "@mui/material/styles";
+import { Box } from "@mui/material";
export default function BarHeader() {
- const theme = useTheme();
return (
-
- Dash
-
-
+ >
);
}
diff --git a/DashAI/front/src/components/threeSectionLayout/Footer.jsx b/DashAI/front/src/components/threeSectionLayout/Footer.jsx
index c157d09a9..1605070fe 100644
--- a/DashAI/front/src/components/threeSectionLayout/Footer.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/Footer.jsx
@@ -1,8 +1,6 @@
-import { Box, Avatar, Divider } from "@mui/material";
-import { useTheme } from "@mui/material/styles";
+import { Box } from "@mui/material";
export default function Footer() {
- const theme = useTheme();
return (
-
-
-
+ >
);
}
diff --git a/DashAI/front/src/hooks/models/useSessions.js b/DashAI/front/src/hooks/models/useSessions.js
index a782c7e60..911d0c913 100644
--- a/DashAI/front/src/hooks/models/useSessions.js
+++ b/DashAI/front/src/hooks/models/useSessions.js
@@ -128,11 +128,9 @@ export function useSessions({ t }) {
const executeTraining = async (run) => {
try {
- // Delete operations if they exist
- if (
- operationsCount &&
- (operationsCount.explainers > 0 || operationsCount.predictions > 0)
- ) {
+ // Always silently delete any existing operations before training
+ const opsCount = await getRunOperationsCount(run.id.toString());
+ if (opsCount.explainers > 0 || opsCount.predictions > 0) {
await deleteRunOperations(run.id.toString());
}
@@ -212,7 +210,7 @@ export function useSessions({ t }) {
const onTrainRun = async (run) => {
try {
- // Check if run has been trained before (has metrics)
+ // Only show confirmation dialog for previously trained runs (Retrain flow)
const hasBeenTrained =
run.test_metrics ||
run.train_metrics ||
@@ -220,13 +218,11 @@ export function useSessions({ t }) {
run.status === 3; // Finished
if (hasBeenTrained) {
- // Check for existing operations
const opsCount = await getRunOperationsCount(run.id.toString());
const hasOperations =
opsCount.explainers > 0 || opsCount.predictions > 0;
if (hasOperations) {
- // Show confirmation dialog
setRunToRetrain(run);
setOperationsCount(opsCount);
setRetrainDialogOpen(true);
@@ -234,7 +230,6 @@ export function useSessions({ t }) {
}
}
- // Proceed with training if no confirmation needed
await executeTraining(run);
} catch (error) {
console.error("Error checking operations:", error);
diff --git a/DashAI/front/src/pages/results/components/ResultsDetailsLayout.jsx b/DashAI/front/src/pages/results/components/ResultsDetailsLayout.jsx
index 9782b21f3..010cfba6d 100644
--- a/DashAI/front/src/pages/results/components/ResultsDetailsLayout.jsx
+++ b/DashAI/front/src/pages/results/components/ResultsDetailsLayout.jsx
@@ -4,8 +4,6 @@ import { Tabs, Tab, Typography, Paper, Box, Button } from "@mui/material";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import CustomLayout from "../../../components/custom/CustomLayout";
import ResultsTabInfo from "./ResultsTabInfo";
-import ResultsTabParameters from "./ResultsTabParameters";
-import ResultsTabMetrics from "./ResultsTabMetrics";
import ResultsTabHyperparameters from "./ResultsTabHyperparameters";
import { getTabsResultsDetails } from "../constants/getTabsResultsDetails";
import { checkIfHaveOptimazers } from "../../../utils/schema";
@@ -50,8 +48,6 @@ function ResultsDetailsLayout({
{currentTab === 0 && (
)}
- {/* {currentTab === 1 && }
- {currentTab === 2 && } */}
{currentTab === 3 && }
{currentTab === 4 && {t("common:todo")}}
diff --git a/DashAI/front/src/utils/i18n/locales/en/common.json b/DashAI/front/src/utils/i18n/locales/en/common.json
index b5f07cf8f..5c18dd167 100644
--- a/DashAI/front/src/utils/i18n/locales/en/common.json
+++ b/DashAI/front/src/utils/i18n/locales/en/common.json
@@ -124,6 +124,11 @@
"todo": "TODO...",
"train": "Train",
"trainVerb": "Train",
+ "retrain": "Retrain",
+ "saving": "Saving...",
+ "saveChanges": "Save Changes",
+ "important": "Important",
+ "optimizerParameters": "Optimizer Parameters",
"type": "Type",
"unknown": "Unknown",
"unknownError": "Unknown error",
diff --git a/DashAI/front/src/utils/i18n/locales/en/models.json b/DashAI/front/src/utils/i18n/locales/en/models.json
index 07834e87a..24a7b2960 100644
--- a/DashAI/front/src/utils/i18n/locales/en/models.json
+++ b/DashAI/front/src/utils/i18n/locales/en/models.json
@@ -11,6 +11,8 @@
"hideParameters": "Hide Parameters",
"modifyParameters": "Modify Parameters",
"newPrediction": "New Prediction",
+ "newDatasetPrediction": "New Dataset Prediction",
+ "newManualPrediction": "New Manual Prediction",
"newSession": "New Session",
"retrain": "Re-train",
"runAll": "Run All",
@@ -18,7 +20,8 @@
"runModel": "Run Model",
"saveAndRunModel": "Save and Run Model",
"showOperations": "Show Operations",
- "showParameters": "Show Parameters"
+ "showParameters": "Show Parameters",
+ "updateAndRetrain": "Update & Retrain"
},
"error": {
"completeRequiredFields": "Please complete all required fields",
@@ -44,7 +47,11 @@
"preparingRunsTable": "Error while preparing runs table",
"runFailed": "Run {{runName}} failed: {{error}}",
"runFailedId": "Run {{runId}} failed: {{error}}",
+ "runNameEmpty": "Run name cannot be empty",
"runNameExists": "A run with the name {{name}} already exists",
+ "failedToUpdateRun": "Error updating run: {{error}}",
+ "selectGoalMetricRequired": "Please select a goal metric for optimization",
+ "selectOptimizerRequired": "Please select an optimizer for optimizable parameters",
"selectSessionFirst": "Please select a session first",
"sessionNameEmpty": "Session name cannot be empty",
"sessionNameExists": "A session with this name already exists"
@@ -58,8 +65,10 @@
"configureOptimizer": "Configure Optimizer",
"configureTasksTrainCompareModels": "Configure tasks, train and compare models in organized sessions. Select a task to begin your modeling workflow.",
"customMetrics": "Custom Metrics",
+ "datasetPredictions": "Dataset Predictions",
"editRunParameters": "Edit parameters and re-run the model",
"epoch": "Epoch",
+ "explainability": "Explainability",
"experimentResults": "Experiment {{name}} results",
"explainersCount_one": "• <1>{{count}}1> explainer",
"explainersCount_other": "• <1>{{count}}1> explainers",
@@ -67,18 +76,26 @@
"globalExplainer": "Global Explainer",
"globalExplainers": "Global Explainers",
"goalMetric": "Goal Metric",
+ "hideResults": "Hide Results",
"hyperparameterOptimizationPlots": "Hyperparameter Optimization Plots",
+ "hyperparameterOptimizerConfiguration": "Hyperparameter Optimizer Configuration",
+ "hyperparameters": "Hyperparameters",
+ "runName": "Run Name",
+ "confirmParameterUpdate": "Confirm Parameter Update",
"liveMetrics": "Live Metrics",
"localExplainer": "Local Explainer",
"localExplainers": "Local Explainers",
+ "manualPredictions": "Manual Predictions",
"metricsEmptyForDisplaySet": "The result metrics for {{set}} are empty.",
"metricToOptimize": "Metric to Optimize",
"modelComparison": "Model Comparison",
"modelsModule": "Models Module",
"nameYourSession": "Name Your Session",
"noCompatibleModelsFound": "No compatible models found",
+ "noDatasetPredictionsYet": "No dataset predictions yet",
"noGlobalExplainersYet": "No global explainers yet",
"noLocalExplainersYet": "No local explainers yet",
+ "noManualPredictionsYet": "No manual predictions yet",
"noMetricsAvailable": "No metrics available",
"noMetricsAvailableForThisView": "No metrics available for this view",
"noModelsMatchSearch": "No models match your search",
@@ -98,6 +115,9 @@
"retrainModel": "Re-train Model?",
"retrainWillDeleteOperations": "This run has existing operations that will be deleted",
"retrainWillDeleteOperationsDetails": "Re-training run \"<1>{{runName}}1>\" will delete:",
+ "saveParameterChanges": "Save Parameter Changes?",
+ "showResults": "Show Results",
+ "saveWillDeleteOperationsDetails": "Saving \"<1>{{runName}}1>\" will reset the run. The following will be deleted when you train again:",
"runDetails": "Run Details",
"runExperimentToSeeMetrics": "Go to<1>experiments tab1>to run your experiment.",
"runFailedNoHyperparameterPlots": "Run Failed. No hyperparameter plots available.",
@@ -125,6 +145,19 @@
"message": {
"allRunsCompleted": "{{experiment}} has completed all its runs.",
"confirmDeleteRun": "Are you sure you want to delete this run? This action cannot be undone.",
+ "editingParametersWarning": "Editing parameters will reset this run's status to 'Not Started'. All existing metrics and results will be lost.",
+ "aboutToUpdateParameters": "You are about to update the parameters for run {{runName}}. This action will have the following consequences:",
+ "dataWillBeDeleted": "The following data will be permanently deleted:",
+ "metricsWillBeCleared": "All existing metrics will be cleared",
+ "trainValidationTestMetrics": "Training, validation, and test metrics",
+ "trainingResultsWillBeReset": "Training results will be reset",
+ "startEndDeliveryTime": "Start time, end time, and delivery time",
+ "operationsMayBecomeInvalid": "Existing explainers and predictions may become invalid",
+ "mayNeedToRecreate": "You may need to recreate them after retraining",
+ "willBeRetrainedAfterUpdate": "The run will be automatically retrained after updating parameters.",
+ "parametersMarkedForOptimization": "Parameters marked for optimization require a hyperparameter optimizer.",
+ "retrainWillResetOperations": "Warning: This will reset {{explainersCount}} explainer(s) and {{predictionsCount}} prediction(s)",
+ "saveWillDeleteOperations": "This run has existing operations that will be deleted when you train again",
"errorEnqueueingRun": "Error enqueueing run with ID {{runId}}",
"errorFetchingRuns": "Error retrieving runs for {{experiment}}",
"noRunsToExecute": "No runs available to execute. Selected runs may already be running or completed.",
@@ -138,6 +171,7 @@
"runsStartedSuccessfully_one": "{{count}} run started successfully",
"runsStartedSuccessfully_other": "{{count}} runs started successfully",
"runStarted": "Run \"{{runName}}\" started",
+ "runUpdatedSuccess": "Run \"{{runName}}\" updated successfully. Status changed to Not Started.",
"runStartedSuccessfully": "Run {{runId}} started successfully",
"sessionCreatedSuccess": "Session successfully created.",
"sessionDeleted": "Session deleted successfully",
diff --git a/DashAI/front/src/utils/i18n/locales/es/common.json b/DashAI/front/src/utils/i18n/locales/es/common.json
index acf2e15ba..24f479d17 100644
--- a/DashAI/front/src/utils/i18n/locales/es/common.json
+++ b/DashAI/front/src/utils/i18n/locales/es/common.json
@@ -124,6 +124,11 @@
"todo": "TODO...",
"train": "Entrenamiento",
"trainVerb": "Entrenar",
+ "retrain": "Re-entrenar",
+ "saving": "Guardando...",
+ "saveChanges": "Guardar Cambios",
+ "important": "Importante",
+ "optimizerParameters": "Parámetros del Optimizador",
"type": "Tipo",
"unknown": "Desconocido",
"unknownError": "Error desconocido",
diff --git a/DashAI/front/src/utils/i18n/locales/es/models.json b/DashAI/front/src/utils/i18n/locales/es/models.json
index a99cafe19..72f80f4b1 100644
--- a/DashAI/front/src/utils/i18n/locales/es/models.json
+++ b/DashAI/front/src/utils/i18n/locales/es/models.json
@@ -11,6 +11,8 @@
"hideParameters": "Ocultar Parámetros",
"modifyParameters": "Modificar Parámetros",
"newPrediction": "Nueva Predicción",
+ "newDatasetPrediction": "Nueva Predicción de Dataset",
+ "newManualPrediction": "Nueva Predicción Manual",
"newSession": "Nueva Sesión",
"retrain": "Re-entrenar",
"runAll": "Ejecutar Todo",
@@ -18,7 +20,8 @@
"runModel": "Ejecutar Modelo",
"saveAndRunModel": "Guardar y Ejecutar Modelo",
"showOperations": "Mostrar Operaciones",
- "showParameters": "Mostrar Parámetros"
+ "showParameters": "Mostrar Parámetros",
+ "updateAndRetrain": "Actualizar y Re-entrenar"
},
"error": {
"completeRequiredFields": "Por favor complete todos los campos requeridos",
@@ -44,7 +47,11 @@
"preparingRunsTable": "Error al preparar la tabla de ejecuciones",
"runFailed": "La ejecución {{runName}} falló: {{error}}",
"runFailedId": "La ejecución {{runId}} falló: {{error}}",
+ "runNameEmpty": "El nombre de la ejecución no puede estar vacío",
"runNameExists": "Ya existe una ejecución con el nombre {{name}}",
+ "failedToUpdateRun": "Error al actualizar la ejecución: {{error}}",
+ "selectGoalMetricRequired": "Por favor seleccione una métrica objetivo para la optimización",
+ "selectOptimizerRequired": "Por favor seleccione un optimizador para los parámetros optimizables",
"selectSessionFirst": "Por favor seleccione una sesión primero",
"sessionNameEmpty": "El nombre de la sesión no puede estar vacío",
"sessionNameExists": "Ya existe una sesión con este nombre"
@@ -58,8 +65,10 @@
"configureOptimizer": "Configurar Optimizador",
"configureTasksTrainCompareModels": "Configure tareas, entrene y compare modelos en sesiones organizadas. Seleccione una tarea para comenzar su flujo de trabajo de modelado.",
"customMetrics": "Métricas Personalizadas",
+ "datasetPredictions": "Predicciones de Dataset",
"editRunParameters": "Editar parámetros y volver a ejecutar el modelo",
"epoch": "Época",
+ "explainability": "Explicabilidad",
"experimentResults": "Resultados del experimento {{name}}",
"explainersCount_one": "• <1>{{count}}1> explicador",
"explainersCount_other": "• <1>{{count}}1> explicadores",
@@ -67,18 +76,26 @@
"globalExplainer": "Explicador Global",
"globalExplainers": "Explicadores Globales",
"goalMetric": "Métrica Objetivo",
+ "hideResults": "Ocultar Resultados",
"hyperparameterOptimizationPlots": "Gráficos de Optimización de Hiperparámetros",
+ "hyperparameterOptimizerConfiguration": "Configuración del Optimizador de Hiperparámetros",
+ "hyperparameters": "Hiperparámetros",
+ "runName": "Nombre de la Ejecución",
+ "confirmParameterUpdate": "Confirmar Actualización de Parámetros",
"liveMetrics": "Métricas en Vivo",
"localExplainer": "Explicador Local",
"localExplainers": "Explicadores Locales",
+ "manualPredictions": "Predicciones Manuales",
"metricsEmptyForDisplaySet": "Las métricas de resultado para {{set}} están vacías.",
"metricToOptimize": "Métrica a Optimizar",
"modelComparison": "Comparación de Modelos",
"modelsModule": "Módulo de Modelos",
"nameYourSession": "Nombre su Sesión",
"noCompatibleModelsFound": "No se encontraron modelos compatibles",
+ "noDatasetPredictionsYet": "Aún no hay predicciones de dataset",
"noGlobalExplainersYet": "Aún no hay explicadores globales",
"noLocalExplainersYet": "Aún no hay explicadores locales",
+ "noManualPredictionsYet": "Aún no hay predicciones manuales",
"noMetricsAvailable": "No hay métricas disponibles",
"noMetricsAvailableForThisView": "No hay métricas disponibles para esta vista",
"noModelsMatchSearch": "No hay modelos que coincidan con su búsqueda",
@@ -98,6 +115,9 @@
"retrainModel": "¿Re-entrenar Modelo?",
"retrainWillDeleteOperations": "Esta ejecución tiene operaciones existentes que serán eliminadas",
"retrainWillDeleteOperationsDetails": "Re-entrenar la ejecución \"<1>{{runName}}1>\" eliminará:",
+ "saveParameterChanges": "¿Guardar Cambios de Parámetros?",
+ "showResults": "Mostrar Resultados",
+ "saveWillDeleteOperationsDetails": "Guardar \"<1>{{runName}}1>\" restablecerá la ejecución. Lo siguiente se eliminará cuando vuelva a entrenar:",
"runDetails": "Detalles de la Ejecución",
"runExperimentToSeeMetrics": "Ve a la <1>pestaña de experimentos1> para ejecutar tu experimento.",
"runFailedNoHyperparameterPlots": "Ejecución Fallida. No hay gráficos de hiperparámetros disponibles.",
@@ -125,6 +145,19 @@
"message": {
"allRunsCompleted": "{{experiment}} ha completado todas sus ejecuciones.",
"confirmDeleteRun": "¿Está seguro de que desea eliminar esta ejecución? Esta acción no se puede deshacer.",
+ "editingParametersWarning": "Al editar los parámetros se restablecerá el estado de esta ejecución a 'No Iniciado'. Todas las métricas y resultados existentes se perderán.",
+ "aboutToUpdateParameters": "Está a punto de actualizar los parámetros para la ejecución {{runName}}. Esta acción tendrá las siguientes consecuencias:",
+ "dataWillBeDeleted": "Los siguientes datos serán eliminados permanentemente:",
+ "metricsWillBeCleared": "Todas las métricas existentes serán eliminadas",
+ "trainValidationTestMetrics": "Métricas de entrenamiento, validación y prueba",
+ "trainingResultsWillBeReset": "Los resultados de entrenamiento serán reiniciados",
+ "startEndDeliveryTime": "Tiempo de inicio, fin y entrega",
+ "operationsMayBecomeInvalid": "Los explicadores y predicciones existentes pueden quedar inválidos",
+ "mayNeedToRecreate": "Es posible que deba recrearlos después de re-entrenar",
+ "willBeRetrainedAfterUpdate": "La ejecución se re-entrenará automáticamente después de actualizar los parámetros.",
+ "parametersMarkedForOptimization": "Los parámetros marcados para optimización requieren un optimizador de hiperparámetros.",
+ "retrainWillResetOperations": "Advertencia: Esto restablecerá {{explainersCount}} explicador(es) y {{predictionsCount}} predicción(es)",
+ "saveWillDeleteOperations": "Esta ejecución tiene operaciones existentes que se eliminarán al volver a entrenar",
"errorEnqueueingRun": "Error al encolar ejecución con ID {{runId}}",
"errorFetchingRuns": "Error al recuperar ejecuciones para {{experiment}}",
"noRunsToExecute": "No hay ejecuciones disponibles para ejecutar. Las ejecuciones seleccionadas pueden estar ya en ejecución o completadas.",
@@ -139,6 +172,7 @@
"runsStartedSuccessfully_many": "{{count}} ejecuciones iniciadas exitosamente",
"runsStartedSuccessfully_other": "{{count}} ejecuciones iniciadas exitosamente",
"runStarted": "Ejecución \"{{runName}}\" iniciada",
+ "runUpdatedSuccess": "Ejecución \"{{runName}}\" actualizada correctamente. Estado cambiado a No Iniciado.",
"runStartedSuccessfully": "Ejecución {{runId}} iniciada exitosamente",
"sessionCreatedSuccess": "Sesión creada exitosamente.",
"sessionDeleted": "Sesión eliminada exitosamente",