From 8baa961c4b1b3d020f29cc863b2954b8faf7b14e Mon Sep 17 00:00:00 2001 From: Felipe Date: Sun, 25 Jan 2026 22:52:57 -0300 Subject: [PATCH 01/15] feat: Add EditRunDialog component for editing model run parameters - 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. --- .../explainers/ExplanainersCard.jsx | 36 +- .../models/EditConfirmationDialog.jsx | 124 +++++ .../src/components/models/EditRunDialog.jsx | 362 +++++++++++++ .../src/components/models/PredictionCard.jsx | 15 +- .../models/PredictionCreationDialog.jsx | 28 +- .../front/src/components/models/RunCard.jsx | 334 ++++++------ .../src/components/models/RunOperations.jsx | 507 ++++++++++++------ .../threeSectionLayout/BarHeader.jsx | 12 +- .../components/threeSectionLayout/Footer.jsx | 10 +- .../front/src/pages/models/ModelsContent.jsx | 92 +++- 10 files changed, 1129 insertions(+), 391 deletions(-) create mode 100644 DashAI/front/src/components/models/EditConfirmationDialog.jsx create mode 100644 DashAI/front/src/components/models/EditRunDialog.jsx diff --git a/DashAI/front/src/components/explainers/ExplanainersCard.jsx b/DashAI/front/src/components/explainers/ExplanainersCard.jsx index f267629fd..977828b1c 100644 --- a/DashAI/front/src/components/explainers/ExplanainersCard.jsx +++ b/DashAI/front/src/components/explainers/ExplanainersCard.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Grid, Typography, @@ -34,7 +34,18 @@ 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; + }); + + // Persist expanded state + useEffect(() => { + localStorage.setItem( + `explainer-${explainer.id}-expanded`, + JSON.stringify(expanded), + ); + }, [expanded, explainer.id]); function plotName(name) { return name.match(/[A-Z][a-z]+|[0-9]+/g).join(" "); @@ -73,11 +84,24 @@ export default function ExplainersCard({ alignItems="center" > - + {plotName(explainer.explainer_name)} - - - {explainer.name} + + {explainer.name} + diff --git a/DashAI/front/src/components/models/EditConfirmationDialog.jsx b/DashAI/front/src/components/models/EditConfirmationDialog.jsx new file mode 100644 index 000000000..7e354af19 --- /dev/null +++ b/DashAI/front/src/components/models/EditConfirmationDialog.jsx @@ -0,0 +1,124 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + DialogContentText, + Button, + Alert, + AlertTitle, + Box, + List, + ListItem, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import { + Warning as WarningIcon, + DeleteSweep as DeleteSweepIcon, + RestartAlt as RestartAltIcon, +} from "@mui/icons-material"; + +/** + * Confirmation dialog for editing run parameters + * Warns user about consequences of overwriting existing model + */ +function EditConfirmationDialog({ + open, + onClose, + onConfirm, + run, + hasOperations = false, +}) { + if (!run) { + return null; + } + + return ( + + Confirm Parameter Update + + + You are about to update the parameters for run{" "} + {run.name}. This action will have the following + consequences: + + + + Important + The following data will be permanently deleted: + + + + + + + + + + + + + + + + {hasOperations && ( + + + + + + + )} + + + + + + + The run will be automatically retrained after updating parameters. + + + + + + + + + + + ); +} + +EditConfirmationDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + run: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + }), + hasOperations: PropTypes.bool, +}; + +export default EditConfirmationDialog; diff --git a/DashAI/front/src/components/models/EditRunDialog.jsx b/DashAI/front/src/components/models/EditRunDialog.jsx new file mode 100644 index 000000000..8a1d2ca9a --- /dev/null +++ b/DashAI/front/src/components/models/EditRunDialog.jsx @@ -0,0 +1,362 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import PropTypes from "prop-types"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Stepper, + Step, + StepLabel, + TextField, + Box, + IconButton, + Typography, +} from "@mui/material"; +import { Close as CloseIcon } from "@mui/icons-material"; +import { useSnackbar } from "notistack"; +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"; + +/** + * Dialog for editing an existing model run parameters + * Step 1: Configure model name and parameters + * Step 2: Configure optimizer (if optimizable parameters exist) + */ +function EditRunDialog({ + open, + onClose, + session, + run, + existingRuns = [], + onRunUpdated, + onConfirmEdit, +}) { + const { enqueueSnackbar } = useSnackbar(); + const [activeStep, setActiveStep] = useState(0); + const [name, setName] = useState(""); + const [modelParameters, setModelParameters] = useState({}); + const [selectedOptimizer, setSelectedOptimizer] = useState(""); + const [optimizerParameters, setOptimizerParameters] = useState({}); + const [loading, setLoading] = useState(false); + const [hasUserTouchedName, setHasUserTouchedName] = useState(false); + const [goalMetric, setGoalMetric] = useState(""); + const [hasLoadedInitialParams, setHasLoadedInitialParams] = useState(false); + + const { defaultValues: defaultOptimizerParams } = useSchema({ + modelName: selectedOptimizer, + }); + + // Initialize form with existing run data + useEffect(() => { + if (open && run) { + setName(run.name || ""); + setModelParameters(run.parameters || {}); + setSelectedOptimizer(run.optimizer_name || ""); + setOptimizerParameters(run.optimizer_parameters || {}); + setGoalMetric(run.goal_metric || ""); + setHasLoadedInitialParams(true); + setHasUserTouchedName(false); + setActiveStep(0); + } + }, [open, run]); + + const hasOptimizableParams = useMemo(() => { + return Object.values(modelParameters).some( + (value) => + value && + typeof value === "object" && + !Array.isArray(value) && + value.optimize === true, + ); + }, [modelParameters]); + + const steps = hasOptimizableParams + ? ["Configure Model", "Configure Optimizer"] + : ["Configure Model"]; + + const handleModelParametersChange = useCallback((values) => { + setModelParameters(values); + }, []); + + const handleOptimizerParametersChange = useCallback((values) => { + setOptimizerParameters((prevParams) => ({ ...prevParams, ...values })); + }, []); + + useEffect(() => { + if ( + defaultOptimizerParams && + Object.keys(defaultOptimizerParams).length > 0 && + !hasLoadedInitialParams + ) { + setOptimizerParameters((prev) => { + const prevKeys = Object.keys(prev).sort().join(","); + const newKeys = Object.keys(defaultOptimizerParams).sort().join(","); + if ( + prevKeys === newKeys && + JSON.stringify(prev) === JSON.stringify(defaultOptimizerParams) + ) { + return prev; + } + return defaultOptimizerParams; + }); + } + }, [defaultOptimizerParams, hasLoadedInitialParams]); + + const handleClose = () => { + setTimeout(() => { + setActiveStep(0); + setName(""); + setModelParameters({}); + setSelectedOptimizer(""); + setOptimizerParameters({}); + setGoalMetric(""); + setHasUserTouchedName(false); + setHasLoadedInitialParams(false); + }, 100); + onClose(); + }; + + const handleNext = () => { + if (activeStep === 0) { + if (name.trim() === "") { + enqueueSnackbar("Please enter a name for the model", { + variant: "warning", + }); + return; + } + + // Check for duplicate names (excluding current run) + const nameExists = existingRuns.some( + (r) => + r.id !== run.id && + r.name && + r.name.toLowerCase() === name.trim().toLowerCase(), + ); + if (nameExists) { + enqueueSnackbar("A run with this name already exists", { + variant: "error", + }); + return; + } + + if (hasOptimizableParams) { + setActiveStep(1); + } else { + handleConfirmUpdate(); + } + } else { + handleConfirmUpdate(); + } + }; + + const handleBack = () => { + if (activeStep > 0) { + setActiveStep(activeStep - 1); + } + }; + + const handleConfirmUpdate = () => { + if (onConfirmEdit) { + // Pass the updated data to the confirmation dialog + onConfirmEdit({ + runId: run.id, + name: name.trim(), + parameters: modelParameters, + optimizer: selectedOptimizer || "", + optimizer_parameters: optimizerParameters || {}, + goal_metric: goalMetric || "", + }); + } + handleClose(); + }; + + const handleOptimizerSelected = (optimizerName, defaultValues) => { + setSelectedOptimizer(optimizerName); + if (defaultValues && Object.keys(defaultValues).length > 0) { + setOptimizerParameters(defaultValues); + } + }; + + const isStep1Valid = Boolean(run?.model_name && name.trim() !== ""); + const isStep2Valid = Boolean( + selectedOptimizer && + optimizerParameters && + Object.keys(optimizerParameters).length > 0 && + goalMetric, + ); + + if (!run) { + return null; + } + + return ( + + + + Edit Model Parameters + + + + + + + + + {steps.map((label) => ( + + {label} + + ))} + + + {activeStep === 0 && ( + + { + setName(e.target.value); + setHasUserTouchedName(true); + }} + fullWidth + required + placeholder="Model Name" + helperText={run.model_name ? `Model: ${run.model_name}` : ""} + /> + + {run.model_name && ( + + + Model Parameters + + + {}} + hideButtons + /> + + + )} + + )} + + {activeStep === 1 && ( + + + Configure Hyperparameter Optimizer + + + + + Goal Metric * + + + + + + + {selectedOptimizer && ( + + + Optimizer Parameters + + + setOptimizerParameters(values)} + onValuesChange={handleOptimizerParametersChange} + onCancel={() => {}} + hideButtons + /> + + + )} + + )} + + + + + {activeStep > 0 && ( + + )} + + + + ); +} + +EditRunDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + session: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + task_name: PropTypes.string, + }), + run: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + model_name: PropTypes.string, + parameters: PropTypes.object, + optimizer_name: PropTypes.string, + optimizer_parameters: PropTypes.object, + goal_metric: PropTypes.string, + }), + existingRuns: PropTypes.array, + onRunUpdated: PropTypes.func, + onConfirmEdit: PropTypes.func, +}; + +export default EditRunDialog; diff --git a/DashAI/front/src/components/models/PredictionCard.jsx b/DashAI/front/src/components/models/PredictionCard.jsx index 039f5be1b..bdd2af4d3 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, @@ -37,10 +37,21 @@ const RUNNING_STATUSES = ["Delivered", "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(); + // Persist expanded state + useEffect(() => { + localStorage.setItem( + `prediction-${prediction.id}-expanded`, + JSON.stringify(expanded), + ); + }, [expanded, prediction.id]); + const statusText = getPredictionStatus(prediction.status); // Status color mapping diff --git a/DashAI/front/src/components/models/PredictionCreationDialog.jsx b/DashAI/front/src/components/models/PredictionCreationDialog.jsx index 1a584c131..8c3859e88 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"; @@ -30,7 +29,7 @@ import { useSnackbar } from "notistack"; import { startJobPolling } from "../../utils/jobPoller"; import { getPredictions } from "../../api/predict"; -const steps = ["Select Mode", "Configure Input", "Confirm"]; +const steps = ["Configure Input", "Confirm"]; /** * PredictionCreationDialog - Wizard for creating new predictions @@ -41,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([]); @@ -59,13 +59,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 () => { @@ -203,19 +203,6 @@ export default function PredictionCreationDialog({ const renderStepContent = (step) => { switch (step) { case 0: - return ( - - - Select how you want to provide input data for prediction - - - - ); - - case 1: return ( {predictionMode === "dataset" ? ( @@ -256,7 +243,7 @@ export default function PredictionCreationDialog({ ); - case 2: + case 1: return ( @@ -323,7 +310,7 @@ export default function PredictionCreationDialog({ ))} - {loadingExperiment && activeStep === 1 ? ( + {loadingExperiment && activeStep === 0 ? ( - {/* Run Name and Status */} + {/* Header: Model Name (User Name) with Status and Actions */} - - {run.name} - - - + {/* Left: Model Name and User Name with Gear */} + + + setExpanded(!expanded)} + color={expanded ? "primary" : "default"} + > + + + + + {modelDisplayName} + + ({run.name}) + + + - {/* Model */} - - - - {modelDisplayName} - + {/* Right: Actions and Status */} + + {/* Train/Re-train Button */} + {canTrain && ( + + )} + {isRunning && ( + + )} + + {/* Status Chip */} + + + {/* Delete Button */} + + onDelete(run)} + disabled={isRunning} + > + + + + {/* Metrics Summary */} @@ -186,98 +251,87 @@ function RunCard({ )} {/* Expandable Details */} - - - - - - {/* Model Parameters */} - {run.parameters && Object.keys(run.parameters).length > 0 && ( - - - Model Parameters - - - - - - Parameter - Value + + + {/* Model Parameters */} + {run.parameters && Object.keys(run.parameters).length > 0 && ( + + + Model Parameters + + +
+ + + Parameter + 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)} + - - - {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 && ( - - - Optimizer: {run.optimizer_name} - - {run.optimizer_parameters && - Object.keys(run.optimizer_parameters).length > 0 && ( - - - - - Parameter - Value - - - - {Object.entries(run.optimizer_parameters).map( - ([key, value]) => ( - - {key} - - {typeof value === "object" - ? JSON.stringify(value) - : String(value)} - - - ), - )} - -
-
- )} -
- )} + {/* Optimizer Configuration */} + {run.optimizer_name && ( + + + Optimizer: {run.optimizer_name} + + {run.optimizer_parameters && + Object.keys(run.optimizer_parameters).length > 0 && ( + + + + + Parameter + Value + + + + {Object.entries(run.optimizer_parameters).map( + ([key, value]) => ( + + {key} + + {typeof value === "object" + ? JSON.stringify(value) + : String(value)} + + + ), + )} + +
+
+ )} +
+ )} - {/* Goal Metric */} - {run.goal_metric && ( - - - Goal Metric: {run.goal_metric} - - - )} -
- -
+ {/* Goal Metric */} + {run.goal_metric && ( + + + Goal Metric: {run.goal_metric} + + + )} +
+ {/* RunOperations - Separate section for finished runs */} {statusText === "Finished" && ( @@ -295,25 +349,6 @@ function RunCard({ - {/* Train/Stop button */} - {isRunning && ( - - )} - {canTrain && ( - - )} - {/* Edit button */} - - {/* Prediction button */} - {statusText === "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="Create prediction" - > - - - )} - - {/* Explainer button */} - {onExplainer && statusText === "Finished" && ( - onExplainer(run)} - color="primary" - title="Create explainer" - > - - - )} - - {/* Delete button */} - onDelete(run)} - color="error" - disabled={isRunning} - title="Delete run" - > - - ); diff --git a/DashAI/front/src/components/models/RunOperations.jsx b/DashAI/front/src/components/models/RunOperations.jsx index 2bb21bdc2..bce1b94a6 100644 --- a/DashAI/front/src/components/models/RunOperations.jsx +++ b/DashAI/front/src/components/models/RunOperations.jsx @@ -11,6 +11,9 @@ import { Stack, CircularProgress, Collapse, + Tabs, + Tab, + Grid, } from "@mui/material"; import { ExpandMore as ExpandMoreIcon, @@ -41,17 +44,21 @@ export default function RunOperations({ const [localExplainers, setLocalExplainers] = useState([]); const [predictions, setPredictions] = useState([]); const [loading, setLoading] = useState(true); - const [operationsVisible, setOperationsVisible] = useState(false); + const [operationsVisible, setOperationsVisible] = useState(() => { + const saved = localStorage.getItem(`run-${run.id}-operations-visible`); + return saved !== null ? JSON.parse(saved) : false; + }); + const [activeTab, setActiveTab] = useState(() => { + const saved = localStorage.getItem(`run-${run.id}-active-tab`); + return saved !== null ? JSON.parse(saved) : 0; + }); // 0: Explainability, 1: Predictions 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 [datasetPredictionDialogOpen, setDatasetPredictionDialogOpen] = + useState(false); + const [manualPredictionDialogOpen, setManualPredictionDialogOpen] = + useState(false); const fetchOperations = useCallback(async () => { if (!run || !run.id) return; @@ -82,7 +89,7 @@ export default function RunOperations({ useEffect(() => { const handleOpenDialog = (event) => { if (event.detail.runId === run.id) { - setPredictionDialogOpen(true); + setDatasetPredictionDialogOpen(true); } }; window.addEventListener("openPredictionDialog", handleOpenDialog); @@ -90,12 +97,18 @@ export default function RunOperations({ window.removeEventListener("openPredictionDialog", handleOpenDialog); }, [run.id]); - const handleAccordionChange = (section) => (event, isExpanded) => { - setExpandedSections((prev) => ({ - ...prev, - [section]: isExpanded, - })); - }; + // Persist operationsVisible state + useEffect(() => { + localStorage.setItem( + `run-${run.id}-operations-visible`, + JSON.stringify(operationsVisible), + ); + }, [operationsVisible, run.id]); + + // Persist activeTab state + useEffect(() => { + localStorage.setItem(`run-${run.id}-active-tab`, JSON.stringify(activeTab)); + }, [activeTab, run.id]); const handleExplainerCreated = () => { fetchOperations(); @@ -147,161 +160,311 @@ export default function RunOperations({ - {/* Global Explainers Section */} - - }> - - - Global Explainers - - - - - - - - {globalExplainers.length === 0 ? ( - - No global explainers yet - - ) : ( - globalExplainers.map((explainer) => ( - + setActiveTab(newValue)} + aria-label="Operations tabs" + > + + Explainability + - )) - )} - - - + + } + /> + + Predictions + + + } + /> + + - {/* Local Explainers Section */} - - }> - - - Local Explainers - - - - - - - - {localExplainers.length === 0 ? ( - + + {/* Global Explainers Column */} + + - No local explainers yet - - ) : ( - localExplainers.map((explainer) => ( - - )) - )} - - - + + + + Global Explainers + + + + + + + {globalExplainers.length === 0 ? ( + + No global explainers yet + + ) : ( + globalExplainers.map((explainer) => ( + + )) + )} + + +
- {/* Predictions Section */} - - }> - - - Predictions - - - - - - - - {predictions.length === 0 ? ( - + - No predictions yet - - ) : ( - predictions.map((prediction) => ( - - )) - )} - - - + + + + Local Explainers + + + + + + + {localExplainers.length === 0 ? ( + + No local explainers yet + + ) : ( + localExplainers.map((explainer) => ( + + )) + )} + + + + + + )} + + {/* Tab Panel 1: Predictions */} + {activeTab === 1 && ( + + + {/* Dataset Predictions Column */} + + + + + + Dataset Predictions + + p.dataset_id).length} + size="small" + color="primary" + /> + + + + + {predictions.filter((p) => p.dataset_id).length === 0 ? ( + + No dataset predictions yet + + ) : ( + predictions + .filter((p) => p.dataset_id) + .map((prediction) => ( + + )) + )} + + + + + {/* Manual Predictions Column */} + + + + + + Manual Predictions + + !p.dataset_id).length} + size="small" + color="primary" + /> + + + + + {predictions.filter((p) => !p.dataset_id).length === 0 ? ( + + No manual predictions yet + + ) : ( + predictions + .filter((p) => !p.dataset_id) + .map((prediction) => ( + + )) + )} + + + + + + )} {/* Dialogs */} @@ -327,11 +490,21 @@ export default function RunOperations({ /> setPredictionDialogOpen(false)} + open={datasetPredictionDialogOpen} + onClose={() => setDatasetPredictionDialogOpen(false)} + run={run} + session={session} + onPredictionCreated={handlePredictionCreated} + defaultMode="dataset" + /> + + setManualPredictionDialogOpen(false)} run={run} session={session} onPredictionCreated={handlePredictionCreated} + defaultMode="manual" /> ); diff --git a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx index e146d28ab..1e0081c0f 100644 --- a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx +++ b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx @@ -9,16 +9,6 @@ export default function BarHeader() { height={"70px"} px={2} py={1.5} - > - - Dash - - + > ); } diff --git a/DashAI/front/src/components/threeSectionLayout/Footer.jsx b/DashAI/front/src/components/threeSectionLayout/Footer.jsx index bd635ee3d..b98d2339c 100644 --- a/DashAI/front/src/components/threeSectionLayout/Footer.jsx +++ b/DashAI/front/src/components/threeSectionLayout/Footer.jsx @@ -8,14 +8,6 @@ export default function Footer() { alignItems={"center"} flexDirection={"column"} py={2} - > - - - + > ); } diff --git a/DashAI/front/src/pages/models/ModelsContent.jsx b/DashAI/front/src/pages/models/ModelsContent.jsx index de83b1b14..0a124c4b6 100644 --- a/DashAI/front/src/pages/models/ModelsContent.jsx +++ b/DashAI/front/src/pages/models/ModelsContent.jsx @@ -15,6 +15,8 @@ import CreateSessionSteps from "../../components/models/CreateSessionSteps"; import SessionVisualization from "../../components/models/SessionVisualization"; import DatasetVisualization from "../../components/DatasetVisualization"; import AddModelDialog from "../../components/models/AddModelDialog"; +import EditRunDialog from "../../components/models/EditRunDialog"; +import EditConfirmationDialog from "../../components/models/EditConfirmationDialog"; import RetrainConfirmDialog from "../../components/models/RetrainConfirmDialog"; import { getComponents } from "../../api/component"; import { @@ -35,6 +37,7 @@ import { getRunById, getRunOperationsCount, deleteRunOperations, + updateRunParameters, } from "../../api/run"; import { enqueueRunnerJob as enqueueRunnerJobRequest } from "../../api/job"; import { startJobPolling } from "../../utils/jobPoller"; @@ -63,6 +66,11 @@ export default function ModelsContent() { const [runToRetrain, setRunToRetrain] = useState(null); const [operationsCount, setOperationsCount] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [runToEdit, setRunToEdit] = useState(null); + const [editConfirmDialogOpen, setEditConfirmDialogOpen] = useState(false); + const [pendingEditData, setPendingEditData] = useState(null); + const isResizingLeft = useRef(false); const isResizingRight = useRef(false); const [isTogglingLeft, setIsTogglingLeft] = useState(false); @@ -665,7 +673,70 @@ export default function ModelsContent() { }; const handleEditRun = async (run) => { - enqueueSnackbar("Edit functionality coming soon", { variant: "info" }); + setRunToEdit(run); + setEditDialogOpen(true); + }; + + const handleConfirmEdit = async (editData) => { + // Check if run has operations (explainers/predictions) + try { + const count = await getRunOperationsCount(editData.runId.toString()); + const hasOperations = count.explainers > 0 || count.predictions > 0; + + setPendingEditData({ ...editData, hasOperations }); + setEditConfirmDialogOpen(true); + } catch (error) { + console.error("Error checking operations:", error); + // Proceed without operations check + setPendingEditData({ ...editData, hasOperations: false }); + setEditConfirmDialogOpen(true); + } + }; + + const handleExecuteEdit = async () => { + if (!pendingEditData) return; + + try { + setEditConfirmDialogOpen(false); + + // Update run parameters via API + await updateRunParameters( + pendingEditData.runId.toString(), + pendingEditData.parameters, + pendingEditData.optimizer, + pendingEditData.optimizer_parameters, + pendingEditData.goal_metric, + ); + + // Fetch updated run + const updatedRun = await getRunById(pendingEditData.runId.toString()); + + // Update runs list + setRuns((prev) => + prev.map((r) => (r.id === updatedRun.id ? updatedRun : r)), + ); + + enqueueSnackbar(`Run "${pendingEditData.name}" updated successfully`, { + variant: "success", + }); + + // Auto-retrain the updated run + await executeTraining(updatedRun); + + setPendingEditData(null); + } catch (error) { + console.error("Error updating run:", error); + enqueueSnackbar( + `Error updating run: ${error.message || "Unknown error"}`, + { variant: "error" }, + ); + setPendingEditData(null); + } + }; + + const handleCancelEdit = () => { + setEditConfirmDialogOpen(false); + setPendingEditData(null); }; const handleDeleteRun = async (run) => { @@ -1047,6 +1118,25 @@ export default function ModelsContent() { run={runToRetrain} operationsCount={operationsCount} /> + + {/* Edit Run Dialog */} + setEditDialogOpen(false)} + session={selectedSession} + run={runToEdit} + existingRuns={runs} + onConfirmEdit={handleConfirmEdit} + /> + + {/* Edit Confirmation Dialog */} + {!selectedSessionId && ( Date: Sun, 25 Jan 2026 23:28:56 -0300 Subject: [PATCH 02/15] feat: Enhance RunCard component with editing capabilities for model run parameters --- .../front/src/components/models/RunCard.jsx | 532 ++++++++++++++---- .../src/components/models/RunOperations.jsx | 2 +- .../models/SessionVisualization.jsx | 98 +--- .../front/src/pages/models/ModelsContent.jsx | 116 +--- 4 files changed, 453 insertions(+), 295 deletions(-) diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index f48140cad..38f471aac 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react"; import PropTypes from "prop-types"; import { Card, @@ -19,21 +19,27 @@ import { Paper, Divider, Tooltip, - Grid, + TextField, + Alert, } from "@mui/material"; import { PlayArrow, Stop, Edit, Delete, - ExpandMore, - ExpandLess, Settings, - TrendingUp, - QueryStats, + Save, + Cancel, } from "@mui/icons-material"; +import { useSnackbar } from "notistack"; import { getRunStatus } from "../../utils/runStatus"; import RunOperations from "./RunOperations"; +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"; /** * Card component displaying a model run with actions and details @@ -43,23 +49,188 @@ function RunCard({ models = [], session, onTrain, - onEdit, - onExplainer, onDelete, onOperationsRefresh, explainerRefreshTrigger, isLastRun = false, + existingRuns = [], + onRefresh, }) { + 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 { 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 handleSaveEdit = async () => { + if (!editedName.trim()) { + enqueueSnackbar("Run name cannot be empty", { variant: "warning" }); + return; + } + + const nameExists = existingRuns.some( + (r) => + r.id !== run.id && + r.name && + r.name.toLowerCase() === editedName.trim().toLowerCase(), + ); + if (nameExists) { + enqueueSnackbar("A run with this name already exists", { + variant: "error", + }); + return; + } + + if (hasOptimizableParams) { + if (!editedOptimizer) { + enqueueSnackbar( + "Please select an optimizer for optimizable parameters", + { variant: "warning" }, + ); + return; + } + if (!editedGoalMetric) { + enqueueSnackbar("Please select a goal metric for optimization", { + variant: "warning", + }); + return; + } + } + + setIsSaving(true); + try { + await updateRunParameters( + run.id.toString(), + editedParameters, + editedOptimizer || "", + editedOptimizerParams || {}, + editedGoalMetric || "", + ); + + enqueueSnackbar( + `Run "${editedName}" updated successfully. Status changed to Not Started.`, + { + variant: "success", + }, + ); + + setIsEditing(false); + + if (onRefresh) { + await onRefresh(); + } + } catch (error) { + console.error("Error updating run:", error); + enqueueSnackbar( + `Error updating run: ${error.message || "Unknown error"}`, + { + variant: "error", + }, + ); + } finally { + setIsSaving(false); + } + }; + + 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 display status from numeric code const statusText = getRunStatus(run.status); - // Get model display name 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 "Not Started": @@ -76,21 +247,12 @@ function RunCard({ } }; - // Check if run can be trained const canTrain = statusText === "Not Started" || statusText === "Error" || statusText === "Finished"; const isRunning = statusText === "Delivered" || statusText === "Started"; - // Format date - const formatDate = (dateString) => { - if (!dateString) return "N/A"; - const date = new Date(dateString); - return date.toLocaleString(); - }; - - // Get metrics from run const getMetrics = () => { if (!run.trained_models || run.trained_models.length === 0) { return null; @@ -128,7 +290,6 @@ function RunCard({ }} > - {/* Header: Model Name (User Name) with Status and Actions */} - {/* Left: Model Name and User Name with Gear */} setExpanded(!expanded)} color={expanded ? "primary" : "default"} + disabled={isEditing} > @@ -170,20 +331,76 @@ function RunCard({ - {/* Right: Actions and Status */} + {/* Edit Mode - Save and Cancel Buttons */} + {isEditing && ( + <> + + + + )} + + {/* Edit Button */} + {!isEditing && + statusText !== "Delivered" && + statusText !== "Started" && ( + + + + + + )} {/* Train/Re-train Button */} {canTrain && ( - + + )} {isRunning && ( + + + + ) : ( + // VIEW MODE - - Optimizer: {run.optimizer_name} - - {run.optimizer_parameters && - Object.keys(run.optimizer_parameters).length > 0 && ( + {/* Model Parameters */} + {run.parameters && Object.keys(run.parameters).length > 0 && ( + + + Model Parameters + @@ -303,13 +610,15 @@ function RunCard({ - {Object.entries(run.optimizer_parameters).map( + {Object.entries(run.parameters).map( ([key, value]) => ( {key} - {typeof value === "object" - ? JSON.stringify(value) + {typeof value === "object" && value !== null + ? value.fixed_value !== undefined + ? String(value.fixed_value) + : JSON.stringify(value) : String(value)} @@ -318,16 +627,53 @@ function RunCard({
- )} -
- )} +
+ )} - {/* Goal Metric */} - {run.goal_metric && ( - - - Goal Metric: {run.goal_metric} - + {/* Optimizer Configuration */} + {run.optimizer_name && ( + + + Optimizer: {run.optimizer_name} + + {run.optimizer_parameters && + Object.keys(run.optimizer_parameters).length > 0 && ( + + + + + Parameter + Value + + + + {Object.entries(run.optimizer_parameters).map( + ([key, value]) => ( + + {key} + + {typeof value === "object" + ? JSON.stringify(value) + : String(value)} + + + ), + )} + +
+
+ )} +
+ )} + + {/* Goal Metric */} + {run.goal_metric && ( + + + Goal Metric: {run.goal_metric} + + + )}
)} @@ -345,21 +691,6 @@ function RunCard({ )}
- - - - - {/* Edit button */} - onEdit(run)} - color="primary" - disabled={isRunning} - title="Edit parameters" - > - - - ); } @@ -386,11 +717,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 index bce1b94a6..3dc127386 100644 --- a/DashAI/front/src/components/models/RunOperations.jsx +++ b/DashAI/front/src/components/models/RunOperations.jsx @@ -51,7 +51,7 @@ export default function RunOperations({ const [activeTab, setActiveTab] = useState(() => { const saved = localStorage.getItem(`run-${run.id}-active-tab`); return saved !== null ? JSON.parse(saved) : 0; - }); // 0: Explainability, 1: Predictions + }); const [globalDialogOpen, setGlobalDialogOpen] = useState(false); const [localDialogOpen, setLocalDialogOpen] = useState(false); diff --git a/DashAI/front/src/components/models/SessionVisualization.jsx b/DashAI/front/src/components/models/SessionVisualization.jsx index aa0668b0f..834039c02 100644 --- a/DashAI/front/src/components/models/SessionVisualization.jsx +++ b/DashAI/front/src/components/models/SessionVisualization.jsx @@ -8,10 +8,6 @@ import { Divider, Button, ButtonGroup, - Dialog, - DialogTitle, - DialogContent, - DialogActions, ToggleButtonGroup, ToggleButton, } from "@mui/material"; @@ -22,15 +18,13 @@ 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"; export default function SessionVisualization({ session, runs = [], onTrain, - onEditRun, onDeleteRun, + onRefreshRuns, }) { const [models, setModels] = useState([]); const [selectedRunId, setSelectedRunId] = useState(null); @@ -38,11 +32,6 @@ export default function SessionVisualization({ 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); @@ -92,25 +81,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], @@ -392,11 +362,11 @@ export default function SessionVisualization({ models={models} session={session} onTrain={onTrain} - onEdit={onEditRun} - onExplainer={handleExplainer} onDelete={onDeleteRun} explainerRefreshTrigger={explainerRefreshTrigger} isLastRun={index === sortedRuns.length - 1} + existingRuns={runs} + onRefresh={onRefreshRuns} /> ))} @@ -405,66 +375,6 @@ export default function SessionVisualization({ - {/* Explainer Type Selection Dialog */} - - Select Explainer Type - - - - - - - - - - - - {/* Global Explainer Modal */} - { - setExplainerRefreshTrigger((prev) => prev + 1); - }} - /> - - {/* Local Explainer Modal */} - { - setExplainerRefreshTrigger((prev) => prev + 1); - }} - /> - ); @@ -478,6 +388,6 @@ SessionVisualization.propTypes = { }), runs: PropTypes.array, onTrain: PropTypes.func.isRequired, - onEditRun: PropTypes.func.isRequired, onDeleteRun: PropTypes.func.isRequired, + onRefreshRuns: PropTypes.func, }; diff --git a/DashAI/front/src/pages/models/ModelsContent.jsx b/DashAI/front/src/pages/models/ModelsContent.jsx index 0a124c4b6..24742a6d9 100644 --- a/DashAI/front/src/pages/models/ModelsContent.jsx +++ b/DashAI/front/src/pages/models/ModelsContent.jsx @@ -15,8 +15,6 @@ import CreateSessionSteps from "../../components/models/CreateSessionSteps"; import SessionVisualization from "../../components/models/SessionVisualization"; import DatasetVisualization from "../../components/DatasetVisualization"; import AddModelDialog from "../../components/models/AddModelDialog"; -import EditRunDialog from "../../components/models/EditRunDialog"; -import EditConfirmationDialog from "../../components/models/EditConfirmationDialog"; import RetrainConfirmDialog from "../../components/models/RetrainConfirmDialog"; import { getComponents } from "../../api/component"; import { @@ -37,7 +35,6 @@ import { getRunById, getRunOperationsCount, deleteRunOperations, - updateRunParameters, } from "../../api/run"; import { enqueueRunnerJob as enqueueRunnerJobRequest } from "../../api/job"; import { startJobPolling } from "../../utils/jobPoller"; @@ -66,11 +63,6 @@ export default function ModelsContent() { const [runToRetrain, setRunToRetrain] = useState(null); const [operationsCount, setOperationsCount] = useState(null); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [runToEdit, setRunToEdit] = useState(null); - const [editConfirmDialogOpen, setEditConfirmDialogOpen] = useState(false); - const [pendingEditData, setPendingEditData] = useState(null); - const isResizingLeft = useRef(false); const isResizingRight = useRef(false); const [isTogglingLeft, setIsTogglingLeft] = useState(false); @@ -140,8 +132,8 @@ export default function ModelsContent() { } }; - const handleTrainRunWithTour = (run) => { - handleTrainRun(run); + const handleTrainRunWithTour = (run, operationsCount = null) => { + handleTrainRun(run, operationsCount); // Advance tour after clicking train (step 5 -> end) if (sessionTourContext?.run && sessionTourContext?.stepIndex === 5) { @@ -169,8 +161,8 @@ export default function ModelsContent() { session={selectedSession} runs={runs} onTrain={handleTrainRunWithTour} - onEditRun={handleEditRun} onDeleteRun={handleDeleteRun} + onRefreshRuns={fetchRuns} /> @@ -562,9 +554,19 @@ export default function ModelsContent() { }); }; - const handleTrainRun = async (run) => { + const handleTrainRun = async (run, passedOperationsCount = null) => { try { - // Check if run has been trained before (has metrics) + if ( + passedOperationsCount && + (passedOperationsCount.explainers > 0 || + passedOperationsCount.predictions > 0) + ) { + setRunToRetrain(run); + setOperationsCount(passedOperationsCount); + setRetrainDialogOpen(true); + return; + } + const hasBeenTrained = run.test_metrics || run.train_metrics || @@ -672,73 +674,6 @@ export default function ModelsContent() { setOperationsCount(null); }; - const handleEditRun = async (run) => { - setRunToEdit(run); - setEditDialogOpen(true); - }; - - const handleConfirmEdit = async (editData) => { - // Check if run has operations (explainers/predictions) - try { - const count = await getRunOperationsCount(editData.runId.toString()); - const hasOperations = count.explainers > 0 || count.predictions > 0; - - setPendingEditData({ ...editData, hasOperations }); - setEditConfirmDialogOpen(true); - } catch (error) { - console.error("Error checking operations:", error); - // Proceed without operations check - setPendingEditData({ ...editData, hasOperations: false }); - setEditConfirmDialogOpen(true); - } - }; - - const handleExecuteEdit = async () => { - if (!pendingEditData) return; - - try { - setEditConfirmDialogOpen(false); - - // Update run parameters via API - await updateRunParameters( - pendingEditData.runId.toString(), - pendingEditData.parameters, - pendingEditData.optimizer, - pendingEditData.optimizer_parameters, - pendingEditData.goal_metric, - ); - - // Fetch updated run - const updatedRun = await getRunById(pendingEditData.runId.toString()); - - // Update runs list - setRuns((prev) => - prev.map((r) => (r.id === updatedRun.id ? updatedRun : r)), - ); - - enqueueSnackbar(`Run "${pendingEditData.name}" updated successfully`, { - variant: "success", - }); - - // Auto-retrain the updated run - await executeTraining(updatedRun); - - setPendingEditData(null); - } catch (error) { - console.error("Error updating run:", error); - enqueueSnackbar( - `Error updating run: ${error.message || "Unknown error"}`, - { variant: "error" }, - ); - setPendingEditData(null); - } - }; - - const handleCancelEdit = () => { - setEditConfirmDialogOpen(false); - setPendingEditData(null); - }; - const handleDeleteRun = async (run) => { try { await deleteRun(run.id.toString()); @@ -979,8 +914,8 @@ export default function ModelsContent() { session={selectedSession} runs={runs} onTrain={handleTrainRun} - onEditRun={handleEditRun} onDeleteRun={handleDeleteRun} + onRefreshRuns={fetchRuns} /> ) : step === 1 && selectedTask ? ( - - {/* Edit Run Dialog */} - setEditDialogOpen(false)} - session={selectedSession} - run={runToEdit} - existingRuns={runs} - onConfirmEdit={handleConfirmEdit} - /> - - {/* Edit Confirmation Dialog */} - {!selectedSessionId && ( Date: Mon, 26 Jan 2026 20:36:20 -0300 Subject: [PATCH 03/15] feat: Add LiveMetricsChart and HyperparameterPlots components for enhanced model run visualization --- .../components/models/HyperparameterPlots.jsx | 213 +++++++++++ .../components/models/LiveMetricsChart.jsx | 358 ++++++++++++++++++ .../front/src/components/models/RunCard.jsx | 62 +-- .../{RunOperations.jsx => RunResults.jsx} | 112 +++--- .../components/ResultsDetailsLayout.jsx | 4 - 5 files changed, 676 insertions(+), 73 deletions(-) create mode 100644 DashAI/front/src/components/models/HyperparameterPlots.jsx create mode 100644 DashAI/front/src/components/models/LiveMetricsChart.jsx rename DashAI/front/src/components/models/{RunOperations.jsx => RunResults.jsx} (86%) diff --git a/DashAI/front/src/components/models/HyperparameterPlots.jsx b/DashAI/front/src/components/models/HyperparameterPlots.jsx new file mode 100644 index 000000000..53fde35f3 --- /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]); + + 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..0a4dd9d16 --- /dev/null +++ b/DashAI/front/src/components/models/LiveMetricsChart.jsx @@ -0,0 +1,358 @@ +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 { getExperimentById } from "../../api/experiment"; + +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; + + getExperimentById(run.experiment_id).then((exp) => { + if (!mounted) return; + + setAvailableMetrics({ + TRAIN: exp.train_metrics ?? [], + VALIDATION: exp.validation_metrics ?? [], + TEST: exp.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) { + 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/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index 38f471aac..fa6686642 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -3,7 +3,6 @@ import PropTypes from "prop-types"; import { Card, CardContent, - CardActions, Box, Typography, Chip, @@ -27,13 +26,14 @@ import { Stop, Edit, Delete, - Settings, Save, Cancel, + ExpandMore, + ExpandLess, } 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"; @@ -307,7 +307,11 @@ function RunCard({ color={expanded ? "primary" : "default"} disabled={isEditing} > - + {expanded ? ( + + ) : ( + + )} - - - - + )} {/* Train/Re-train Button */} {canTrain && ( @@ -469,7 +473,17 @@ function RunCard({ {/* Expandable Details */} - + {isEditing ? ( // EDIT MODE @@ -679,17 +693,15 @@ function RunCard({ - {/* RunOperations - Separate section for finished runs */} - {statusText === "Finished" && ( - - - - )} + {/* RunResults - Shows live metrics, explainers, predictions, and hyperparameters */} + + + ); diff --git a/DashAI/front/src/components/models/RunOperations.jsx b/DashAI/front/src/components/models/RunResults.jsx similarity index 86% rename from DashAI/front/src/components/models/RunOperations.jsx rename to DashAI/front/src/components/models/RunResults.jsx index 3dc127386..261824eb8 100644 --- a/DashAI/front/src/components/models/RunOperations.jsx +++ b/DashAI/front/src/components/models/RunResults.jsx @@ -26,15 +26,14 @@ 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 { useSnackbar } from "notistack"; -/** - * RunOperations component - Shows explainers and predictions for a finished run - * Displays as expandable sections within a RunCard - */ -export default function RunOperations({ +export default function RunResults({ run, session, onRefresh, @@ -44,13 +43,13 @@ export default function RunOperations({ const [localExplainers, setLocalExplainers] = useState([]); const [predictions, setPredictions] = useState([]); const [loading, setLoading] = useState(true); - const [operationsVisible, setOperationsVisible] = useState(() => { - const saved = localStorage.getItem(`run-${run.id}-operations-visible`); - return saved !== null ? JSON.parse(saved) : false; + 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 !== null ? JSON.parse(saved) : 0; + return saved ? JSON.parse(saved) : 0; }); const [globalDialogOpen, setGlobalDialogOpen] = useState(false); @@ -60,6 +59,10 @@ export default function RunOperations({ 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 fetchOperations = useCallback(async () => { if (!run || !run.id) return; @@ -85,7 +88,6 @@ export default function RunOperations({ fetchOperations(); }, [fetchOperations, explainerRefreshTrigger]); - // Listen for prediction dialog open event useEffect(() => { const handleOpenDialog = (event) => { if (event.detail.runId === run.id) { @@ -97,15 +99,20 @@ export default function RunOperations({ window.removeEventListener("openPredictionDialog", handleOpenDialog); }, [run.id]); - // Persist operationsVisible state + useEffect(() => { + if (isRunning && !resultsVisible) { + setResultsVisible(true); + setActiveTab(0); // Live Metrics tab + } + }, [isRunning, resultsVisible]); + useEffect(() => { localStorage.setItem( - `run-${run.id}-operations-visible`, - JSON.stringify(operationsVisible), + `run-${run.id}-results-visible`, + JSON.stringify(resultsVisible), ); - }, [operationsVisible, run.id]); + }, [resultsVisible, run.id]); - // Persist activeTab state useEffect(() => { localStorage.setItem(`run-${run.id}-active-tab`, JSON.stringify(activeTab)); }, [activeTab, run.id]); @@ -136,7 +143,7 @@ export default function RunOperations({ const totalOperations = globalExplainers.length + localExplainers.length + predictions.length; - if (loading) { + if (loading && isFinished) { return ( @@ -145,60 +152,75 @@ export default function RunOperations({ } return ( - - {/* Header with Show/Hide button */} + - - {/* Tabs for Explainability and Predictions */} + setActiveTab(newValue)} - aria-label="Operations tabs" + aria-label="Results tabs" > + Explainability - + {isFinished && ( + + )} } + disabled={!isFinished} /> Predictions - + {isFinished && ( + + )} } + disabled={!isFinished} + /> + - {/* Tab Panel 0: Explainability */} {activeTab === 0 && ( + + + + )} + + {activeTab === 1 && isFinished && ( - {/* Global Explainers Column */} - {/* Local Explainers Column */} )} - {/* Tab Panel 1: Predictions */} - {activeTab === 1 && ( + {activeTab === 2 && isFinished && ( - {/* Dataset Predictions Column */} - {/* Manual Predictions Column */} )} + + {activeTab === 3 && isFinished && optimizables > 0 && ( + + + + )} - {/* Dialogs */} )} - {/* {currentTab === 1 && } - {currentTab === 2 && } */} {currentTab === 3 && } {currentTab === 4 && TODO...} From ab6f061276096f6f06f586e738598584f6bc7be1 Mon Sep 17 00:00:00 2001 From: Felipe Date: Mon, 26 Jan 2026 23:01:21 -0300 Subject: [PATCH 04/15] Fix translation issues in models --- .../models/EditConfirmationDialog.jsx | 31 ++++++------- .../components/models/LiveMetricsChart.jsx | 10 ++--- .../front/src/components/models/RunCard.jsx | 44 ++++++++----------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/DashAI/front/src/components/models/EditConfirmationDialog.jsx b/DashAI/front/src/components/models/EditConfirmationDialog.jsx index 7e354af19..2b92f6dae 100644 --- a/DashAI/front/src/components/models/EditConfirmationDialog.jsx +++ b/DashAI/front/src/components/models/EditConfirmationDialog.jsx @@ -20,6 +20,7 @@ import { DeleteSweep as DeleteSweepIcon, RestartAlt as RestartAltIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; /** * Confirmation dialog for editing run parameters @@ -32,23 +33,23 @@ function EditConfirmationDialog({ run, hasOperations = false, }) { + const { t } = useTranslation(["models", "common"]); + if (!run) { return null; } return ( - Confirm Parameter Update + {t("models:label.confirmParameterUpdate")} - You are about to update the parameters for run{" "} - {run.name}. This action will have the following - consequences: + {t("models:message.aboutToUpdateParameters", { runName: run.name })} - Important - The following data will be permanently deleted: + {t("common:important")} + {t("models:message.dataWillBeDeleted")} @@ -57,8 +58,8 @@ function EditConfirmationDialog({ @@ -66,8 +67,8 @@ function EditConfirmationDialog({ {hasOperations && ( @@ -76,8 +77,8 @@ function EditConfirmationDialog({ )} @@ -87,7 +88,7 @@ function EditConfirmationDialog({ - The run will be automatically retrained after updating parameters. + {t("models:message.willBeRetrainedAfterUpdate")} @@ -95,7 +96,7 @@ function EditConfirmationDialog({ diff --git a/DashAI/front/src/components/models/LiveMetricsChart.jsx b/DashAI/front/src/components/models/LiveMetricsChart.jsx index 0a4dd9d16..28c3cbb42 100644 --- a/DashAI/front/src/components/models/LiveMetricsChart.jsx +++ b/DashAI/front/src/components/models/LiveMetricsChart.jsx @@ -20,7 +20,7 @@ import { ResponsiveContainer, } from "recharts"; import { useEffect, useRef, useState } from "react"; -import { getExperimentById } from "../../api/experiment"; +import { getModelSessionById } from "../../api/modelSession"; export function LiveMetricsChart({ run }) { const [level, setLevel] = useState(null); @@ -148,13 +148,13 @@ export function LiveMetricsChart({ run }) { useEffect(() => { let mounted = true; - getExperimentById(run.experiment_id).then((exp) => { + getModelSessionById(run.model_session_id.toString()).then((session) => { if (!mounted) return; setAvailableMetrics({ - TRAIN: exp.train_metrics ?? [], - VALIDATION: exp.validation_metrics ?? [], - TEST: exp.test_metrics ?? [], + TRAIN: session.train_metrics ?? [], + VALIDATION: session.validation_metrics ?? [], + TEST: session.test_metrics ?? [], }); }); diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index 025c0f61d..e01e1aa0c 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -41,7 +41,6 @@ import OptimizationTableSelectOptimizer from "../experiments/OptimizationTableSe import ModelsTableSelectMetric from "../experiments/ModelsTableSelectMetric"; import useSchema from "../../hooks/useSchema"; import { updateRunParameters, getRunOperationsCount } from "../../api/run"; -import RunOperations from "./RunOperations"; import { useTranslation } from "react-i18next"; /** @@ -231,7 +230,7 @@ function RunCard({ } }; - const statusText = getRunStatus(run.status); + const statusText = getRunStatus(run.status, t); const model = models.find((m) => m.name === run.model_name); const modelDisplayName = model?.display_name || run.model_name; @@ -251,11 +250,8 @@ function RunCard({ } }; - const canTrain = - statusText === "Not Started" || - statusText === "Error" || - statusText === "Finished"; - const isRunning = statusText === "Delivered" || statusText === "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 const getMetrics = () => { if (!run.trained_models || run.trained_models.length === 0) { @@ -284,9 +280,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" @@ -369,23 +365,21 @@ function RunCard({ )} - {!isEditing && - statusText !== "Delivered" && - statusText !== "Started" && ( - - )} + {!isEditing && run.status !== 1 && run.status !== 2 && ( + + )} {canTrain && ( 0 || operationsCount.predictions > 0) @@ -396,7 +390,7 @@ function RunCard({ From 81eccb182ed46b73d19410372ec5c4f79ebdff3a Mon Sep 17 00:00:00 2001 From: Felipe Date: Mon, 26 Jan 2026 23:10:00 -0300 Subject: [PATCH 05/15] fix precommit --- .../src/components/experiments/runButtons/EditRunDialog.jsx | 4 +--- DashAI/front/src/components/models/AddModelDialog.jsx | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) 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/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx index a525dcb71..b544b08f3 100644 --- a/DashAI/front/src/components/models/AddModelDialog.jsx +++ b/DashAI/front/src/components/models/AddModelDialog.jsx @@ -258,9 +258,9 @@ function AddModelDialog({ const isStep1Valid = Boolean(selectedModel && name.trim() !== ""); const isStep2Valid = Boolean( selectedOptimizer && - optimizerParameters && - Object.keys(optimizerParameters).length > 0 && - goalMetric, + optimizerParameters && + Object.keys(optimizerParameters).length > 0 && + goalMetric, ); return ( From e6a3c796188faf633017c00f543a41148169b5e3 Mon Sep 17 00:00:00 2001 From: Felipe Date: Sat, 31 Jan 2026 21:34:14 -0300 Subject: [PATCH 06/15] Fix translations errors --- DashAI/front/src/components/models/RunCard.jsx | 5 ++++- .../src/utils/i18n/locales/en/common.json | 5 +++++ .../src/utils/i18n/locales/en/models.json | 18 +++++++++++++++++- .../src/utils/i18n/locales/es/common.json | 5 +++++ .../src/utils/i18n/locales/es/models.json | 18 +++++++++++++++++- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index e01e1aa0c..cb2eb7f72 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -383,7 +383,10 @@ function RunCard({ operationsCount && (operationsCount.explainers > 0 || operationsCount.predictions > 0) - ? `Warning: This will reset ${operationsCount.explainers} explainer(s) and ${operationsCount.predictions} prediction(s)` + ? t("models:message.retrainWillResetOperations", { + explainersCount: operationsCount.explainers, + predictionsCount: operationsCount.predictions, + }) : "" } > 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..19d6946e6 100644 --- a/DashAI/front/src/utils/i18n/locales/en/models.json +++ b/DashAI/front/src/utils/i18n/locales/en/models.json @@ -18,7 +18,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", @@ -68,6 +69,9 @@ "globalExplainers": "Global Explainers", "goalMetric": "Goal Metric", "hyperparameterOptimizationPlots": "Hyperparameter Optimization Plots", + "hyperparameterOptimizerConfiguration": "Hyperparameter Optimizer Configuration", + "runName": "Run Name", + "confirmParameterUpdate": "Confirm Parameter Update", "liveMetrics": "Live Metrics", "localExplainer": "Local Explainer", "localExplainers": "Local Explainers", @@ -125,6 +129,18 @@ "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)", "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.", 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..59470811c 100644 --- a/DashAI/front/src/utils/i18n/locales/es/models.json +++ b/DashAI/front/src/utils/i18n/locales/es/models.json @@ -18,7 +18,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", @@ -68,6 +69,9 @@ "globalExplainers": "Explicadores Globales", "goalMetric": "Métrica Objetivo", "hyperparameterOptimizationPlots": "Gráficos de Optimización de Hiperparámetros", + "hyperparameterOptimizerConfiguration": "Configuración del Optimizador de 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", @@ -125,6 +129,18 @@ "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)", "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.", From fe3192a4c3b34e7568956f2d7fe4f982b34073a4 Mon Sep 17 00:00:00 2001 From: Felipe Date: Sat, 31 Jan 2026 21:48:13 -0300 Subject: [PATCH 07/15] fix: Now the optimizer info in "show parameters" is displayed only when the optimizer is used --- DashAI/front/src/components/models/RunCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index cb2eb7f72..e10b5a22b 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -639,7 +639,7 @@ function RunCard({ )} - {run.optimizer_name && ( + {run.optimizer_name && run.goal_metric && ( {t("common:optimizer")}: {run.optimizer_name} From 088977119bffdcedc62730e1f8707f34fd5c9130 Mon Sep 17 00:00:00 2001 From: Felipe Date: Sat, 31 Jan 2026 22:31:23 -0300 Subject: [PATCH 08/15] fix: update run parameters to include optional name and clean up unused imports in components --- DashAI/front/src/api/run.ts | 2 ++ .../models/PredictionCreationDialog.jsx | 15 --------------- DashAI/front/src/components/models/RunCard.jsx | 1 + DashAI/front/src/components/models/RunResults.jsx | 4 ---- .../components/threeSectionLayout/BarHeader.jsx | 2 -- 5 files changed, 3 insertions(+), 21 deletions(-) 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/models/PredictionCreationDialog.jsx b/DashAI/front/src/components/models/PredictionCreationDialog.jsx index aea77e209..5de4e2230 100644 --- a/DashAI/front/src/components/models/PredictionCreationDialog.jsx +++ b/DashAI/front/src/components/models/PredictionCreationDialog.jsx @@ -29,7 +29,6 @@ import { useSnackbar } from "notistack"; import { startJobPolling } from "../../utils/jobPoller"; import { getPredictions } from "../../api/predict"; -const steps = ["Configure Input", "Confirm"]; import { useTranslation } from "react-i18next"; /** @@ -59,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"), ]; @@ -211,19 +209,6 @@ export default function PredictionCreationDialog({ const renderStepContent = (step) => { switch (step) { case 0: - return ( - - - {t("prediction:label.selectPredictionMode")} - - - - ); - - case 1: return ( {predictionMode === "dataset" ? ( diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index e10b5a22b..349fda685 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -184,6 +184,7 @@ function RunCard({ try { await updateRunParameters( run.id.toString(), + editedName.trim(), editedParameters, editedOptimizer || "", editedOptimizerParams || {}, diff --git a/DashAI/front/src/components/models/RunResults.jsx b/DashAI/front/src/components/models/RunResults.jsx index 261824eb8..4665425f2 100644 --- a/DashAI/front/src/components/models/RunResults.jsx +++ b/DashAI/front/src/components/models/RunResults.jsx @@ -2,9 +2,6 @@ import React, { useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import { Box, - Accordion, - AccordionSummary, - AccordionDetails, Typography, Button, Chip, @@ -31,7 +28,6 @@ import HyperparameterPlots from "./HyperparameterPlots"; import { getExplainers } from "../../api/explainer"; import { getPredictions } from "../../api/predict"; import { checkHowManyOptimazers } from "../../utils/schema"; -import { useSnackbar } from "notistack"; export default function RunResults({ run, diff --git a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx index cc9b670fa..1e0081c0f 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"; export default function BarHeader() { - const theme = useTheme(); return ( Date: Sat, 31 Jan 2026 22:51:44 -0300 Subject: [PATCH 09/15] Fix1 precommit --- DashAI/front/src/components/models/AddModelDialog.jsx | 2 +- DashAI/front/src/components/models/EditRunDialog.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DashAI/front/src/components/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx index b544b08f3..c7321ac21 100644 --- a/DashAI/front/src/components/models/AddModelDialog.jsx +++ b/DashAI/front/src/components/models/AddModelDialog.jsx @@ -28,7 +28,7 @@ import { useTranslation } from "react-i18next"; /** * 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/EditRunDialog.jsx b/DashAI/front/src/components/models/EditRunDialog.jsx index 8a1d2ca9a..9b4f31644 100644 --- a/DashAI/front/src/components/models/EditRunDialog.jsx +++ b/DashAI/front/src/components/models/EditRunDialog.jsx @@ -25,7 +25,7 @@ import useSchema from "../../hooks/useSchema"; /** * Dialog for editing an existing model run parameters * Step 1: Configure model name and parameters - * Step 2: Configure optimizer (if optimizable parameters exist) + * Step 2: Configure optimizer */ function EditRunDialog({ open, From dfc3763508b59a77083907d4a141dc239e94749e Mon Sep 17 00:00:00 2001 From: Felipe Date: Sat, 31 Jan 2026 23:07:28 -0300 Subject: [PATCH 10/15] fix requirements transformers test --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c05369d00..598a66848 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ typer rich torch torchvision -transformers +transformers<5.0.0 sacrebleu sentencepiece optuna From bc6e2e48052d839b60397fe31642e8dc09dca5c6 Mon Sep 17 00:00:00 2001 From: Cristian Tamblay Date: Mon, 23 Feb 2026 18:00:47 -0300 Subject: [PATCH 11/15] Fixing precommit try 1 --- DashAI/front/src/components/models/AddModelDialog.jsx | 6 +++--- DashAI/front/src/components/models/EditRunDialog.jsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DashAI/front/src/components/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx index c7321ac21..ebd89fda1 100644 --- a/DashAI/front/src/components/models/AddModelDialog.jsx +++ b/DashAI/front/src/components/models/AddModelDialog.jsx @@ -258,9 +258,9 @@ function AddModelDialog({ const isStep1Valid = Boolean(selectedModel && name.trim() !== ""); const isStep2Valid = Boolean( selectedOptimizer && - optimizerParameters && - Object.keys(optimizerParameters).length > 0 && - goalMetric, + optimizerParameters && + Object.keys(optimizerParameters).length > 0 && + goalMetric, ); return ( diff --git a/DashAI/front/src/components/models/EditRunDialog.jsx b/DashAI/front/src/components/models/EditRunDialog.jsx index 9b4f31644..6cf5c35a1 100644 --- a/DashAI/front/src/components/models/EditRunDialog.jsx +++ b/DashAI/front/src/components/models/EditRunDialog.jsx @@ -185,9 +185,9 @@ function EditRunDialog({ const isStep1Valid = Boolean(run?.model_name && name.trim() !== ""); const isStep2Valid = Boolean( selectedOptimizer && - optimizerParameters && - Object.keys(optimizerParameters).length > 0 && - goalMetric, + optimizerParameters && + Object.keys(optimizerParameters).length > 0 && + goalMetric, ); if (!run) { From 15eaa3e33e40c28e1c50567a567623cc780bacaf Mon Sep 17 00:00:00 2001 From: Felipe Date: Wed, 25 Feb 2026 13:40:20 -0300 Subject: [PATCH 12/15] feat: explainers cards have same width when plots are hidden --- .../components/explainers/ExplainersPlot.jsx | 85 ++++++++++--------- .../explainers/ExplanainersCard.jsx | 8 +- .../src/components/models/RunResults.jsx | 10 ++- 3 files changed, 54 insertions(+), 49 deletions(-) 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 2628d3a8c..a93aea612 100644 --- a/DashAI/front/src/components/explainers/ExplanainersCard.jsx +++ b/DashAI/front/src/components/explainers/ExplanainersCard.jsx @@ -23,6 +23,7 @@ import { useNavigate } from "react-router-dom"; import { deleteExplainer } from "../../api/explainer"; import { useTranslation } from "react-i18next"; import { getComponentById } from "../../api/component"; +import { maxWidth } from "@mui/system"; /** * GlobalExplainersCard @@ -87,7 +88,7 @@ export default function ExplainersCard({ if (compact) { return ( <> - + - + {componentData @@ -181,7 +183,7 @@ export default function ExplainersCard({ // Full mode for standalone page return ( - + - - + + + - + Date: Wed, 25 Feb 2026 14:30:39 -0300 Subject: [PATCH 13/15] feat: remove JobQueueWidget from multiple components and create only one instance of the widget. Now opens and close automatically when a job is running. --- DashAI/front/src/App.jsx | 2 + .../src/components/DatasetVisualization.jsx | 3 -- .../src/components/custom/CustomLayout.jsx | 22 +---------- .../components/generative/GenerativeChat.jsx | 3 -- .../src/components/jobs/JobQueueWidget.jsx | 39 ++++++++++++++----- .../components/models/CreateSessionSteps.jsx | 5 +-- .../models/SessionVisualization.jsx | 3 -- .../ConfigureAndUploadDatasetStep.jsx | 2 + .../notebook/NotebookVisualization.jsx | 2 - 9 files changed, 35 insertions(+), 46 deletions(-) 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/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/generative/GenerativeChat.jsx b/DashAI/front/src/components/generative/GenerativeChat.jsx index f67432a52..f2c1c0e76 100644 --- a/DashAI/front/src/components/generative/GenerativeChat.jsx +++ b/DashAI/front/src/components/generative/GenerativeChat.jsx @@ -18,7 +18,6 @@ import InfoSessionModal from "./InfoSessionModal"; import { useSnackbar } from "notistack"; import { TextInput } from "./TextInput"; import { MediaInput } from "./MediaInput"; -import JobQueueWidget from "../jobs/JobQueueWidget"; import { getRunStatus } from "../../utils/runStatus"; import { Trans, useTranslation } from "react-i18next"; @@ -334,8 +333,6 @@ export default function GenerativeChat({ sessionId, taskName, paramsVersion }) { onClose={() => 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/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/SessionVisualization.jsx b/DashAI/front/src/components/models/SessionVisualization.jsx index adacc5485..c3cccbafb 100644 --- a/DashAI/front/src/components/models/SessionVisualization.jsx +++ b/DashAI/front/src/components/models/SessionVisualization.jsx @@ -11,7 +11,6 @@ import { 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"; @@ -198,7 +197,6 @@ export default function SessionVisualization() { {t("models:label.selectSessionToViewModels")} - ); } @@ -423,7 +421,6 @@ export default function SessionVisualization() { - - ); } From d54c351d15991239d467235037d4959c227e2a38 Mon Sep 17 00:00:00 2001 From: Felipe Date: Wed, 25 Feb 2026 17:27:36 -0300 Subject: [PATCH 14/15] feat: Added a dialog to confirm when try to edit parameters and the model have explainers or predictions. --- .../explainers/ExplanainersCard.jsx | 1 - .../components/models/LiveMetricsChart.jsx | 4 +- .../models/RetrainConfirmDialog.jsx | 35 ++++++-- .../front/src/components/models/RunCard.jsx | 90 ++++++++++++------- .../components/threeSectionLayout/Footer.jsx | 4 +- DashAI/front/src/hooks/models/useSessions.js | 13 +-- .../src/utils/i18n/locales/en/models.json | 3 + .../src/utils/i18n/locales/es/models.json | 3 + 8 files changed, 97 insertions(+), 56 deletions(-) diff --git a/DashAI/front/src/components/explainers/ExplanainersCard.jsx b/DashAI/front/src/components/explainers/ExplanainersCard.jsx index a93aea612..3a4d4bc98 100644 --- a/DashAI/front/src/components/explainers/ExplanainersCard.jsx +++ b/DashAI/front/src/components/explainers/ExplanainersCard.jsx @@ -23,7 +23,6 @@ import { useNavigate } from "react-router-dom"; import { deleteExplainer } from "../../api/explainer"; import { useTranslation } from "react-i18next"; import { getComponentById } from "../../api/component"; -import { maxWidth } from "@mui/system"; /** * GlobalExplainersCard diff --git a/DashAI/front/src/components/models/LiveMetricsChart.jsx b/DashAI/front/src/components/models/LiveMetricsChart.jsx index 28c3cbb42..d3cc8c249 100644 --- a/DashAI/front/src/components/models/LiveMetricsChart.jsx +++ b/DashAI/front/src/components/models/LiveMetricsChart.jsx @@ -146,6 +146,8 @@ export function LiveMetricsChart({ run }) { }, [run.id, run.test_metrics]); useEffect(() => { + if (!run.model_session_id) return; + let mounted = true; getModelSessionById(run.model_session_id.toString()).then((session) => { @@ -161,7 +163,7 @@ export function LiveMetricsChart({ run }) { return () => { mounted = false; }; - }, [run.experiment_id]); + }, [run.model_session_id]); const metrics = data[split]?.[level] ?? {}; const allowed = availableMetrics[split] ?? []; diff --git a/DashAI/front/src/components/models/RetrainConfirmDialog.jsx b/DashAI/front/src/components/models/RetrainConfirmDialog.jsx index 8dc42fdf3..5e1553b77 100644 --- a/DashAI/front/src/components/models/RetrainConfirmDialog.jsx +++ b/DashAI/front/src/components/models/RetrainConfirmDialog.jsx @@ -23,6 +23,7 @@ export default function RetrainConfirmDialog({ onConfirm, run, operationsCount, + mode = "retrain", }) { const { t } = useTranslation(["models", "common"]); @@ -35,7 +36,11 @@ export default function RetrainConfirmDialog({ {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 349fda685..a9d15bc03 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -41,6 +41,7 @@ import OptimizationTableSelectOptimizer from "../experiments/OptimizationTableSe 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"; /** @@ -78,6 +79,7 @@ function RunCard({ ); const [operationsCount, setOperationsCount] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [saveConfirmOpen, setSaveConfirmOpen] = useState(false); const { defaultValues: defaultOptimizerParams } = useSchema({ modelName: editedOptimizer, @@ -145,6 +147,44 @@ function RunCard({ setEditedGoalMetric(run.goal_metric || ""); }; + const doSave = async () => { + setSaveConfirmOpen(false); + setIsSaving(true); + try { + await updateRunParameters( + run.id.toString(), + editedName.trim(), + editedParameters, + editedOptimizer || "", + editedOptimizerParams || {}, + editedGoalMetric || "", + ); + + enqueueSnackbar( + `Run "${editedName}" updated successfully. Status changed to Not Started.`, + { + variant: "success", + }, + ); + + setIsEditing(false); + + if (onRefresh) { + await onRefresh(); + } + } catch (error) { + console.error("Error updating run:", error); + enqueueSnackbar( + `Error updating run: ${error.message || "Unknown error"}`, + { + variant: "error", + }, + ); + } finally { + setIsSaving(false); + } + }; + const handleSaveEdit = async () => { if (!editedName.trim()) { enqueueSnackbar("Run name cannot be empty", { variant: "warning" }); @@ -180,40 +220,16 @@ function RunCard({ } } - setIsSaving(true); - try { - await updateRunParameters( - run.id.toString(), - editedName.trim(), - editedParameters, - editedOptimizer || "", - editedOptimizerParams || {}, - editedGoalMetric || "", - ); - - enqueueSnackbar( - `Run "${editedName}" updated successfully. Status changed to Not Started.`, - { - variant: "success", - }, - ); - - setIsEditing(false); - - if (onRefresh) { - await onRefresh(); - } - } catch (error) { - console.error("Error updating run:", error); - enqueueSnackbar( - `Error updating run: ${error.message || "Unknown error"}`, - { - variant: "error", - }, - ); - } finally { - setIsSaving(false); + // 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) => { @@ -699,6 +715,14 @@ function RunCard({ explainerRefreshTrigger={explainerRefreshTrigger} /> + setSaveConfirmOpen(false)} + onConfirm={doSave} + run={run} + operationsCount={operationsCount} + /> ); diff --git a/DashAI/front/src/components/threeSectionLayout/Footer.jsx b/DashAI/front/src/components/threeSectionLayout/Footer.jsx index f630adcdc..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 ( { 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/utils/i18n/locales/en/models.json b/DashAI/front/src/utils/i18n/locales/en/models.json index 19d6946e6..712346f2c 100644 --- a/DashAI/front/src/utils/i18n/locales/en/models.json +++ b/DashAI/front/src/utils/i18n/locales/en/models.json @@ -102,6 +102,8 @@ "retrainModel": "Re-train Model?", "retrainWillDeleteOperations": "This run has existing operations that will be deleted", "retrainWillDeleteOperationsDetails": "Re-training run \"<1>{{runName}}\" will delete:", + "saveParameterChanges": "Save Parameter Changes?", + "saveWillDeleteOperationsDetails": "Saving \"<1>{{runName}}\" will reset the run. The following will be deleted when you train again:", "runDetails": "Run Details", "runExperimentToSeeMetrics": "Go to<1>experiments tabto run your experiment.", "runFailedNoHyperparameterPlots": "Run Failed. No hyperparameter plots available.", @@ -141,6 +143,7 @@ "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.", diff --git a/DashAI/front/src/utils/i18n/locales/es/models.json b/DashAI/front/src/utils/i18n/locales/es/models.json index 59470811c..d28498e9d 100644 --- a/DashAI/front/src/utils/i18n/locales/es/models.json +++ b/DashAI/front/src/utils/i18n/locales/es/models.json @@ -102,6 +102,8 @@ "retrainModel": "¿Re-entrenar Modelo?", "retrainWillDeleteOperations": "Esta ejecución tiene operaciones existentes que serán eliminadas", "retrainWillDeleteOperationsDetails": "Re-entrenar la ejecución \"<1>{{runName}}\" eliminará:", + "saveParameterChanges": "¿Guardar Cambios de Parámetros?", + "saveWillDeleteOperationsDetails": "Guardar \"<1>{{runName}}\" 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 experimentos para ejecutar tu experimento.", "runFailedNoHyperparameterPlots": "Ejecución Fallida. No hay gráficos de hiperparámetros disponibles.", @@ -141,6 +143,7 @@ "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.", From 4a82723d22f48d059d2a98cf8fde531a1e4ce187 Mon Sep 17 00:00:00 2001 From: Felipe Date: Wed, 25 Feb 2026 17:43:21 -0300 Subject: [PATCH 15/15] fix copilot github overview. --- .../src/components/models/AddModelDialog.jsx | 6 +- .../models/EditConfirmationDialog.jsx | 125 ------ .../src/components/models/EditRunDialog.jsx | 362 ------------------ .../components/models/HyperparameterPlots.jsx | 2 +- .../components/models/LiveMetricsChart.jsx | 6 +- .../front/src/components/models/RunCard.jsx | 32 +- .../src/components/models/RunResults.jsx | 40 +- .../threeSectionLayout/BarHeader.jsx | 2 +- .../src/utils/i18n/locales/en/models.json | 15 + .../src/utils/i18n/locales/es/models.json | 15 + 10 files changed, 78 insertions(+), 527 deletions(-) delete mode 100644 DashAI/front/src/components/models/EditConfirmationDialog.jsx delete mode 100644 DashAI/front/src/components/models/EditRunDialog.jsx diff --git a/DashAI/front/src/components/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx index 641b076c3..912e2bede 100644 --- a/DashAI/front/src/components/models/AddModelDialog.jsx +++ b/DashAI/front/src/components/models/AddModelDialog.jsx @@ -283,9 +283,9 @@ function AddModelDialog({ const isStep1Valid = Boolean(selectedModel && name.trim() !== ""); const isStep2Valid = Boolean( selectedOptimizer && - optimizerParameters && - Object.keys(optimizerParameters).length > 0 && - goalMetric, + optimizerParameters && + Object.keys(optimizerParameters).length > 0 && + goalMetric, ); return ( diff --git a/DashAI/front/src/components/models/EditConfirmationDialog.jsx b/DashAI/front/src/components/models/EditConfirmationDialog.jsx deleted file mode 100644 index 2b92f6dae..000000000 --- a/DashAI/front/src/components/models/EditConfirmationDialog.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - DialogContentText, - Button, - Alert, - AlertTitle, - Box, - List, - ListItem, - ListItemIcon, - ListItemText, -} from "@mui/material"; -import { - Warning as WarningIcon, - DeleteSweep as DeleteSweepIcon, - RestartAlt as RestartAltIcon, -} from "@mui/icons-material"; -import { useTranslation } from "react-i18next"; - -/** - * Confirmation dialog for editing run parameters - * Warns user about consequences of overwriting existing model - */ -function EditConfirmationDialog({ - open, - onClose, - onConfirm, - run, - hasOperations = false, -}) { - const { t } = useTranslation(["models", "common"]); - - if (!run) { - return null; - } - - return ( - - {t("models:label.confirmParameterUpdate")} - - - {t("models:message.aboutToUpdateParameters", { runName: run.name })} - - - - {t("common:important")} - {t("models:message.dataWillBeDeleted")} - - - - - - - - - - - - - - - - {hasOperations && ( - - - - - - - )} - - - - - - - {t("models:message.willBeRetrainedAfterUpdate")} - - - - - - - - - - - ); -} - -EditConfirmationDialog.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - run: PropTypes.shape({ - id: PropTypes.number, - name: PropTypes.string, - }), - hasOperations: PropTypes.bool, -}; - -export default EditConfirmationDialog; diff --git a/DashAI/front/src/components/models/EditRunDialog.jsx b/DashAI/front/src/components/models/EditRunDialog.jsx deleted file mode 100644 index 6cf5c35a1..000000000 --- a/DashAI/front/src/components/models/EditRunDialog.jsx +++ /dev/null @@ -1,362 +0,0 @@ -import React, { useState, useEffect, useMemo, useCallback } from "react"; -import PropTypes from "prop-types"; -import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Stepper, - Step, - StepLabel, - TextField, - Box, - IconButton, - Typography, -} from "@mui/material"; -import { Close as CloseIcon } from "@mui/icons-material"; -import { useSnackbar } from "notistack"; -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"; - -/** - * Dialog for editing an existing model run parameters - * Step 1: Configure model name and parameters - * Step 2: Configure optimizer - */ -function EditRunDialog({ - open, - onClose, - session, - run, - existingRuns = [], - onRunUpdated, - onConfirmEdit, -}) { - const { enqueueSnackbar } = useSnackbar(); - const [activeStep, setActiveStep] = useState(0); - const [name, setName] = useState(""); - const [modelParameters, setModelParameters] = useState({}); - const [selectedOptimizer, setSelectedOptimizer] = useState(""); - const [optimizerParameters, setOptimizerParameters] = useState({}); - const [loading, setLoading] = useState(false); - const [hasUserTouchedName, setHasUserTouchedName] = useState(false); - const [goalMetric, setGoalMetric] = useState(""); - const [hasLoadedInitialParams, setHasLoadedInitialParams] = useState(false); - - const { defaultValues: defaultOptimizerParams } = useSchema({ - modelName: selectedOptimizer, - }); - - // Initialize form with existing run data - useEffect(() => { - if (open && run) { - setName(run.name || ""); - setModelParameters(run.parameters || {}); - setSelectedOptimizer(run.optimizer_name || ""); - setOptimizerParameters(run.optimizer_parameters || {}); - setGoalMetric(run.goal_metric || ""); - setHasLoadedInitialParams(true); - setHasUserTouchedName(false); - setActiveStep(0); - } - }, [open, run]); - - const hasOptimizableParams = useMemo(() => { - return Object.values(modelParameters).some( - (value) => - value && - typeof value === "object" && - !Array.isArray(value) && - value.optimize === true, - ); - }, [modelParameters]); - - const steps = hasOptimizableParams - ? ["Configure Model", "Configure Optimizer"] - : ["Configure Model"]; - - const handleModelParametersChange = useCallback((values) => { - setModelParameters(values); - }, []); - - const handleOptimizerParametersChange = useCallback((values) => { - setOptimizerParameters((prevParams) => ({ ...prevParams, ...values })); - }, []); - - useEffect(() => { - if ( - defaultOptimizerParams && - Object.keys(defaultOptimizerParams).length > 0 && - !hasLoadedInitialParams - ) { - setOptimizerParameters((prev) => { - const prevKeys = Object.keys(prev).sort().join(","); - const newKeys = Object.keys(defaultOptimizerParams).sort().join(","); - if ( - prevKeys === newKeys && - JSON.stringify(prev) === JSON.stringify(defaultOptimizerParams) - ) { - return prev; - } - return defaultOptimizerParams; - }); - } - }, [defaultOptimizerParams, hasLoadedInitialParams]); - - const handleClose = () => { - setTimeout(() => { - setActiveStep(0); - setName(""); - setModelParameters({}); - setSelectedOptimizer(""); - setOptimizerParameters({}); - setGoalMetric(""); - setHasUserTouchedName(false); - setHasLoadedInitialParams(false); - }, 100); - onClose(); - }; - - const handleNext = () => { - if (activeStep === 0) { - if (name.trim() === "") { - enqueueSnackbar("Please enter a name for the model", { - variant: "warning", - }); - return; - } - - // Check for duplicate names (excluding current run) - const nameExists = existingRuns.some( - (r) => - r.id !== run.id && - r.name && - r.name.toLowerCase() === name.trim().toLowerCase(), - ); - if (nameExists) { - enqueueSnackbar("A run with this name already exists", { - variant: "error", - }); - return; - } - - if (hasOptimizableParams) { - setActiveStep(1); - } else { - handleConfirmUpdate(); - } - } else { - handleConfirmUpdate(); - } - }; - - const handleBack = () => { - if (activeStep > 0) { - setActiveStep(activeStep - 1); - } - }; - - const handleConfirmUpdate = () => { - if (onConfirmEdit) { - // Pass the updated data to the confirmation dialog - onConfirmEdit({ - runId: run.id, - name: name.trim(), - parameters: modelParameters, - optimizer: selectedOptimizer || "", - optimizer_parameters: optimizerParameters || {}, - goal_metric: goalMetric || "", - }); - } - handleClose(); - }; - - const handleOptimizerSelected = (optimizerName, defaultValues) => { - setSelectedOptimizer(optimizerName); - if (defaultValues && Object.keys(defaultValues).length > 0) { - setOptimizerParameters(defaultValues); - } - }; - - const isStep1Valid = Boolean(run?.model_name && name.trim() !== ""); - const isStep2Valid = Boolean( - selectedOptimizer && - optimizerParameters && - Object.keys(optimizerParameters).length > 0 && - goalMetric, - ); - - if (!run) { - return null; - } - - return ( - - - - Edit Model Parameters - - - - - - - - - {steps.map((label) => ( - - {label} - - ))} - - - {activeStep === 0 && ( - - { - setName(e.target.value); - setHasUserTouchedName(true); - }} - fullWidth - required - placeholder="Model Name" - helperText={run.model_name ? `Model: ${run.model_name}` : ""} - /> - - {run.model_name && ( - - - Model Parameters - - - {}} - hideButtons - /> - - - )} - - )} - - {activeStep === 1 && ( - - - Configure Hyperparameter Optimizer - - - - - Goal Metric * - - - - - - - {selectedOptimizer && ( - - - Optimizer Parameters - - - setOptimizerParameters(values)} - onValuesChange={handleOptimizerParametersChange} - onCancel={() => {}} - hideButtons - /> - - - )} - - )} - - - - - {activeStep > 0 && ( - - )} - - - - ); -} - -EditRunDialog.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - session: PropTypes.shape({ - id: PropTypes.number, - name: PropTypes.string, - task_name: PropTypes.string, - }), - run: PropTypes.shape({ - id: PropTypes.number, - name: PropTypes.string, - model_name: PropTypes.string, - parameters: PropTypes.object, - optimizer_name: PropTypes.string, - optimizer_parameters: PropTypes.object, - goal_metric: PropTypes.string, - }), - existingRuns: PropTypes.array, - onRunUpdated: PropTypes.func, - onConfirmEdit: PropTypes.func, -}; - -export default EditRunDialog; diff --git a/DashAI/front/src/components/models/HyperparameterPlots.jsx b/DashAI/front/src/components/models/HyperparameterPlots.jsx index 53fde35f3..48c4eaf5f 100644 --- a/DashAI/front/src/components/models/HyperparameterPlots.jsx +++ b/DashAI/front/src/components/models/HyperparameterPlots.jsx @@ -61,7 +61,7 @@ function HyperparameterPlots({ run }) { } else { setLoading(false); } - }, [run.id, run.status]); + }, [run.id, run.status, run.parameters]); if (loading) { return ( diff --git a/DashAI/front/src/components/models/LiveMetricsChart.jsx b/DashAI/front/src/components/models/LiveMetricsChart.jsx index d3cc8c249..0650db344 100644 --- a/DashAI/front/src/components/models/LiveMetricsChart.jsx +++ b/DashAI/front/src/components/models/LiveMetricsChart.jsx @@ -72,7 +72,11 @@ export function LiveMetricsChart({ run }) { socketRef.current.close(); } - const ws = new WebSocket(`ws://localhost:8000/api/v1/metrics/ws/${run.id}`); + 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); diff --git a/DashAI/front/src/components/models/RunCard.jsx b/DashAI/front/src/components/models/RunCard.jsx index a9d15bc03..8f550186c 100644 --- a/DashAI/front/src/components/models/RunCard.jsx +++ b/DashAI/front/src/components/models/RunCard.jsx @@ -161,10 +161,8 @@ function RunCard({ ); enqueueSnackbar( - `Run "${editedName}" updated successfully. Status changed to Not Started.`, - { - variant: "success", - }, + t("models:message.runUpdatedSuccess", { runName: editedName }), + { variant: "success" }, ); setIsEditing(false); @@ -175,10 +173,10 @@ function RunCard({ } catch (error) { console.error("Error updating run:", error); enqueueSnackbar( - `Error updating run: ${error.message || "Unknown error"}`, - { - variant: "error", - }, + t("models:error.failedToUpdateRun", { + error: error.message || t("common:unknownError"), + }), + { variant: "error" }, ); } finally { setIsSaving(false); @@ -187,7 +185,7 @@ function RunCard({ const handleSaveEdit = async () => { if (!editedName.trim()) { - enqueueSnackbar("Run name cannot be empty", { variant: "warning" }); + enqueueSnackbar(t("models:error.runNameEmpty"), { variant: "warning" }); return; } @@ -198,22 +196,22 @@ function RunCard({ r.name.toLowerCase() === editedName.trim().toLowerCase(), ); if (nameExists) { - enqueueSnackbar("A run with this name already exists", { - variant: "error", - }); + enqueueSnackbar( + t("models:error.runNameExists", { name: editedName.trim() }), + { variant: "error" }, + ); return; } if (hasOptimizableParams) { if (!editedOptimizer) { - enqueueSnackbar( - "Please select an optimizer for optimizable parameters", - { variant: "warning" }, - ); + enqueueSnackbar(t("models:error.selectOptimizerRequired"), { + variant: "warning", + }); return; } if (!editedGoalMetric) { - enqueueSnackbar("Please select a goal metric for optimization", { + enqueueSnackbar(t("models:error.selectGoalMetricRequired"), { variant: "warning", }); return; diff --git a/DashAI/front/src/components/models/RunResults.jsx b/DashAI/front/src/components/models/RunResults.jsx index 85367fa57..36e086773 100644 --- a/DashAI/front/src/components/models/RunResults.jsx +++ b/DashAI/front/src/components/models/RunResults.jsx @@ -28,6 +28,7 @@ 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, @@ -58,6 +59,7 @@ export default function RunResults({ 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; @@ -156,7 +158,9 @@ export default function RunResults({ endIcon={resultsVisible ? : } sx={{ textTransform: "none" }} > - {resultsVisible ? "Hide Results" : "Show Results"} + {resultsVisible + ? t("models:label.hideResults") + : t("models:label.showResults")} {isFinished && ( )} @@ -170,11 +174,11 @@ export default function RunResults({ onChange={(e, newValue) => setActiveTab(newValue)} aria-label="Results tabs" > - + - Explainability + {t("models:label.explainability")} {isFinished && ( - Predictions + {t("models:label.predictions")} {isFinished && ( @@ -238,7 +242,7 @@ export default function RunResults({ > - Global Explainers + {t("models:label.globalExplainers")} setGlobalDialogOpen(true)} fullWidth > - New Global Explainer + {t("models:button.createGlobalExplainer")} {globalExplainers.length === 0 ? ( - No global explainers yet + {t("models:label.noGlobalExplainersYet")} ) : ( globalExplainers.map((explainer) => ( @@ -302,7 +306,7 @@ export default function RunResults({ > - Local Explainers + {t("models:label.localExplainers")} setLocalDialogOpen(true)} fullWidth > - New Local Explainer + {t("models:button.createLocalExplainer")} {localExplainers.length === 0 ? ( - No local explainers yet + {t("models:label.noLocalExplainersYet")} ) : ( localExplainers.map((explainer) => ( @@ -371,7 +375,7 @@ export default function RunResults({ > - Dataset Predictions + {t("models:label.datasetPredictions")} p.dataset_id).length} @@ -388,7 +392,7 @@ export default function RunResults({ onClick={() => setDatasetPredictionDialogOpen(true)} fullWidth > - New Dataset Prediction + {t("models:button.newDatasetPrediction")} {predictions.filter((p) => p.dataset_id).length === 0 ? ( - No dataset predictions yet + {t("models:label.noDatasetPredictionsYet")} ) : ( predictions @@ -435,7 +439,7 @@ export default function RunResults({ > - Manual Predictions + {t("models:label.manualPredictions")} !p.dataset_id).length} @@ -452,7 +456,7 @@ export default function RunResults({ onClick={() => setManualPredictionDialogOpen(true)} fullWidth > - New Manual Prediction + {t("models:button.newManualPrediction")} {predictions.filter((p) => !p.dataset_id).length === 0 ? ( - No manual predictions yet + {t("models:label.noManualPredictionsYet")} ) : ( predictions @@ -539,6 +543,8 @@ RunResults.propTypes = { 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, diff --git a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx index 1e0081c0f..2551d3507 100644 --- a/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx +++ b/DashAI/front/src/components/threeSectionLayout/BarHeader.jsx @@ -1,4 +1,4 @@ -import { Box, Typography } from "@mui/material"; +import { Box } from "@mui/material"; export default function BarHeader() { return ( diff --git a/DashAI/front/src/utils/i18n/locales/en/models.json b/DashAI/front/src/utils/i18n/locales/en/models.json index 712346f2c..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", @@ -45,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" @@ -59,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}} explainer", "explainersCount_other": "• <1>{{count}} explainers", @@ -68,21 +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", @@ -103,6 +116,7 @@ "retrainWillDeleteOperations": "This run has existing operations that will be deleted", "retrainWillDeleteOperationsDetails": "Re-training run \"<1>{{runName}}\" will delete:", "saveParameterChanges": "Save Parameter Changes?", + "showResults": "Show Results", "saveWillDeleteOperationsDetails": "Saving \"<1>{{runName}}\" will reset the run. The following will be deleted when you train again:", "runDetails": "Run Details", "runExperimentToSeeMetrics": "Go to<1>experiments tabto run your experiment.", @@ -157,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/models.json b/DashAI/front/src/utils/i18n/locales/es/models.json index d28498e9d..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", @@ -45,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" @@ -59,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}} explicador", "explainersCount_other": "• <1>{{count}} explicadores", @@ -68,21 +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", @@ -103,6 +116,7 @@ "retrainWillDeleteOperations": "Esta ejecución tiene operaciones existentes que serán eliminadas", "retrainWillDeleteOperationsDetails": "Re-entrenar la ejecución \"<1>{{runName}}\" eliminará:", "saveParameterChanges": "¿Guardar Cambios de Parámetros?", + "showResults": "Mostrar Resultados", "saveWillDeleteOperationsDetails": "Guardar \"<1>{{runName}}\" 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 experimentos para ejecutar tu experimento.", @@ -158,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",