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