diff --git a/.gitignore b/.gitignore
index 5baf00eea..54c488e68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -200,3 +200,5 @@ docs/node_modules/
# Re-include docs content section named "build" (not a build output)
!docs/docs/build/
!docs/docs/build/**
+!docs/i18n/**/build/
+!docs/i18n/**/build/**
diff --git a/DashAI/back/dataloaders/classes/csv_dataloader.py b/DashAI/back/dataloaders/classes/csv_dataloader.py
index 2c67bb145..6d48df790 100644
--- a/DashAI/back/dataloaders/classes/csv_dataloader.py
+++ b/DashAI/back/dataloaders/classes/csv_dataloader.py
@@ -30,21 +30,6 @@ class CSVDataloaderSchema(BaseSchema):
``pandas.read_csv``.
"""
- name: schema_field(
- string_field(),
- "",
- description=MultilingualString(
- en=(
- "Custom name to register your dataset. If no name is specified, "
- "the name of the uploaded file will be used."
- ),
- es=(
- "Nombre personalizado para registrar su dataset. Si no se especifica "
- "un nombre, se usará el nombre del archivo subido."
- ),
- ),
- alias=MultilingualString(en="Name", es="Nombre"),
- ) # type: ignore
separator: schema_field(
enum_field([",", ";", "blank space", "tab"]),
",",
diff --git a/DashAI/back/dataloaders/classes/dataloader.py b/DashAI/back/dataloaders/classes/dataloader.py
index 1e7abb5ee..467c04e99 100644
--- a/DashAI/back/dataloaders/classes/dataloader.py
+++ b/DashAI/back/dataloaders/classes/dataloader.py
@@ -5,6 +5,7 @@
from typing import TYPE_CHECKING, Any, Dict, Final
from DashAI.back.config_object import ConfigObject
+from DashAI.back.core.utils import MultilingualString
logger = logging.getLogger(__name__)
@@ -18,6 +19,16 @@ class BaseDataLoader(ConfigObject):
"""Abstract class with base methods for DashAI dataloaders."""
TYPE: Final[str] = "DataLoader"
+ CATEGORY: Final = MultilingualString(
+ en="File Uploading",
+ es="Carga de Archivos",
+ )
+
+ @classmethod
+ def get_metadata(cls) -> Dict[str, Any]:
+ return {
+ "category": cls.CATEGORY if cls.CATEGORY else "File Uploading",
+ }
@abstractmethod
def load_data(
diff --git a/DashAI/back/dataloaders/classes/excel_dataloader.py b/DashAI/back/dataloaders/classes/excel_dataloader.py
index 6fa352937..183ea1a29 100644
--- a/DashAI/back/dataloaders/classes/excel_dataloader.py
+++ b/DashAI/back/dataloaders/classes/excel_dataloader.py
@@ -27,21 +27,6 @@ class ExcelDataloaderSchema(BaseSchema):
leaving the sheet field empty selects the first sheet.
"""
- name: schema_field(
- string_field(),
- "",
- description=MultilingualString(
- en=(
- "Custom name to register your dataset. If no name is specified, "
- "the name of the uploaded file will be used."
- ),
- es=(
- "Nombre personalizado para registrar su dataset. Si no se especifica "
- "un nombre, se usará el nombre del archivo subido."
- ),
- ),
- alias=MultilingualString(en="Name", es="Nombre"),
- ) # type: ignore
sheet: schema_field(
union_type(int_field(ge=0), string_field()),
placeholder=0,
diff --git a/DashAI/back/dataloaders/classes/json_dataloader.py b/DashAI/back/dataloaders/classes/json_dataloader.py
index ac1bb22ff..897b43eae 100644
--- a/DashAI/back/dataloaders/classes/json_dataloader.py
+++ b/DashAI/back/dataloaders/classes/json_dataloader.py
@@ -19,21 +19,6 @@ class JSONDataloaderSchema(BaseSchema):
``None``, the entire JSON value is interpreted as the record list.
"""
- name: schema_field(
- string_field(),
- "",
- description=MultilingualString(
- en=(
- "Custom name to register your dataset. If no name is specified, "
- "the name of the uploaded file will be used."
- ),
- es=(
- "Nombre personalizado para registrar su dataset. Si no se especifica "
- "un nombre, se usará el nombre del archivo subido."
- ),
- ),
- alias=MultilingualString(en="Name", es="Nombre"),
- ) # type: ignore
data_key: schema_field(
none_type(string_field()),
placeholder="data",
diff --git a/DashAI/back/initial_components.py b/DashAI/back/initial_components.py
index 0a01fb093..3ab31b82f 100644
--- a/DashAI/back/initial_components.py
+++ b/DashAI/back/initial_components.py
@@ -123,21 +123,44 @@
from DashAI.back.metrics.translation.ter import Ter
# Models
+from DashAI.back.models.hugging_face.albert_transformer import AlbertTransformer
+from DashAI.back.models.hugging_face.bert_transformer import BertTransformer
+from DashAI.back.models.hugging_face.bertin_transformer import BertinTransformer
+from DashAI.back.models.hugging_face.beto_transformer import BetoTransformer
from DashAI.back.models.hugging_face.deberta_v3_transformer import DebertaV3Transformer
from DashAI.back.models.hugging_face.distilbert_transformer import DistilBertTransformer
+from DashAI.back.models.hugging_face.electra_transformer import ElectraTransformer
from DashAI.back.models.hugging_face.llama_model import LlamaModel
+from DashAI.back.models.hugging_face.m2m100_transformer import M2M100Transformer
+from DashAI.back.models.hugging_face.minilm_transformer import MiniLMTransformer
from DashAI.back.models.hugging_face.mistral_model import MistralModel
from DashAI.back.models.hugging_face.mixtral_model import MixtralModel
from DashAI.back.models.hugging_face.modernbert_transformer import ModernBertTransformer
+from DashAI.back.models.hugging_face.multilingual_bert_transformer import (
+ MultilingualBertTransformer,
+)
from DashAI.back.models.hugging_face.nllb_transformer import NllbTransformer
+from DashAI.back.models.hugging_face.opus_mt_en_de_transformer import (
+ OpusMtEnDeTransformer,
+)
from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
OpusMtEnESTransformer,
)
+from DashAI.back.models.hugging_face.opus_mt_en_fr_transformer import (
+ OpusMtEnFrTransformer,
+)
+from DashAI.back.models.hugging_face.opus_mt_en_pt_transformer import (
+ OpusMtEnPtTransformer,
+)
from DashAI.back.models.hugging_face.opus_mt_es_en_transformer import (
OpusMtEsENTransformer,
)
+from DashAI.back.models.hugging_face.opus_mt_fr_en_transformer import (
+ OpusMtFrEnTransformer,
+)
from DashAI.back.models.hugging_face.pixart_sigma_model import PixArtSigmaModel
from DashAI.back.models.hugging_face.qwen_model import QwenModel
+from DashAI.back.models.hugging_face.roberta_transformer import RobertaTransformer
from DashAI.back.models.hugging_face.sd15_depth_controlnet_model import (
SD15DepthControlNetModel,
)
@@ -164,24 +187,52 @@
from DashAI.back.models.hugging_face.stable_diffusion_xl_model import (
StableDiffusionXLModel,
)
+from DashAI.back.models.hugging_face.t5_small_transformer import T5SmallTransformer
from DashAI.back.models.hugging_face.tongyi_z_image_model import TongyiZImageModel
+from DashAI.back.models.hugging_face.xlm_roberta_transformer import (
+ XlmRobertaTransformer,
+)
+from DashAI.back.models.hugging_face.xlnet_transformer import XlnetTransformer
+from DashAI.back.models.scikit_learn.adaboost_classifier import AdaBoostClassifier
+from DashAI.back.models.scikit_learn.adaboost_regression import AdaBoostRegression
+from DashAI.back.models.scikit_learn.bagging_classifier import BaggingClassifier
+from DashAI.back.models.scikit_learn.bayesian_ridge_regression import (
+ BayesianRidgeRegression,
+)
from DashAI.back.models.scikit_learn.bow_text_classification_model import (
BagOfWordsTextClassificationModel,
)
from DashAI.back.models.scikit_learn.decision_tree_classifier import (
DecisionTreeClassifier,
)
+from DashAI.back.models.scikit_learn.decision_tree_regression import (
+ DecisionTreeRegression,
+)
from DashAI.back.models.scikit_learn.dummy_classifier import DummyClassifier
+from DashAI.back.models.scikit_learn.elastic_net_regression import ElasticNetRegression
+from DashAI.back.models.scikit_learn.extra_trees_classifier import ExtraTreesClassifier
+from DashAI.back.models.scikit_learn.extra_trees_regression import ExtraTreesRegression
+from DashAI.back.models.scikit_learn.gaussian_nb import GaussianNB
+from DashAI.back.models.scikit_learn.gradient_boosting_classifier import (
+ GradientBoostingClassifier,
+)
from DashAI.back.models.scikit_learn.gradient_boosting_regression import (
GradientBoostingR,
)
from DashAI.back.models.scikit_learn.hist_gradient_boosting_classifier import (
HistGradientBoostingClassifier,
)
+from DashAI.back.models.scikit_learn.hist_gradient_boosting_regression import (
+ HistGradientBoostingRegression,
+)
from DashAI.back.models.scikit_learn.k_neighbors_classifier import KNeighborsClassifier
+from DashAI.back.models.scikit_learn.k_neighbors_regression import KNeighborsRegression
+from DashAI.back.models.scikit_learn.lasso_regression import LassoRegression
from DashAI.back.models.scikit_learn.linear_regression import LinearRegression
+from DashAI.back.models.scikit_learn.linear_svc_classifier import LinearSVCClassifier
from DashAI.back.models.scikit_learn.linearSVR import LinearSVR
from DashAI.back.models.scikit_learn.logistic_regression import LogisticRegression
+from DashAI.back.models.scikit_learn.mlp_classifier import MLPClassifier
from DashAI.back.models.scikit_learn.mlp_regression import MLPRegression
from DashAI.back.models.scikit_learn.random_forest_classifier import (
RandomForestClassifier,
@@ -190,7 +241,12 @@
RandomForestRegression,
)
from DashAI.back.models.scikit_learn.ridge_regression import RidgeRegression
+from DashAI.back.models.scikit_learn.sgd_classifier import SGDClassifier
from DashAI.back.models.scikit_learn.svc import SVC
+from DashAI.back.models.scikit_learn.svr import SVR
+from DashAI.back.models.scikit_learn.tfidf_logreg_text_classification_model import (
+ TfIdfLogRegTextClassificationModel,
+)
# Optimizers
from DashAI.back.optimizers.hyperopt_optimizer import HyperOptOptimizer
@@ -240,42 +296,76 @@ def get_initial_components():
TextToTextGenerationTask,
ControlNetTask,
# Models
- SVC,
+ AdaBoostClassifier,
+ AlbertTransformer,
+ AdaBoostRegression,
+ BaggingClassifier,
+ BagOfWordsTextClassificationModel,
+ BertTransformer,
+ BertinTransformer,
+ BetoTransformer,
+ BayesianRidgeRegression,
+ DebertaV3Transformer,
DecisionTreeClassifier,
+ DecisionTreeRegression,
+ DistilBertTransformer,
DummyClassifier,
+ ElasticNetRegression,
+ ElectraTransformer,
+ ExtraTreesClassifier,
+ ExtraTreesRegression,
+ GaussianNB,
+ GradientBoostingClassifier,
GradientBoostingR,
HistGradientBoostingClassifier,
+ HistGradientBoostingRegression,
KNeighborsClassifier,
- QwenModel,
+ KNeighborsRegression,
+ LassoRegression,
+ LinearRegression,
+ LinearSVCClassifier,
+ LinearSVR,
LlamaModel,
+ LogisticRegression,
+ M2M100Transformer,
+ MiniLMTransformer,
MistralModel,
MixtralModel,
- SmolLMModel,
- StableDiffusionV2Model,
- StableDiffusionV3Model,
- StableDiffusionXLModel,
- SDXLTurboModel,
- PixArtSigmaModel,
- TongyiZImageModel,
- StableDiffusionXLV1ControlNet,
- SD15DepthControlNetModel,
- SD15OpenPoseControlNetModel,
- SD15HEDControlNetModel,
- SDXLCannyControlNetModel,
- LogisticRegression,
+ MultilingualBertTransformer,
+ MLPClassifier,
MLPRegression,
- RandomForestClassifier,
- RandomForestRegression,
- DistilBertTransformer,
ModernBertTransformer,
- DebertaV3Transformer,
+ NllbTransformer,
+ OpusMtEnDeTransformer,
OpusMtEnESTransformer,
+ OpusMtEnFrTransformer,
+ OpusMtEnPtTransformer,
OpusMtEsENTransformer,
- NllbTransformer,
- BagOfWordsTextClassificationModel,
+ OpusMtFrEnTransformer,
+ PixArtSigmaModel,
+ QwenModel,
+ RandomForestClassifier,
+ RobertaTransformer,
+ RandomForestRegression,
RidgeRegression,
- LinearSVR,
- LinearRegression,
+ SD15DepthControlNetModel,
+ SD15HEDControlNetModel,
+ SD15OpenPoseControlNetModel,
+ SDXLCannyControlNetModel,
+ SDXLTurboModel,
+ SGDClassifier,
+ SmolLMModel,
+ StableDiffusionV2Model,
+ StableDiffusionV3Model,
+ StableDiffusionXLModel,
+ StableDiffusionXLV1ControlNet,
+ SVC,
+ SVR,
+ T5SmallTransformer,
+ TfIdfLogRegTextClassificationModel,
+ TongyiZImageModel,
+ XlmRobertaTransformer,
+ XlnetTransformer,
# Dataloaders
CSVDataLoader,
JSONDataLoader,
diff --git a/DashAI/back/metrics/classification/log_loss.py b/DashAI/back/metrics/classification/log_loss.py
index 37e9f4ab5..094cce5dd 100644
--- a/DashAI/back/metrics/classification/log_loss.py
+++ b/DashAI/back/metrics/classification/log_loss.py
@@ -86,4 +86,8 @@ def score(
from sklearn.metrics import log_loss
true_labels, _ = prepare_to_metric(true_labels, probs_pred_labels)
- return log_loss(true_labels, probs_pred_labels)
+ # Pass all expected class indices so log_loss works even when a split
+ # happens to contain only one class (e.g. small validation sets).
+ n_classes = probs_pred_labels.shape[1]
+ labels = list(range(n_classes))
+ return log_loss(true_labels, probs_pred_labels, labels=labels)
diff --git a/DashAI/back/metrics/classification/roc_auc.py b/DashAI/back/metrics/classification/roc_auc.py
index c5c79e77d..5d86d2ef7 100644
--- a/DashAI/back/metrics/classification/roc_auc.py
+++ b/DashAI/back/metrics/classification/roc_auc.py
@@ -80,14 +80,26 @@ def score(
float
RoC AUC score between true labels and predicted labels
"""
+ import numpy as np
+
true_labels, _ = prepare_to_metric(true_labels, probs_pred_labels)
- # Use the provided multiclass parameter or determine it using is_multiclass
- if multiclass is None:
- multiclass = ClassificationMetric.is_multiclass(true_labels)
+
+ # ROC AUC is undefined when only one class is present in y_true.
+ if len(np.unique(true_labels)) < 2:
+ return float("nan")
from sklearn.metrics import roc_auc_score
+ n_classes = probs_pred_labels.shape[1]
+
+ if multiclass is None:
+ multiclass = n_classes > 2
if multiclass:
- return roc_auc_score(true_labels, probs_pred_labels, multi_class="ovr")
+ return roc_auc_score(
+ true_labels,
+ probs_pred_labels,
+ multi_class="ovr",
+ labels=list(range(n_classes)),
+ )
else:
return roc_auc_score(true_labels, probs_pred_labels[:, 1])
diff --git a/DashAI/back/models/hugging_face/albert_transformer.py b/DashAI/back/models/hugging_face/albert_transformer.py
new file mode 100644
index 000000000..15e6d8199
--- /dev/null
+++ b/DashAI/back/models/hugging_face/albert_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of ALBERT model for English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class AlbertTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained ALBERT model for efficient English text classification.
+
+ ALBERT (A Lite BERT) reduces BERT's parameters via cross-layer parameter
+ sharing and factorised embedding parametrisation, making it significantly
+ smaller and faster while retaining high accuracy. Requires the
+ ``sentencepiece`` package for its tokeniser.
+
+ References
+ ----------
+ - [1] Lan, Z. et al. (2020). "ALBERT: A Lite BERT for Self-supervised
+ Learning of Language Representations." ICLR 2020.
+ - [2] https://huggingface.co/albert-base-v2
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="ALBERT Transformer",
+ es="Transformer ALBERT",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Parameter-efficient BERT variant for English text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Variante de BERT eficiente en parámetros para clasificación en inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#00838F"
+ ICON: str = "Speed"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "albert-base-v2"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_albert"
diff --git a/DashAI/back/models/hugging_face/base_opus_mt_transformer.py b/DashAI/back/models/hugging_face/base_opus_mt_transformer.py
new file mode 100644
index 000000000..a3d4f223d
--- /dev/null
+++ b/DashAI/back/models/hugging_face/base_opus_mt_transformer.py
@@ -0,0 +1,261 @@
+"""Shared base class for Helsinki-NLP Opus-MT translation transformers."""
+
+import shutil
+import tempfile
+from pathlib import Path
+from typing import TYPE_CHECKING, List, Optional, Union
+
+from sklearn.exceptions import NotFittedError
+
+from DashAI.back.models.translation_model import TranslationModel
+from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER
+
+if TYPE_CHECKING:
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+
+class OpusMtTransformerMixin(TranslationModel):
+ """Shared implementation for Helsinki-NLP Opus-MT translation wrappers.
+
+ Subclasses must define ``MODEL_NAME`` (the HuggingFace checkpoint ID) and
+ ``SCHEMA``. ``TEMP_CHECKPOINT_DIR`` defaults to a generic path but should
+ be overridden with a model-specific directory to avoid collisions between
+ concurrent training runs of different language pairs.
+
+ All seq2seq training, tokenization, inference, save, and load logic lives
+ here so each language-pair subclass only needs to set class attributes.
+
+ .. note::
+ Requires internet access on first use to download pretrained weights
+ from the Hugging Face Hub.
+ """
+
+ MODEL_NAME: str = ""
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus_mt"
+
+ def __init__(self, model=None, **kwargs):
+ """Initialize tokenizer and seq2seq model.
+
+ Parameters
+ ----------
+ model : transformers.PreTrainedModel or None
+ Pre-loaded model to reuse instead of downloading weights.
+ **kwargs
+ Training hyperparameters forwarded to ``validate_and_transform``.
+ """
+ kwargs = self.validate_and_transform(kwargs)
+
+ from transformers import AutoTokenizer
+
+ if not self.MODEL_NAME:
+ raise ValueError(
+ f"{self.__class__.__name__} must define a non-empty MODEL_NAME."
+ )
+
+ self.model_name = self.MODEL_NAME
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
+
+ self.training_args = {
+ "num_train_epochs": kwargs.get("num_train_epochs", 2),
+ "learning_rate": kwargs.get("learning_rate", 2e-5),
+ "weight_decay": kwargs.get("weight_decay", 0.01),
+ }
+ self.batch_size = kwargs.get("batch_size", 4)
+ self.device = kwargs.get("device") or GPU_OR_CPU_PLACEHOLDER
+ self.log_train_every_n_epochs = kwargs.get("log_train_every_n_epochs", 1)
+ self.log_train_every_n_steps = kwargs.get("log_train_every_n_steps", None)
+ self.log_validation_every_n_epochs = kwargs.get(
+ "log_validation_every_n_epochs", 1
+ )
+ self.log_validation_every_n_steps = kwargs.get(
+ "log_validation_every_n_steps", None
+ )
+
+ if model is None:
+ from transformers import AutoModelForSeq2SeqLM
+
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
+ else:
+ self.model = model
+
+ self.num_train_epochs = self.training_args.get("num_train_epochs", 2)
+ self.fitted = model is not None
+
+ def tokenize_data(
+ self, x: "DashAIDataset", y: Optional["DashAIDataset"] = None
+ ) -> "DashAIDataset":
+ """Tokenize source (and optionally target) dataset for seq2seq training."""
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+ is_y = bool(y)
+ if not y:
+ y = DashAIDataset.from_list([{"foo": 0}] * len(x))
+
+ dataset = []
+ input_column_name = x.column_names[0]
+ output_column_name = y.column_names[0] if is_y else None
+
+ for i, input_sample in enumerate(x):
+ tokenized_input = self.tokenizer(
+ input_sample[input_column_name],
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample = {
+ "input_ids": tokenized_input["input_ids"],
+ "attention_mask": tokenized_input["attention_mask"],
+ }
+ if is_y:
+ output_sample = y[i]
+ tokenized_output = self.tokenizer(
+ output_sample[output_column_name],
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample["labels"] = tokenized_output["input_ids"]
+ dataset.append(sample)
+
+ return DashAIDataset.from_list(dataset)
+
+ def train(
+ self,
+ x_train: "DashAIDataset",
+ y_train: "DashAIDataset",
+ x_validation: "DashAIDataset" = None,
+ y_validation: "DashAIDataset" = None,
+ ):
+ """Fine-tune the Opus-MT model on translation data."""
+ from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
+
+ from DashAI.back.models.hugging_face.metrics_callback import MetricsCallback
+
+ dataset = self.tokenize_data(x_train, y_train)
+ dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
+
+ has_validation_data = x_validation is not None and y_validation is not None
+
+ output_root = Path(self.TEMP_CHECKPOINT_DIR)
+ output_root.mkdir(parents=True, exist_ok=True)
+ run_output_dir = tempfile.mkdtemp(
+ prefix=f"{self.__class__.__name__.lower()}_",
+ dir=str(output_root),
+ )
+
+ training_args_obj = Seq2SeqTrainingArguments(
+ output_dir=run_output_dir,
+ save_steps=1,
+ save_total_limit=1,
+ per_device_train_batch_size=self.batch_size,
+ per_device_eval_batch_size=self.batch_size,
+ use_cpu=self.device.lower() != "gpu",
+ **self.training_args,
+ )
+
+ metrics_callback = MetricsCallback(
+ model_instance=self,
+ x_train=x_train,
+ y_train=y_train,
+ x_val=x_validation,
+ y_val=y_validation,
+ total_epochs=self.num_train_epochs,
+ log_training_every_n_epochs=self.log_train_every_n_epochs,
+ log_training_every_n_steps=self.log_train_every_n_steps,
+ log_val_every_n_epochs=(
+ self.log_validation_every_n_epochs if has_validation_data else None
+ ),
+ log_val_every_n_steps=(
+ self.log_validation_every_n_steps if has_validation_data else None
+ ),
+ )
+
+ trainer = Seq2SeqTrainer(
+ model=self.model,
+ args=training_args_obj,
+ train_dataset=dataset,
+ callbacks=[metrics_callback],
+ )
+
+ self.fitted = True
+ try:
+ trainer.train()
+ finally:
+ shutil.rmtree(run_output_dir, ignore_errors=True)
+
+ return self
+
+ def predict(self, x_pred: "DashAIDataset") -> List:
+ """Translate source texts using the fine-tuned model."""
+ if not self.fitted:
+ raise NotFittedError(
+ f"This {self.__class__.__name__} instance is not fitted yet. "
+ "Call 'train' with appropriate arguments before using this estimator."
+ )
+
+ dataset = self.tokenize_data(x_pred)
+ dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
+
+ translations = []
+ for example in dataset:
+ inputs = {
+ k: v.unsqueeze(0).to(self.model.device) for k, v in example.items()
+ }
+ outputs = self.model.generate(**inputs)
+ translated_text = self.tokenizer.decode(
+ outputs[0], skip_special_tokens=True
+ )
+ translations.append(translated_text)
+
+ return translations
+
+ def prepare_dataset(
+ self, dataset: "DashAIDataset", is_fit: bool = False
+ ) -> "DashAIDataset":
+ """Return the dataset unchanged (no preprocessing required)."""
+ return dataset
+
+ def save(self, filename: Union[str, "Path"]) -> None:
+ """Persist model weights and hyperparameters to disk."""
+ from transformers import AutoConfig
+
+ save_dir = Path(filename)
+ if save_dir.exists() and save_dir.is_file():
+ save_dir.unlink()
+ save_dir.mkdir(parents=True, exist_ok=True)
+
+ self.model.save_pretrained(save_dir)
+ config = AutoConfig.from_pretrained(save_dir)
+ config.custom_params = {
+ "num_train_epochs": self.training_args.get("num_train_epochs"),
+ "batch_size": self.batch_size,
+ "learning_rate": self.training_args.get("learning_rate"),
+ "device": self.device,
+ "weight_decay": self.training_args.get("weight_decay"),
+ "fitted": self.fitted,
+ }
+ config.save_pretrained(save_dir)
+
+ @classmethod
+ def load(cls, filename: Union[str, "Path"]):
+ """Restore a model instance from disk."""
+ from transformers import AutoConfig, AutoModelForSeq2SeqLM
+
+ model = AutoModelForSeq2SeqLM.from_pretrained(filename)
+ config = AutoConfig.from_pretrained(filename)
+ custom_params = getattr(config, "custom_params", {})
+
+ loaded_model = cls(
+ model=model,
+ num_train_epochs=custom_params.get("num_train_epochs"),
+ batch_size=custom_params.get("batch_size"),
+ learning_rate=custom_params.get("learning_rate"),
+ device=custom_params.get("device"),
+ weight_decay=custom_params.get("weight_decay"),
+ log_train_every_n_epochs=None,
+ log_train_every_n_steps=None,
+ log_validation_every_n_epochs=None,
+ log_validation_every_n_steps=None,
+ )
+ loaded_model.fitted = custom_params.get("fitted", False)
+ return loaded_model
diff --git a/DashAI/back/models/hugging_face/base_text_classification_transformer.py b/DashAI/back/models/hugging_face/base_text_classification_transformer.py
index 6f8dc5707..c378d627f 100644
--- a/DashAI/back/models/hugging_face/base_text_classification_transformer.py
+++ b/DashAI/back/models/hugging_face/base_text_classification_transformer.py
@@ -30,6 +30,10 @@ class HuggingFaceTextClassificationTransformer(TextClassificationModel):
- Training via ``transformers.Trainer`` with DashAI metric callbacks.
- Inference returning per-class probability matrices.
- Save/load utilities that preserve custom training parameters.
+
+ .. note::
+ Requires internet access on first use to download pre-trained weights
+ from the Hugging Face Hub.
"""
MODEL_NAME: str = ""
diff --git a/DashAI/back/models/hugging_face/bert_transformer.py b/DashAI/back/models/hugging_face/bert_transformer.py
new file mode 100644
index 000000000..a02bac698
--- /dev/null
+++ b/DashAI/back/models/hugging_face/bert_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of BERT model for English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class BertTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained BERT model for English text classification.
+
+ BERT (Bidirectional Encoder Representations from Transformers) pre-trains deep
+ bidirectional representations by jointly conditioning on both left and right
+ context in all layers. Fine-tuned BERT achieves strong results on a wide range
+ of text classification tasks.
+
+ References
+ ----------
+ - [1] Devlin, J. et al. (2019). "BERT: Pre-training of Deep Bidirectional
+ Transformers for Language Understanding." NAACL 2019.
+ - [2] https://huggingface.co/bert-base-uncased
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="BERT Transformer",
+ es="Transformer BERT",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Bidirectional BERT model for English text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Modelo BERT bidireccional para clasificación de texto en inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#1565C0"
+ ICON: str = "Psychology"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "bert-base-uncased"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_bert"
diff --git a/DashAI/back/models/hugging_face/bertin_transformer.py b/DashAI/back/models/hugging_face/bertin_transformer.py
new file mode 100644
index 000000000..5f0c72d53
--- /dev/null
+++ b/DashAI/back/models/hugging_face/bertin_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of BERTIN model for Spanish text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class BertinTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained BERTIN model (Spanish RoBERTa) for Spanish text classification.
+
+ BERTIN is a Spanish RoBERTa model trained on the Spanish portion of mC4 and
+ additional Spanish corpora. It applies RoBERTa's improved training recipe to
+ Spanish and typically outperforms BETO on Spanish NLP benchmarks. Requires
+ the ``sentencepiece`` package for its tokeniser.
+
+ References
+ ----------
+ - [1] de la Rosa, J. et al. (2022). "BERTIN: Efficient Pre-Training of a
+ Spanish Language Model using Perplexity Sampling."
+ - [2] https://huggingface.co/bertin-project/bertin-roberta-base-spanish
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="BERTIN Spanish RoBERTa",
+ es="BERTIN RoBERTa en Español",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Spanish RoBERTa (BERTIN) pre-trained on large Spanish corpora. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "RoBERTa en español (BERTIN) pre-entrenada en grandes corpus en español. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#AD1457"
+ ICON: str = "RecordVoiceOver"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "bertin-project/bertin-roberta-base-spanish"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_bertin"
diff --git a/DashAI/back/models/hugging_face/beto_transformer.py b/DashAI/back/models/hugging_face/beto_transformer.py
new file mode 100644
index 000000000..7e1d87e93
--- /dev/null
+++ b/DashAI/back/models/hugging_face/beto_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of BETO model for Spanish text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class BetoTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained BETO model for Spanish text classification.
+
+ BETO is a Spanish BERT trained on the Spanish Wikipedia and other Spanish
+ corpora using the whole-word masking strategy. It achieves state-of-the-art
+ results on several Spanish NLP benchmarks. Particularly useful for tasks
+ involving Spanish text.
+
+ References
+ ----------
+ - [1] Cañete, J. et al. (2020). "Spanish Pre-Trained BERT Model and
+ Evaluation Data." PML4DC at ICLR 2020.
+ - [2] https://huggingface.co/dccuchile/bert-base-spanish-wwm-cased
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="BETO Spanish BERT",
+ es="BETO BERT en Español",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Spanish BERT (BETO) pre-trained on Spanish corpora. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "BERT en español (BETO) pre-entrenado en corpus en español. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#C62828"
+ ICON: str = "RecordVoiceOver"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "dccuchile/bert-base-spanish-wwm-cased"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_beto"
diff --git a/DashAI/back/models/hugging_face/electra_transformer.py b/DashAI/back/models/hugging_face/electra_transformer.py
new file mode 100644
index 000000000..a774c8ac6
--- /dev/null
+++ b/DashAI/back/models/hugging_face/electra_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of ELECTRA model for English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class ElectraTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained ELECTRA model for efficient English text classification.
+
+ ELECTRA uses a replaced-token-detection pre-training objective: a generator
+ produces plausible token replacements while a discriminator is trained to
+ identify which tokens were replaced. This allows ELECTRA to train on all
+ input tokens rather than only masked ones, making pre-training more efficient.
+
+ References
+ ----------
+ - [1] Clark, K. et al. (2020). "ELECTRA: Pre-training Text Encoders as
+ Discriminators Rather Than Generators." ICLR 2020.
+ - [2] https://huggingface.co/google/electra-small-discriminator
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="ELECTRA Transformer",
+ es="Transformer ELECTRA",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Sample-efficient ELECTRA discriminator for text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Discriminador ELECTRA eficiente en muestras para clasificación de texto. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#558B2F"
+ ICON: str = "ElectricBolt"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "google/electra-small-discriminator"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_electra"
diff --git a/DashAI/back/models/hugging_face/m2m100_transformer.py b/DashAI/back/models/hugging_face/m2m100_transformer.py
new file mode 100644
index 000000000..097a222cc
--- /dev/null
+++ b/DashAI/back/models/hugging_face/m2m100_transformer.py
@@ -0,0 +1,327 @@
+"""M2M100 multilingual translation transformer for DashAI."""
+
+import shutil
+import tempfile
+from pathlib import Path
+from typing import TYPE_CHECKING, List, Optional, Union
+
+from sklearn.exceptions import NotFittedError
+
+from DashAI.back.core.schema_fields import schema_field, string_field
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+from DashAI.back.models.translation_model import TranslationModel
+from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER
+
+if TYPE_CHECKING:
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+
+class M2M100TransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the M2M100 multilingual translation model.
+
+ Extends the standard translation schema with ``source_language`` and
+ ``target_language`` fields, which accept ISO 639-1 language codes
+ (e.g. ``"en"`` for English, ``"es"`` for Spanish, ``"fr"`` for French).
+ """
+
+ source_language: schema_field(
+ string_field(),
+ placeholder="en",
+ description=MultilingualString(
+ en=(
+ "Source language ISO 639-1 code (e.g. 'en', 'es', 'fr', 'de'). "
+ "Supports 100 languages."
+ ),
+ es=(
+ "Código ISO 639-1 del idioma de origen (ej. 'en', 'es', 'fr', 'de'). "
+ "Soporta 100 idiomas."
+ ),
+ ),
+ alias=MultilingualString(en="Source language", es="Idioma de origen"),
+ ) # type: ignore
+ target_language: schema_field(
+ string_field(),
+ placeholder="es",
+ description=MultilingualString(
+ en=(
+ "Target language ISO 639-1 code (e.g. 'en', 'es', 'fr', 'de'). "
+ "Supports 100 languages."
+ ),
+ es=(
+ "Código ISO 639-1 del idioma destino (ej. 'en', 'es', 'fr', 'de'). "
+ "Soporta 100 idiomas."
+ ),
+ ),
+ alias=MultilingualString(en="Target language", es="Idioma destino"),
+ ) # type: ignore
+
+
+class M2M100Transformer(TranslationModel):
+ """M2M100 multilingual seq2seq model for configurable language-pair translation.
+
+ Fine-tunes the ``facebook/m2m100_418M`` checkpoint from Meta AI. The base
+ model supports direct translation across 100 languages using ISO 639-1
+ language codes (e.g. ``"en"``, ``"es"``, ``"fr"``). Unlike pivot-based
+ systems, M2M100 translates directly between any supported pair.
+
+ Target language generation is guided by ``forced_bos_token_id`` obtained
+ from ``tokenizer.get_lang_id(target_language)``, identical in principle
+ to the NLLB approach but using simpler ISO codes.
+
+ References
+ ----------
+ - [1] https://huggingface.co/facebook/m2m100_418M
+ - [2] Fan et al. (2021). "Beyond English-Centric Multilingual Machine
+ Translation." JMLR 2021.
+ """
+
+ SCHEMA = M2M100TransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="M2M-100 Multilingual Transformer",
+ es="Transformer Multilingüe M2M-100",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Facebook M2M-100 model for direct translation across 100 languages "
+ "using ISO 639-1 codes. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Modelo M2M-100 de Facebook para traducción directa entre 100 idiomas "
+ "usando códigos ISO 639-1. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#6A1B9A"
+ ICON: str = "Language"
+
+ def __init__(self, model=None, **kwargs):
+ kwargs = self.validate_and_transform(kwargs)
+
+ from transformers import AutoTokenizer
+
+ self.model_name = "facebook/m2m100_418M"
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
+
+ self.source_language = kwargs.get("source_language", "en")
+ self.target_language = kwargs.get("target_language", "es")
+
+ if hasattr(self.tokenizer, "src_lang"):
+ self.tokenizer.src_lang = self.source_language
+
+ self.training_args = {
+ "num_train_epochs": kwargs.get("num_train_epochs", 2),
+ "learning_rate": kwargs.get("learning_rate", 2e-5),
+ "weight_decay": kwargs.get("weight_decay", 0.01),
+ }
+ self.batch_size = kwargs.get("batch_size", 4)
+ self.device = kwargs.get("device") or GPU_OR_CPU_PLACEHOLDER
+ self.log_train_every_n_epochs = kwargs.get("log_train_every_n_epochs", 1)
+ self.log_train_every_n_steps = kwargs.get("log_train_every_n_steps", None)
+ self.log_validation_every_n_epochs = kwargs.get(
+ "log_validation_every_n_epochs", 1
+ )
+ self.log_validation_every_n_steps = kwargs.get(
+ "log_validation_every_n_steps", None
+ )
+
+ if model is None:
+ from transformers import AutoModelForSeq2SeqLM
+
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
+ else:
+ self.model = model
+
+ self.num_train_epochs = self.training_args.get("num_train_epochs", 2)
+ self.fitted = model is not None
+
+ def tokenize_data(
+ self, x: "DashAIDataset", y: Optional["DashAIDataset"] = None
+ ) -> "DashAIDataset":
+ """Tokenize with src_lang set for M2M100."""
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+ if hasattr(self.tokenizer, "src_lang"):
+ self.tokenizer.src_lang = self.source_language
+
+ is_y = bool(y)
+ if not y:
+ y = DashAIDataset.from_list([{"foo": 0}] * len(x))
+
+ dataset = []
+ input_column_name = x.column_names[0]
+ output_column_name = y.column_names[0] if is_y else None
+
+ for i, input_sample in enumerate(x):
+ tokenized_input = self.tokenizer(
+ input_sample[input_column_name],
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample = {
+ "input_ids": tokenized_input["input_ids"],
+ "attention_mask": tokenized_input["attention_mask"],
+ }
+ if is_y:
+ output_sample = y[i]
+ tokenized_output = self.tokenizer(
+ output_sample[output_column_name],
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample["labels"] = tokenized_output["input_ids"]
+ dataset.append(sample)
+
+ return DashAIDataset.from_list(dataset)
+
+ def train(
+ self,
+ x_train: "DashAIDataset",
+ y_train: "DashAIDataset",
+ x_validation: "DashAIDataset" = None,
+ y_validation: "DashAIDataset" = None,
+ ):
+ """Fine-tune M2M100 on the configured language pair."""
+ from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
+
+ from DashAI.back.models.hugging_face.metrics_callback import MetricsCallback
+
+ dataset = self.tokenize_data(x_train, y_train)
+ dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
+
+ has_validation_data = x_validation is not None and y_validation is not None
+
+ output_root = Path("DashAI/back/user_models/temp_checkpoints_m2m100")
+ output_root.mkdir(parents=True, exist_ok=True)
+ run_output_dir = tempfile.mkdtemp(prefix="m2m100_", dir=str(output_root))
+
+ training_args_obj = Seq2SeqTrainingArguments(
+ output_dir=run_output_dir,
+ save_steps=1,
+ save_total_limit=1,
+ per_device_train_batch_size=self.batch_size,
+ per_device_eval_batch_size=self.batch_size,
+ use_cpu=self.device.lower() != "gpu",
+ **self.training_args,
+ )
+
+ metrics_callback = MetricsCallback(
+ model_instance=self,
+ x_train=x_train,
+ y_train=y_train,
+ x_val=x_validation,
+ y_val=y_validation,
+ total_epochs=self.num_train_epochs,
+ log_training_every_n_epochs=self.log_train_every_n_epochs,
+ log_training_every_n_steps=self.log_train_every_n_steps,
+ log_val_every_n_epochs=(
+ self.log_validation_every_n_epochs if has_validation_data else None
+ ),
+ log_val_every_n_steps=(
+ self.log_validation_every_n_steps if has_validation_data else None
+ ),
+ )
+
+ trainer = Seq2SeqTrainer(
+ model=self.model,
+ args=training_args_obj,
+ train_dataset=dataset,
+ callbacks=[metrics_callback],
+ )
+
+ self.fitted = True
+ try:
+ trainer.train()
+ finally:
+ shutil.rmtree(run_output_dir, ignore_errors=True)
+
+ return self
+
+ def predict(self, x_pred: "DashAIDataset") -> List:
+ """Translate using forced_bos_token_id for the target language."""
+ if not self.fitted:
+ raise NotFittedError(
+ f"This {self.__class__.__name__} instance is not fitted yet. "
+ "Call 'train' before using this estimator."
+ )
+
+ dataset = self.tokenize_data(x_pred)
+ dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
+
+ target_bos = self.tokenizer.get_lang_id(self.target_language)
+ translations = []
+
+ for example in dataset:
+ inputs = {
+ k: v.unsqueeze(0).to(self.model.device) for k, v in example.items()
+ }
+ outputs = self.model.generate(
+ **inputs,
+ forced_bos_token_id=target_bos,
+ )
+ translated_text = self.tokenizer.decode(
+ outputs[0], skip_special_tokens=True
+ )
+ translations.append(translated_text)
+
+ return translations
+
+ def prepare_dataset(
+ self, dataset: "DashAIDataset", is_fit: bool = False
+ ) -> "DashAIDataset":
+ """Return the dataset unchanged."""
+ return dataset
+
+ def save(self, filename: Union[str, "Path"]) -> None:
+ """Persist model weights and hyperparameters to disk."""
+ from transformers import AutoConfig
+
+ save_dir = Path(filename)
+ if save_dir.exists() and save_dir.is_file():
+ save_dir.unlink()
+ save_dir.mkdir(parents=True, exist_ok=True)
+
+ self.model.save_pretrained(save_dir)
+ config = AutoConfig.from_pretrained(save_dir)
+ config.custom_params = {
+ "num_train_epochs": self.training_args.get("num_train_epochs"),
+ "batch_size": self.batch_size,
+ "learning_rate": self.training_args.get("learning_rate"),
+ "device": self.device,
+ "weight_decay": self.training_args.get("weight_decay"),
+ "source_language": self.source_language,
+ "target_language": self.target_language,
+ "fitted": self.fitted,
+ }
+ config.save_pretrained(save_dir)
+
+ @classmethod
+ def load(cls, filename: Union[str, "Path"]):
+ """Restore an M2M100Transformer instance from disk."""
+ from transformers import AutoConfig, AutoModelForSeq2SeqLM
+
+ model = AutoModelForSeq2SeqLM.from_pretrained(filename)
+ config = AutoConfig.from_pretrained(filename)
+ custom_params = getattr(config, "custom_params", {})
+
+ loaded_model = cls(
+ model=model,
+ num_train_epochs=custom_params.get("num_train_epochs"),
+ batch_size=custom_params.get("batch_size"),
+ learning_rate=custom_params.get("learning_rate"),
+ device=custom_params.get("device"),
+ weight_decay=custom_params.get("weight_decay"),
+ source_language=custom_params.get("source_language", "en"),
+ target_language=custom_params.get("target_language", "es"),
+ log_train_every_n_epochs=None,
+ log_train_every_n_steps=None,
+ log_validation_every_n_epochs=None,
+ log_validation_every_n_steps=None,
+ )
+ loaded_model.fitted = custom_params.get("fitted", False)
+ return loaded_model
diff --git a/DashAI/back/models/hugging_face/minilm_transformer.py b/DashAI/back/models/hugging_face/minilm_transformer.py
new file mode 100644
index 000000000..5b1347936
--- /dev/null
+++ b/DashAI/back/models/hugging_face/minilm_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of MiniLM model for efficient English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class MiniLMTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained MiniLM model for lightweight English text classification.
+
+ MiniLM is a compressed BERT-like model distilled from a larger teacher
+ network using deep self-attention distillation. It achieves competitive
+ performance while being significantly smaller and faster than BERT, making
+ it a good choice for resource-constrained deployments.
+
+ References
+ ----------
+ - [1] Wang, W. et al. (2020). "MiniLM: Deep Self-Attention Distillation for
+ Task-Agnostic Compression of Pre-Trained Transformers." NeurIPS 2020.
+ - [2] https://huggingface.co/microsoft/MiniLM-L12-H384-uncased
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="MiniLM Transformer",
+ es="Transformer MiniLM",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Compact, fast MiniLM model for efficient text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Modelo MiniLM compacto y rápido para clasificación de texto eficiente. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#0277BD"
+ ICON: str = "Speed"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "microsoft/MiniLM-L12-H384-uncased"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_minilm"
diff --git a/DashAI/back/models/hugging_face/multilingual_bert_transformer.py b/DashAI/back/models/hugging_face/multilingual_bert_transformer.py
new file mode 100644
index 000000000..4a7788744
--- /dev/null
+++ b/DashAI/back/models/hugging_face/multilingual_bert_transformer.py
@@ -0,0 +1,47 @@
+"""DashAI implementation of Multilingual BERT for multilingual text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class MultilingualBertTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained Multilingual BERT for cross-lingual text classification.
+
+ mBERT (Multilingual BERT) is a single BERT model pre-trained on the Wikipedia
+ text of 104 languages. It uses a shared vocabulary and can be fine-tuned on a
+ task in one language and applied to another (zero-shot cross-lingual transfer).
+
+ References
+ ----------
+ - [1] Devlin, J. et al. (2019). "BERT: Pre-training of Deep Bidirectional
+ Transformers for Language Understanding." NAACL 2019.
+ - [2] https://huggingface.co/bert-base-multilingual-cased
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="Multilingual BERT Transformer",
+ es="Transformer BERT Multilingüe",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "BERT pre-trained on 104 languages for multilingual text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "BERT pre-entrenado en 104 idiomas para clasificación de texto "
+ "multilingüe. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#283593"
+ ICON: str = "Translate"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "bert-base-multilingual-cased"
+ TEMP_CHECKPOINT_DIR: str = (
+ "DashAI/back/user_models/temp_checkpoints_multilingual_bert"
+ )
diff --git a/DashAI/back/models/hugging_face/nllb_transformer.py b/DashAI/back/models/hugging_face/nllb_transformer.py
index b30619b06..df7114759 100644
--- a/DashAI/back/models/hugging_face/nllb_transformer.py
+++ b/DashAI/back/models/hugging_face/nllb_transformer.py
@@ -31,12 +31,15 @@ class NllbTransformerSchema(OpusMtEnESTransformerSchema):
placeholder="spa_Latn",
description=MultilingualString(
en=(
- "Source language code for NLLB tokenizer. "
- "Example: spa_Latn for Spanish."
+ "Source language code for NLLB tokenizer (e.g. spa_Latn for Spanish, "
+ "eng_Latn for English). It uses BCP-47 language tags in the format"
+ "[Examples](https://dl-translate.readthedocs.io/en/latest/available_languages/#nllb-200)"
),
es=(
- "Código de idioma de origen para el tokenizer NLLB. "
- "Ejemplo: spa_Latn para español."
+ "Código de idioma de origen para el tokenizer NLLB (ej. spa_Latn para "
+ "español, eng_Latn para inglés). Utiliza etiquetas de idioma BCP-47 "
+ "en el formato "
+ "[Ejemplos](https://dl-translate.readthedocs.io/en/latest/available_languages/#nllb-200)"
),
),
alias=MultilingualString(en="Source language", es="Idioma de origen"),
@@ -46,12 +49,15 @@ class NllbTransformerSchema(OpusMtEnESTransformerSchema):
placeholder="eng_Latn",
description=MultilingualString(
en=(
- "Target language code for NLLB generation. "
- "Example: eng_Latn for English."
+ "Target language code for NLLB generation (e.g. eng_Latn for English, "
+ "fra_Latn for French). It uses BCP-47 language tags in the format "
+ "[Examples](https://dl-translate.readthedocs.io/en/latest/available_languages/#nllb-200)"
),
es=(
- "Código de idioma destino para la generación NLLB. "
- "Ejemplo: eng_Latn para inglés."
+ "Código de idioma destino para la generación NLLB (ej. eng_Latn para "
+ "inglés, fra_Latn para francés). Utiliza etiquetas de idioma BCP-47 "
+ "en el formato "
+ "[Ejemplos](https://dl-translate.readthedocs.io/en/latest/available_languages/#nllb-200)"
),
),
alias=MultilingualString(en="Target language", es="Idioma destino"),
diff --git a/DashAI/back/models/hugging_face/opus_mt_en_de_transformer.py b/DashAI/back/models/hugging_face/opus_mt_en_de_transformer.py
new file mode 100644
index 000000000..9bf12c8a0
--- /dev/null
+++ b/DashAI/back/models/hugging_face/opus_mt_en_de_transformer.py
@@ -0,0 +1,47 @@
+"""OpusMtEnDeTransformer model for English-to-German translation."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+
+
+class OpusMtEnDeTransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the English-to-German Opus-MT model."""
+
+
+class OpusMtEnDeTransformer(OpusMtTransformerMixin):
+ """Pre-trained transformer for English-to-German translation.
+
+ Fine-tunes the Helsinki-NLP ``opus-mt-en-de`` checkpoint, a MarianMT
+ seq2seq model trained on parallel English-German corpora from the OPUS
+ collection.
+
+ References
+ ----------
+ - [1] https://huggingface.co/Helsinki-NLP/opus-mt-en-de
+ - [2] https://opus.nlpl.eu/
+ """
+
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-en-de"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-en-de"
+ SCHEMA = OpusMtEnDeTransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Opus MT En-De Transformer",
+ es="Transformer Opus MT En-De",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Pre-trained transformer for English-German translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción inglés-alemán. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#455A64"
+ ICON: str = "Translate"
diff --git a/DashAI/back/models/hugging_face/opus_mt_en_es_transformer.py b/DashAI/back/models/hugging_face/opus_mt_en_es_transformer.py
index 1a4c7bbd3..96ecaab78 100644
--- a/DashAI/back/models/hugging_face/opus_mt_en_es_transformer.py
+++ b/DashAI/back/models/hugging_face/opus_mt_en_es_transformer.py
@@ -1,10 +1,4 @@
-"""OpusMtEnESTransformer model for english-spanish translation DashAI implementation."""
-
-import shutil
-from pathlib import Path
-from typing import TYPE_CHECKING, List, Optional, Union
-
-from sklearn.exceptions import NotFittedError
+"""OpusMtEnESTransformer model for English-to-Spanish translation."""
from DashAI.back.core.schema_fields import (
BaseSchema,
@@ -15,20 +9,18 @@
schema_field,
)
from DashAI.back.core.utils import MultilingualString
-from DashAI.back.models.translation_model import TranslationModel
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
from DashAI.back.models.utils import GPU_OR_CPU, GPU_OR_CPU_PLACEHOLDER
-if TYPE_CHECKING:
- from pathlib import Path
-
- from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
-
class OpusMtEnESTransformerSchema(BaseSchema):
- """opus-mt-en-es is a transformer pre-trained model that allows translation of
- texts from English to Spanish. The implementation is based on the Helsinki-NLP
- opus-mt-en-es checkpoint, which uses the MarianMT architecture and was trained
- on parallel corpora from the OPUS collection.
+ """Schema for Opus-MT translation models (MarianMT architecture).
+
+ Shared by all Helsinki-NLP Opus-MT language-pair wrappers. Controls
+ training duration, batch size, learning rate, device, regularization, and
+ metric-logging frequency.
"""
num_train_epochs: schema_field(
@@ -44,8 +36,8 @@ class OpusMtEnESTransformerSchema(BaseSchema):
int_field(ge=1),
placeholder=4,
description=MultilingualString(
- en="The batch size per GPU/TPU core/CPU for training",
- es="El tamaño de lote por núcleo GPU/TPU/CPU para entrenamiento",
+ en="The batch size per GPU/TPU core/CPU for training.",
+ es="El tamaño de lote por núcleo GPU/TPU/CPU para entrenamiento.",
),
alias=MultilingualString(en="Batch size", es="Tamaño de lote"),
) # type: ignore
@@ -53,8 +45,8 @@ class OpusMtEnESTransformerSchema(BaseSchema):
float_field(ge=0.0),
placeholder=2e-5,
description=MultilingualString(
- en="The initial learning rate for AdamW optimizer",
- es="La tasa de aprendizaje inicial para el optimizador AdamW",
+ en="The initial learning rate for AdamW optimizer.",
+ es="La tasa de aprendizaje inicial para el optimizador AdamW.",
),
alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
) # type: ignore
@@ -63,14 +55,13 @@ class OpusMtEnESTransformerSchema(BaseSchema):
placeholder=GPU_OR_CPU_PLACEHOLDER,
description=MultilingualString(
en=(
- "Hardware on which the training is run. If available, GPU is "
- "recommended for efficiency reasons. Otherwise, use CPU. "
- "If GPU is selected then it will use all gpus available. "
+ "Hardware on which training is run. GPU is recommended when "
+ "available. If GPU is selected, all available GPUs are used."
),
es=(
- "Hardware en el que se ejecuta el entrenamiento. Si está disponible, "
- "se recomienda GPU por razones de eficiencia. De lo contrario, use "
- "CPU. Si se selecciona GPU, usará todas las GPUs disponibles."
+ "Hardware en el que se ejecuta el entrenamiento. Se recomienda "
+ "GPU cuando está disponible. Si se selecciona GPU, se usan "
+ "todas las GPUs disponibles."
),
),
alias=MultilingualString(en="Device", es="Dispositivo"),
@@ -80,107 +71,87 @@ class OpusMtEnESTransformerSchema(BaseSchema):
placeholder=0.01,
description=MultilingualString(
en=(
- "Weight decay is a regularization technique used in training "
- "neural networks to prevent overfitting. In the context of the AdamW "
- "optimizer, the 'weight_decay' parameter is the rate at which the "
- "weights of all layers are reduced during training, provided that "
- "this rate is not zero."
+ "L2 regularization coefficient applied via the AdamW optimizer "
+ "to prevent overfitting."
),
es=(
- "Weight decay es una técnica de regularización usada en el "
- "entrenamiento de redes neuronales para prevenir sobreajuste. En el "
- "contexto del optimizador AdamW, el parámetro 'weight_decay' es la "
- "tasa a la cual los pesos de todas las capas se reducen durante el "
- "entrenamiento, siempre que esta tasa no sea cero."
+ "Coeficiente de regularización L2 aplicado mediante el "
+ "optimizador AdamW para prevenir sobreajuste."
),
),
alias=MultilingualString(en="Weight decay", es="Decaimiento de pesos"),
) # type: ignore
-
log_train_every_n_epochs: schema_field(
none_type(int_field(ge=1)),
placeholder=1,
description=MultilingualString(
- en=(
- "Log metrics for train split every n epochs during training. "
- "If None, it won't log per epoch."
- ),
+ en=("Log train metrics every N epochs. None disables per-epoch logging."),
es=(
- "Registrar métricas del split de entrenamiento cada n épocas. "
- "Si es None, no registrará por época."
+ "Registrar métricas de entrenamiento cada N épocas. "
+ "None desactiva el registro por época."
),
),
alias=MultilingualString(
en="Log train every N epochs", es="Registrar entrenamiento cada N épocas"
),
) # type: ignore
-
log_train_every_n_steps: schema_field(
none_type(int_field(ge=1)),
placeholder=None,
description=MultilingualString(
- en=(
- "Log metrics for train split every n steps during training. "
- "If None, it won't log per step."
- ),
+ en=("Log train metrics every N steps. None disables per-step logging."),
es=(
- "Registrar métricas del split de entrenamiento cada n pasos. "
- "Si es None, no registrará por paso."
+ "Registrar métricas de entrenamiento cada N pasos. "
+ "None desactiva el registro por paso."
),
),
alias=MultilingualString(
en="Log train every N steps", es="Registrar entrenamiento cada N pasos"
),
) # type: ignore
-
log_validation_every_n_epochs: schema_field(
none_type(int_field(ge=1)),
placeholder=1,
description=MultilingualString(
en=(
- "Log metrics for validation split every n epochs during training. "
- "If None, it won't log per epoch."
+ "Log validation metrics every N epochs. "
+ "None disables per-epoch logging."
),
es=(
- "Registrar métricas del split de validación cada n épocas. "
- "Si es None, no registrará por época."
+ "Registrar métricas de validación cada N épocas. "
+ "None desactiva el registro por época."
),
),
alias=MultilingualString(
- en="Log validation every N epochs", es="Registrar validación cada N épocas"
+ en="Log validation every N epochs",
+ es="Registrar validación cada N épocas",
),
) # type: ignore
-
log_validation_every_n_steps: schema_field(
none_type(int_field(ge=1)),
placeholder=None,
description=MultilingualString(
en=(
- "Log metrics for validation split every n steps during training. "
- "If None, it won't log per step."
+ "Log validation metrics every N steps. None disables per-step logging."
),
es=(
- "Registrar métricas del split de validación cada n pasos. "
- "Si es None, no registrará por paso."
+ "Registrar métricas de validación cada N pasos. "
+ "None desactiva el registro por paso."
),
),
alias=MultilingualString(
- en="Log validation every N steps", es="Registrar validación cada N pasos"
+ en="Log validation every N steps",
+ es="Registrar validación cada N pasos",
),
) # type: ignore
-class OpusMtEnESTransformer(TranslationModel):
+class OpusMtEnESTransformer(OpusMtTransformerMixin):
"""Pre-trained transformer for English-to-Spanish translation.
- This model fine-tunes the Helsinki-NLP ``opus-mt-en-es`` checkpoint, which is
- based on the MarianMT sequence-to-sequence architecture. The base model was
- trained on parallel English-Spanish corpora from the OPUS collection and supports
- direct translation without intermediate pivot languages.
-
- Fine-tuning is performed with the HuggingFace ``Seq2SeqTrainer`` using the AdamW
- optimizer. Training and validation metrics are logged at configurable epoch and
- step intervals via a custom ``MetricsCallback``.
+ Fine-tunes the Helsinki-NLP ``opus-mt-en-es`` checkpoint, a MarianMT
+ seq2seq model trained on parallel English-Spanish corpora from the OPUS
+ collection. Supports direct translation without pivot languages.
References
----------
@@ -188,334 +159,22 @@ class OpusMtEnESTransformer(TranslationModel):
- [2] https://opus.nlpl.eu/
"""
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-en-es"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-en-es"
SCHEMA = OpusMtEnESTransformerSchema
DISPLAY_NAME: str = MultilingualString(
en="Opus MT En-Es Transformer",
es="Transformer Opus MT En-Es",
)
DESCRIPTION: str = MultilingualString(
- en="Pre-trained transformer for English-Spanish translation.",
- es="Transformer pre-entrenado para traducción inglés-español.",
+ en=(
+ "Pre-trained transformer for English-Spanish translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción inglés-español. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
)
COLOR: str = "#FFA500"
ICON: str = "Translate"
-
- def __init__(self, model=None, **kwargs):
- """Initialize the transformer.
-
- This process includes the instantiation of the pre-trained model and the
- associated tokenizer. When ``model`` is ``None`` the Helsinki-NLP
- ``opus-mt-en-es`` checkpoint is downloaded from HuggingFace; when a
- model object is supplied the tokenizer is reused without re-downloading.
-
- Parameters
- ----------
- model : transformers.PreTrainedModel or None, optional
- An already-loaded HuggingFace translation model to reuse. If
- ``None``, the pre-trained ``Helsinki-NLP/opus-mt-en-es`` checkpoint
- is downloaded and initialised. Default ``None``.
- **kwargs : dict
- Additional hyperparameters forwarded to ``validate_and_transform``
- and used to configure training arguments (e.g. ``batch_size``,
- ``epochs``, ``learning_rate``).
- """
- kwargs = self.validate_and_transform(kwargs)
- from transformers import AutoTokenizer
-
- self.model_name = "Helsinki-NLP/opus-mt-en-es"
- self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
- if model is None:
- self.training_args = kwargs
- self.batch_size = kwargs.pop("batch_size", 16)
- self.device = kwargs.pop("device")
- self.log_train_every_n_epochs = kwargs.pop("log_train_every_n_epochs", 1)
- self.log_train_every_n_steps = kwargs.pop("log_train_every_n_steps", None)
- self.log_validation_every_n_epochs = kwargs.pop(
- "log_validation_every_n_epochs", 1
- )
- self.log_validation_every_n_steps = kwargs.pop(
- "log_validation_every_n_steps", None
- )
- if model is None:
- from transformers import AutoModelForSeq2SeqLM
-
- self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
- else:
- self.model = model
- self.num_train_epochs = kwargs.get("num_train_epochs", 2)
- self.fitted = model is not None
-
- def tokenize_data(
- self, x: "DashAIDataset", y: Optional["DashAIDataset"] = None
- ) -> "DashAIDataset":
- """Tokenize input and optional target datasets for seq2seq training.
-
- Each sample is tokenized with truncation and max-length padding to 512
- tokens. When ``y`` is provided, the target tokens are stored under the
- ``labels`` key so the ``Seq2SeqTrainer`` can compute the loss directly.
-
- Parameters
- ----------
- x : DashAIDataset
- Source-language dataset. Only the first column is used.
- y : DashAIDataset, optional
- Target-language dataset. When provided, tokenized targets are added
- as ``labels``. When ``None``, only ``input_ids`` and
- ``attention_mask`` are returned (inference mode).
-
- Returns
- -------
- DashAIDataset
- Tokenized dataset with keys ``input_ids``, ``attention_mask``, and
- optionally ``labels``.
- """
- from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
-
- is_y = bool(y)
- if not y:
- y = DashAIDataset.from_list([{"foo": 0}] * len(x))
- dataset = []
- input_column_name = x.column_names[0]
- output_column_name = y.column_names[0] if is_y else None
-
- for i, input_sample in enumerate(x):
- tokenized_input = self.tokenizer(
- input_sample[input_column_name],
- truncation=True,
- padding="max_length",
- max_length=512,
- )
-
- sample = {
- "input_ids": tokenized_input["input_ids"],
- "attention_mask": tokenized_input["attention_mask"],
- }
-
- if is_y:
- output_sample = y[i]
- tokenized_output = self.tokenizer(
- output_sample[output_column_name],
- truncation=True,
- padding="max_length",
- max_length=512,
- )
- sample["labels"] = tokenized_output["input_ids"]
-
- dataset.append(sample)
- return DashAIDataset.from_list(dataset)
-
- def train(
- self,
- x_train: "DashAIDataset",
- y_train: "DashAIDataset",
- x_validation: "DashAIDataset" = None,
- y_validation: "DashAIDataset" = None,
- ) -> "OpusMtEnESTransformer":
- """Fine-tune the opus-mt-en-es model on English-Spanish translation data.
-
- Parameters
- ----------
- x_train : DashAIDataset
- Input English text features for training.
- y_train : DashAIDataset
- Target Spanish translation labels for training.
- x_validation : DashAIDataset, optional
- Input English text features for validation. Defaults to None.
- y_validation : DashAIDataset, optional
- Target Spanish translation labels for validation. Defaults to None.
-
- Returns
- -------
- OpusMtEnESTransformer
- The fine-tuned model instance.
- """
- from DashAI.back.models.hugging_face.metrics_callback import MetricsCallback
-
- dataset = self.tokenize_data(x_train, y_train)
- dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
-
- from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
-
- training_args = Seq2SeqTrainingArguments(
- output_dir="DashAI/back/user_models/temp_checkpoints_opus-mt-en-es",
- save_steps=1,
- save_total_limit=1,
- per_device_train_batch_size=self.batch_size,
- per_device_eval_batch_size=self.batch_size,
- use_cpu=self.device.lower() != "gpu",
- **self.training_args,
- )
-
- # Initialize the custom callback with epoch information
- metrics_callback = MetricsCallback(
- model_instance=self,
- x_train=x_train,
- y_train=y_train,
- x_val=x_validation,
- y_val=y_validation,
- total_epochs=self.num_train_epochs,
- log_training_every_n_epochs=self.log_train_every_n_epochs,
- log_training_every_n_steps=self.log_train_every_n_steps,
- log_val_every_n_epochs=self.log_validation_every_n_epochs,
- log_val_every_n_steps=self.log_validation_every_n_steps,
- )
-
- trainer = Seq2SeqTrainer(
- model=self.model,
- args=training_args,
- train_dataset=dataset,
- callbacks=[metrics_callback],
- )
-
- self.fitted = True
- trainer.train()
- shutil.rmtree(
- "DashAI/back/user_models/temp_checkpoints_opus-mt-en-es", ignore_errors=True
- )
- return self
-
- def predict(self, x_pred: "DashAIDataset") -> List:
- """Translate English source texts to Spanish.
-
- Parameters
- ----------
- x_pred : DashAIDataset
- Source-language dataset. Only the first column is used.
-
- Returns
- -------
- list of str
- One translated string per input sample, in the same order as
- ``x_pred``.
-
- Raises
- ------
- sklearn.exceptions.NotFittedError
- If the model has not been fine-tuned yet (``fitted`` is ``False``).
- """
- if not self.fitted:
- raise NotFittedError(
- f"This {self.__class__.__name__} instance is not fitted yet. Call 'fit'"
- " with appropriate arguments before using this "
- "estimator."
- )
-
- dataset = self.tokenize_data(x_pred)
- dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
-
- translations = []
-
- for example in dataset:
- inputs = {
- k: v.unsqueeze(0).to(self.model.device) for k, v in example.items()
- }
- outputs = self.model.generate(**inputs)
- translated_text = self.tokenizer.decode(
- outputs[0], skip_special_tokens=True
- )
- translations.append(translated_text)
-
- return translations
-
- def prepare_dataset(
- self, dataset: "DashAIDataset", is_fit: bool = False
- ) -> "DashAIDataset":
- """Return the dataset unchanged.
-
- No pre-processing transformations are required for this model. The
- method exists for compatibility with the DashAI model interface.
-
- Parameters
- ----------
- dataset : DashAIDataset
- The dataset to be prepared.
- is_fit : bool, optional
- Whether the call is made during fitting. Unused here. Default
- ``False``.
-
- Returns
- -------
- DashAIDataset
- The original dataset, unmodified.
- """
- try:
- # Useless in this case, but we keep it for consistency with other models.
- return dataset
- except Exception as e:
- print(f"Couldn't apply transformations to the dataset for the model: {e}")
-
- def save(self, filename: Union[str, "Path"]) -> None:
- """Store the fine-tuned model and its configuration to disk.
-
- Saves the model weights via ``save_pretrained`` and embeds the
- hyperparameters (epochs, batch size, learning rate, etc.) into the
- Hugging Face config so they can be restored by :meth:`load`.
-
- Parameters
- ----------
- filename : str or Path
- Directory path where the model files will be written.
- """
- save_dir = Path(filename)
- if save_dir.exists() and save_dir.is_file():
- save_dir.unlink()
- save_dir.mkdir(parents=True, exist_ok=True)
-
- self.model.save_pretrained(save_dir)
- from transformers import AutoConfig
-
- config = AutoConfig.from_pretrained(save_dir)
-
- config.custom_params = {
- "num_train_epochs": self.training_args.get("num_train_epochs"),
- "batch_size": self.batch_size,
- "learning_rate": self.training_args.get("learning_rate"),
- "device": self.device,
- "weight_decay": self.training_args.get("weight_decay"),
- "fitted": self.fitted,
- }
-
- config.save_pretrained(save_dir)
-
- @classmethod
- def load(cls, filename: Union[str, "Path"]):
- """Restore an OpusMtEnESTransformer instance from disk.
-
- Reads the Hugging Face config to recover the custom hyperparameters
- saved by :meth:`save`, then reconstructs the seq2seq model and wraps
- it in a new :class:`OpusMtEnESTransformer` instance.
-
- Parameters
- ----------
- filename : str or Path
- Directory path from which the model files will be read.
-
- Returns
- -------
- OpusMtEnESTransformer
- The restored model instance with ``fitted`` set to the persisted
- value.
- """
- from transformers import AutoConfig, AutoModelForSeq2SeqLM
-
- model = AutoModelForSeq2SeqLM.from_pretrained(filename)
-
- config = AutoConfig.from_pretrained(filename)
-
- custom_params = getattr(config, "custom_params", {})
-
- loaded_model = cls(
- model=model,
- num_train_epochs=custom_params.get("num_train_epochs"),
- batch_size=custom_params.get("batch_size"),
- learning_rate=custom_params.get("learning_rate"),
- device=custom_params.get("device"),
- weight_decay=custom_params.get("weight_decay"),
- log_train_every_n_epochs=None,
- log_train_every_n_steps=None,
- log_validation_every_n_epochs=None,
- log_validation_every_n_steps=None,
- )
- loaded_model.fitted = custom_params.get("fitted", False)
-
- return loaded_model
diff --git a/DashAI/back/models/hugging_face/opus_mt_en_fr_transformer.py b/DashAI/back/models/hugging_face/opus_mt_en_fr_transformer.py
new file mode 100644
index 000000000..144d7cca2
--- /dev/null
+++ b/DashAI/back/models/hugging_face/opus_mt_en_fr_transformer.py
@@ -0,0 +1,47 @@
+"""OpusMtEnFrTransformer model for English-to-French translation."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+
+
+class OpusMtEnFrTransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the English-to-French Opus-MT model."""
+
+
+class OpusMtEnFrTransformer(OpusMtTransformerMixin):
+ """Pre-trained transformer for English-to-French translation.
+
+ Fine-tunes the Helsinki-NLP ``opus-mt-en-fr`` checkpoint, a MarianMT
+ seq2seq model trained on parallel English-French corpora from the OPUS
+ collection.
+
+ References
+ ----------
+ - [1] https://huggingface.co/Helsinki-NLP/opus-mt-en-fr
+ - [2] https://opus.nlpl.eu/
+ """
+
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-en-fr"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-en-fr"
+ SCHEMA = OpusMtEnFrTransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Opus MT En-Fr Transformer",
+ es="Transformer Opus MT En-Fr",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Pre-trained transformer for English-French translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción inglés-francés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#1976D2"
+ ICON: str = "Translate"
diff --git a/DashAI/back/models/hugging_face/opus_mt_en_pt_transformer.py b/DashAI/back/models/hugging_face/opus_mt_en_pt_transformer.py
new file mode 100644
index 000000000..9613775d1
--- /dev/null
+++ b/DashAI/back/models/hugging_face/opus_mt_en_pt_transformer.py
@@ -0,0 +1,47 @@
+"""OpusMtEnPtTransformer model for English-to-Portuguese translation."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+
+
+class OpusMtEnPtTransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the English-to-Portuguese Opus-MT model."""
+
+
+class OpusMtEnPtTransformer(OpusMtTransformerMixin):
+ """Pre-trained transformer for English-to-Portuguese translation.
+
+ Fine-tunes the Helsinki-NLP ``opus-mt-en-pt`` checkpoint, a MarianMT
+ seq2seq model trained on parallel English-Portuguese corpora from the OPUS
+ collection.
+
+ References
+ ----------
+ - [1] https://huggingface.co/Helsinki-NLP/opus-mt-en-pt
+ - [2] https://opus.nlpl.eu/
+ """
+
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-en-pt"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-en-pt"
+ SCHEMA = OpusMtEnPtTransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Opus MT En-Pt Transformer",
+ es="Transformer Opus MT En-Pt",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Pre-trained transformer for English-Portuguese translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción inglés-portugués. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#2E7D32"
+ ICON: str = "Translate"
diff --git a/DashAI/back/models/hugging_face/opus_mt_es_en_transformer.py b/DashAI/back/models/hugging_face/opus_mt_es_en_transformer.py
index b93050cd7..8cf69ac80 100644
--- a/DashAI/back/models/hugging_face/opus_mt_es_en_transformer.py
+++ b/DashAI/back/models/hugging_face/opus_mt_es_en_transformer.py
@@ -1,42 +1,27 @@
-"""OpusMtEsENTransformer model for spanish-english translation DashAI implementation."""
-
-import shutil
-import tempfile
-from pathlib import Path
-from typing import TYPE_CHECKING, List, Optional, Union
-
-from sklearn.exceptions import NotFittedError
+"""OpusMtEsENTransformer model for Spanish-to-English translation."""
from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
OpusMtEnESTransformerSchema,
)
-from DashAI.back.models.translation_model import TranslationModel
-from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER
-
-if TYPE_CHECKING:
- from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
class OpusMtEsENTransformerSchema(OpusMtEnESTransformerSchema):
- """opus-mt-es-en is a transformer pre-trained model that allows translation of
- texts from Spanish to English. The implementation is based on the Helsinki-NLP
- opus-mt-es-en checkpoint, which uses the MarianMT architecture and was trained
- on parallel corpora from the OPUS collection.
+ """Schema for the Spanish-to-English Opus-MT model.
+
+ Inherits all fields from ``OpusMtEnESTransformerSchema``.
"""
-class OpusMtEsENTransformer(TranslationModel):
+class OpusMtEsENTransformer(OpusMtTransformerMixin):
"""Pre-trained transformer for Spanish-to-English translation.
- This model fine-tunes the Helsinki-NLP ``opus-mt-es-en`` checkpoint, which is
- based on the MarianMT sequence-to-sequence architecture. The base model was
- trained on parallel Spanish-English corpora from the OPUS collection and supports
- direct translation without intermediate pivot languages.
-
- Fine-tuning is performed with the HuggingFace ``Seq2SeqTrainer`` using the AdamW
- optimizer. Training and validation metrics are logged at configurable epoch and
- step intervals via a custom ``MetricsCallback``.
+ Fine-tunes the Helsinki-NLP ``opus-mt-es-en`` checkpoint, a MarianMT
+ seq2seq model trained on parallel Spanish-English corpora from the OPUS
+ collection. Supports direct translation without pivot languages.
References
----------
@@ -44,344 +29,22 @@ class OpusMtEsENTransformer(TranslationModel):
- [2] https://opus.nlpl.eu/
"""
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-es-en"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-es-en"
SCHEMA = OpusMtEsENTransformerSchema
DISPLAY_NAME: str = MultilingualString(
en="Opus MT Es-En Transformer",
es="Transformer Opus MT Es-En",
)
DESCRIPTION: str = MultilingualString(
- en="Pre-trained transformer for Spanish-English translation.",
- es="Transformer pre-entrenado para traducción español-inglés.",
+ en=(
+ "Pre-trained transformer for Spanish-English translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción español-inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
)
COLOR: str = "#FF8A65"
ICON: str = "Translate"
-
- def __init__(self, model=None, **kwargs):
- """Initialize the transformer.
-
- Downloads the ``Helsinki-NLP/opus-mt-es-en`` tokenizer and, when
- ``model`` is ``None``, the seq2seq model weights from HuggingFace.
- When a pre-loaded model is supplied, the weights are reused directly
- and ``fitted`` is set to ``True``.
-
- Parameters
- ----------
- model : transformers.PreTrainedModel or None, optional
- An already-loaded HuggingFace seq2seq model to reuse. If ``None``,
- the ``Helsinki-NLP/opus-mt-es-en`` checkpoint is downloaded and
- initialised. Default ``None``.
- **kwargs : dict
- Hyperparameters forwarded to ``validate_and_transform`` and used to
- configure training (e.g. ``num_train_epochs``, ``batch_size``,
- ``learning_rate``, ``weight_decay``, ``device``).
- """
- kwargs = self.validate_and_transform(kwargs)
-
- from transformers import AutoTokenizer
-
- self.model_name = "Helsinki-NLP/opus-mt-es-en"
- self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
-
- self.training_args = {
- "num_train_epochs": kwargs.get("num_train_epochs", 2),
- "learning_rate": kwargs.get("learning_rate", 2e-5),
- "weight_decay": kwargs.get("weight_decay", 0.01),
- }
- self.batch_size = kwargs.get("batch_size", 4)
- self.device = kwargs.get("device") or GPU_OR_CPU_PLACEHOLDER
- self.log_train_every_n_epochs = kwargs.get("log_train_every_n_epochs", 1)
- self.log_train_every_n_steps = kwargs.get("log_train_every_n_steps", None)
- self.log_validation_every_n_epochs = kwargs.get(
- "log_validation_every_n_epochs", 1
- )
- self.log_validation_every_n_steps = kwargs.get(
- "log_validation_every_n_steps", None
- )
-
- if model is None:
- from transformers import AutoModelForSeq2SeqLM
-
- self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
- else:
- self.model = model
-
- self.num_train_epochs = self.training_args.get("num_train_epochs", 2)
- self.fitted = model is not None
-
- def tokenize_data(
- self, x: "DashAIDataset", y: Optional["DashAIDataset"] = None
- ) -> "DashAIDataset":
- """Tokenize input and optional target datasets for seq2seq training.
-
- Each sample is tokenized with truncation and max-length padding to 512
- tokens. When ``y`` is provided, the target tokens are stored under the
- ``labels`` key so the ``Seq2SeqTrainer`` can compute the loss directly.
-
- Parameters
- ----------
- x : DashAIDataset
- Source-language dataset. Only the first column is used.
- y : DashAIDataset, optional
- Target-language dataset. When provided, tokenized targets are added
- as ``labels``. When ``None``, only ``input_ids`` and
- ``attention_mask`` are returned (inference mode).
-
- Returns
- -------
- DashAIDataset
- Tokenized dataset with keys ``input_ids``, ``attention_mask``, and
- optionally ``labels``.
- """
- from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
-
- is_y = bool(y)
- if not y:
- y = DashAIDataset.from_list([{"foo": 0}] * len(x))
-
- dataset = []
- input_column_name = x.column_names[0]
- output_column_name = y.column_names[0] if is_y else None
-
- for i, input_sample in enumerate(x):
- tokenized_input = self.tokenizer(
- input_sample[input_column_name],
- truncation=True,
- padding="max_length",
- max_length=512,
- )
-
- sample = {
- "input_ids": tokenized_input["input_ids"],
- "attention_mask": tokenized_input["attention_mask"],
- }
-
- if is_y:
- output_sample = y[i]
- tokenized_output = self.tokenizer(
- output_sample[output_column_name],
- truncation=True,
- padding="max_length",
- max_length=512,
- )
- sample["labels"] = tokenized_output["input_ids"]
-
- dataset.append(sample)
-
- return DashAIDataset.from_list(dataset)
-
- def train(
- self,
- x_train: "DashAIDataset",
- y_train: "DashAIDataset",
- x_validation: "DashAIDataset" = None,
- y_validation: "DashAIDataset" = None,
- ) -> "OpusMtEsENTransformer":
- """Fine-tune the opus-mt-es-en model on Spanish-English translation data.
-
- Parameters
- ----------
- x_train : DashAIDataset
- Input Spanish text features for training.
- y_train : DashAIDataset
- Target English translation labels for training.
- x_validation : DashAIDataset, optional
- Input Spanish text features for validation. Default ``None``.
- y_validation : DashAIDataset, optional
- Target English translation labels for validation. Default ``None``.
-
- Returns
- -------
- OpusMtEsENTransformer
- The fine-tuned model instance.
- """
- from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
-
- from DashAI.back.models.hugging_face.metrics_callback import MetricsCallback
-
- dataset = self.tokenize_data(x_train, y_train)
- dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
-
- has_validation_data = x_validation is not None and y_validation is not None
-
- output_root = Path("DashAI/back/user_models/temp_checkpoints_opus-mt-es-en")
- output_root.mkdir(parents=True, exist_ok=True)
- run_output_dir = tempfile.mkdtemp(prefix="opus_mt_es_en_", dir=str(output_root))
-
- training_args = Seq2SeqTrainingArguments(
- output_dir=run_output_dir,
- save_steps=1,
- save_total_limit=1,
- per_device_train_batch_size=self.batch_size,
- per_device_eval_batch_size=self.batch_size,
- use_cpu=self.device.lower() != "gpu",
- **self.training_args,
- )
-
- metrics_callback = MetricsCallback(
- model_instance=self,
- x_train=x_train,
- y_train=y_train,
- x_val=x_validation,
- y_val=y_validation,
- total_epochs=self.num_train_epochs,
- log_training_every_n_epochs=self.log_train_every_n_epochs,
- log_training_every_n_steps=self.log_train_every_n_steps,
- log_val_every_n_epochs=(
- self.log_validation_every_n_epochs if has_validation_data else None
- ),
- log_val_every_n_steps=(
- self.log_validation_every_n_steps if has_validation_data else None
- ),
- )
-
- trainer = Seq2SeqTrainer(
- model=self.model,
- args=training_args,
- train_dataset=dataset,
- callbacks=[metrics_callback],
- )
-
- self.fitted = True
- try:
- trainer.train()
- finally:
- shutil.rmtree(run_output_dir, ignore_errors=True)
-
- return self
-
- def predict(self, x_pred: "DashAIDataset") -> List:
- """Translate Spanish source texts to English.
-
- Parameters
- ----------
- x_pred : DashAIDataset
- Source-language dataset. Only the first column is used.
-
- Returns
- -------
- list of str
- One translated string per input sample, in the same order as
- ``x_pred``.
-
- Raises
- ------
- sklearn.exceptions.NotFittedError
- If the model has not been fine-tuned yet (``fitted`` is ``False``).
- """
- if not self.fitted:
- raise NotFittedError(
- f"This {self.__class__.__name__} instance is not fitted yet. Call 'fit'"
- " with appropriate arguments before using this estimator."
- )
-
- dataset = self.tokenize_data(x_pred)
- dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
-
- translations = []
-
- for example in dataset:
- inputs = {
- k: v.unsqueeze(0).to(self.model.device) for k, v in example.items()
- }
- outputs = self.model.generate(**inputs)
- translated_text = self.tokenizer.decode(
- outputs[0], skip_special_tokens=True
- )
- translations.append(translated_text)
-
- return translations
-
- def prepare_dataset(
- self, dataset: "DashAIDataset", is_fit: bool = False
- ) -> "DashAIDataset":
- """Return the dataset unchanged.
-
- No pre-processing transformations are required for this model. The
- method exists for compatibility with the DashAI model interface.
-
- Parameters
- ----------
- dataset : DashAIDataset
- The dataset to be prepared.
- is_fit : bool, optional
- Whether the call is made during fitting. Unused here. Default
- ``False``.
-
- Returns
- -------
- DashAIDataset
- The original dataset, unmodified.
- """
- return dataset
-
- def save(self, filename: Union[str, "Path"]) -> None:
- """Store the fine-tuned model and its configuration to disk.
-
- Saves the model weights via ``save_pretrained`` and embeds the
- hyperparameters (epochs, batch size, learning rate, etc.) into the
- HuggingFace config so they can be restored by :meth:`load`.
-
- Parameters
- ----------
- filename : str or Path
- Directory path where the model files will be written. If a file
- exists at that path it is removed and replaced by a directory.
- """
- from transformers import AutoConfig
-
- save_dir = Path(filename)
- if save_dir.exists() and save_dir.is_file():
- save_dir.unlink()
- save_dir.mkdir(parents=True, exist_ok=True)
-
- self.model.save_pretrained(save_dir)
- config = AutoConfig.from_pretrained(save_dir)
- config.custom_params = {
- "num_train_epochs": self.training_args.get("num_train_epochs"),
- "batch_size": self.batch_size,
- "learning_rate": self.training_args.get("learning_rate"),
- "device": self.device,
- "weight_decay": self.training_args.get("weight_decay"),
- "fitted": self.fitted,
- }
- config.save_pretrained(save_dir)
-
- @classmethod
- def load(cls, filename: Union[str, "Path"]):
- """Restore an OpusMtEsENTransformer instance from disk.
-
- Reads the HuggingFace config to recover the custom hyperparameters
- saved by :meth:`save`, then reconstructs the seq2seq model and wraps
- it in a new :class:`OpusMtEsENTransformer` instance.
-
- Parameters
- ----------
- filename : str or Path
- Directory path from which the model files will be read.
-
- Returns
- -------
- OpusMtEsENTransformer
- The restored model instance with ``fitted`` set to the persisted
- value.
- """
- from transformers import AutoConfig, AutoModelForSeq2SeqLM
-
- model = AutoModelForSeq2SeqLM.from_pretrained(filename)
- config = AutoConfig.from_pretrained(filename)
- custom_params = getattr(config, "custom_params", {})
-
- loaded_model = cls(
- model=model,
- num_train_epochs=custom_params.get("num_train_epochs"),
- batch_size=custom_params.get("batch_size"),
- learning_rate=custom_params.get("learning_rate"),
- device=custom_params.get("device"),
- weight_decay=custom_params.get("weight_decay"),
- log_train_every_n_epochs=None,
- log_train_every_n_steps=None,
- log_validation_every_n_epochs=None,
- log_validation_every_n_steps=None,
- )
- loaded_model.fitted = custom_params.get("fitted", False)
- return loaded_model
diff --git a/DashAI/back/models/hugging_face/opus_mt_fr_en_transformer.py b/DashAI/back/models/hugging_face/opus_mt_fr_en_transformer.py
new file mode 100644
index 000000000..419c2702a
--- /dev/null
+++ b/DashAI/back/models/hugging_face/opus_mt_fr_en_transformer.py
@@ -0,0 +1,47 @@
+"""OpusMtFrEnTransformer model for French-to-English translation."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_opus_mt_transformer import (
+ OpusMtTransformerMixin,
+)
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+
+
+class OpusMtFrEnTransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the French-to-English Opus-MT model."""
+
+
+class OpusMtFrEnTransformer(OpusMtTransformerMixin):
+ """Pre-trained transformer for French-to-English translation.
+
+ Fine-tunes the Helsinki-NLP ``opus-mt-fr-en`` checkpoint, a MarianMT
+ seq2seq model trained on parallel French-English corpora from the OPUS
+ collection.
+
+ References
+ ----------
+ - [1] https://huggingface.co/Helsinki-NLP/opus-mt-fr-en
+ - [2] https://opus.nlpl.eu/
+ """
+
+ MODEL_NAME: str = "Helsinki-NLP/opus-mt-fr-en"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_opus-mt-fr-en"
+ SCHEMA = OpusMtFrEnTransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Opus MT Fr-En Transformer",
+ es="Transformer Opus MT Fr-En",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Pre-trained transformer for French-English translation. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Transformer pre-entrenado para traducción francés-inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#0097A7"
+ ICON: str = "Translate"
diff --git a/DashAI/back/models/hugging_face/roberta_transformer.py b/DashAI/back/models/hugging_face/roberta_transformer.py
new file mode 100644
index 000000000..a2aa028d3
--- /dev/null
+++ b/DashAI/back/models/hugging_face/roberta_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of RoBERTa model for English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class RobertaTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained RoBERTa model for English text classification.
+
+ RoBERTa (Robustly Optimised BERT Pre-training Approach) improves upon BERT
+ by training longer with larger mini-batches, removing the next-sentence
+ prediction objective, and using dynamic masking. It achieves consistently
+ higher performance on NLP benchmarks than BERT.
+
+ References
+ ----------
+ - [1] Liu, Y. et al. (2019). "RoBERTa: A Robustly Optimized BERT Pretraining
+ Approach." arXiv:1907.11692.
+ - [2] https://huggingface.co/roberta-base
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="RoBERTa Transformer",
+ es="Transformer RoBERTa",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Robustly optimised BERT for English text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "BERT optimizado robustamente para clasificación de texto en inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#E65100"
+ ICON: str = "SmartToy"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "roberta-base"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_roberta"
diff --git a/DashAI/back/models/hugging_face/t5_small_transformer.py b/DashAI/back/models/hugging_face/t5_small_transformer.py
new file mode 100644
index 000000000..c0da3da66
--- /dev/null
+++ b/DashAI/back/models/hugging_face/t5_small_transformer.py
@@ -0,0 +1,306 @@
+"""T5SmallTransformer model for English-to-{German, French, Romanian} translation."""
+
+import shutil
+import tempfile
+from pathlib import Path
+from typing import TYPE_CHECKING, List, Optional, Union
+
+from sklearn.exceptions import NotFittedError
+
+from DashAI.back.core.schema_fields import enum_field, schema_field
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.opus_mt_en_es_transformer import (
+ OpusMtEnESTransformerSchema,
+)
+from DashAI.back.models.translation_model import TranslationModel
+from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER
+
+if TYPE_CHECKING:
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+
+_T5_SUPPORTED_LANGUAGES = ["German", "French", "Romanian"]
+
+
+class T5SmallTransformerSchema(OpusMtEnESTransformerSchema):
+ """Schema for the T5-small translation model.
+
+ Extends the standard translation schema with a ``target_language`` field
+ restricted to the languages covered by T5's pre-training tasks:
+ German, French, and Romanian (source is always English).
+ """
+
+ target_language: schema_field(
+ enum_field(_T5_SUPPORTED_LANGUAGES),
+ placeholder="German",
+ description=MultilingualString(
+ en=(
+ "Target language for translation. "
+ "Supported: 'German', 'French', 'Romanian'. "
+ "T5-small translates from English only."
+ ),
+ es=(
+ "Idioma destino para la traducción. "
+ "Soportados: 'German', 'French', 'Romanian'. "
+ "T5-small traduce solo desde inglés."
+ ),
+ ),
+ alias=MultilingualString(en="Target language", es="Idioma destino"),
+ ) # type: ignore
+
+
+class T5SmallTransformer(TranslationModel):
+ """T5-small seq2seq model for English-to-{German, French, Romanian} translation.
+
+ Fine-tunes the ``t5-small`` checkpoint from Google. Translation direction is
+ controlled by a task prefix prepended to each source sentence, e.g.
+ ``"translate English to German: "``.
+
+ Supported target languages: German, French, Romanian (T5 pre-training scope).
+ The source language is always English.
+
+ References
+ ----------
+ - [1] https://huggingface.co/t5-small
+ - [2] Raffel et al. (2020). "Exploring the Limits of Transfer Learning with a
+ Unified Text-to-Text Transformer." JMLR 2020.
+ """
+
+ SCHEMA = T5SmallTransformerSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="T5-Small Translation Transformer",
+ es="Transformer de Traducción T5-Small",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Google T5-small model for English-to-{German, French, Romanian} "
+ "translation using task prefixes. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Modelo T5-small de Google para traducción inglés-{alemán, francés, "
+ "rumano} usando prefijos de tarea. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#00695C"
+ ICON: str = "Language"
+
+ def __init__(self, model=None, **kwargs):
+ kwargs = self.validate_and_transform(kwargs)
+
+ from transformers import AutoTokenizer
+
+ self.model_name = "t5-small"
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
+
+ self.target_language = kwargs.get("target_language", "German")
+
+ self.training_args = {
+ "num_train_epochs": kwargs.get("num_train_epochs", 2),
+ "learning_rate": kwargs.get("learning_rate", 2e-5),
+ "weight_decay": kwargs.get("weight_decay", 0.01),
+ }
+ self.batch_size = kwargs.get("batch_size", 4)
+ self.device = kwargs.get("device") or GPU_OR_CPU_PLACEHOLDER
+ self.log_train_every_n_epochs = kwargs.get("log_train_every_n_epochs", 1)
+ self.log_train_every_n_steps = kwargs.get("log_train_every_n_steps", None)
+ self.log_validation_every_n_epochs = kwargs.get(
+ "log_validation_every_n_epochs", 1
+ )
+ self.log_validation_every_n_steps = kwargs.get(
+ "log_validation_every_n_steps", None
+ )
+
+ if model is None:
+ from transformers import AutoModelForSeq2SeqLM
+
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
+ else:
+ self.model = model
+
+ self.num_train_epochs = self.training_args.get("num_train_epochs", 2)
+ self.fitted = model is not None
+
+ def _make_prefix(self) -> str:
+ return f"translate English to {self.target_language}: "
+
+ def tokenize_data(
+ self, x: "DashAIDataset", y: Optional["DashAIDataset"] = None
+ ) -> "DashAIDataset":
+ """Prepend the T5 task prefix and tokenize source/target texts."""
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+ prefix = self._make_prefix()
+ is_y = bool(y)
+ if not y:
+ y = DashAIDataset.from_list([{"foo": 0}] * len(x))
+
+ dataset = []
+ input_column_name = x.column_names[0]
+ output_column_name = y.column_names[0] if is_y else None
+
+ for i, input_sample in enumerate(x):
+ text = prefix + input_sample[input_column_name]
+ tokenized_input = self.tokenizer(
+ text,
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample = {
+ "input_ids": tokenized_input["input_ids"],
+ "attention_mask": tokenized_input["attention_mask"],
+ }
+ if is_y:
+ output_sample = y[i]
+ tokenized_output = self.tokenizer(
+ output_sample[output_column_name],
+ truncation=True,
+ padding="max_length",
+ max_length=512,
+ )
+ sample["labels"] = tokenized_output["input_ids"]
+ dataset.append(sample)
+
+ return DashAIDataset.from_list(dataset)
+
+ def train(
+ self,
+ x_train: "DashAIDataset",
+ y_train: "DashAIDataset",
+ x_validation: "DashAIDataset" = None,
+ y_validation: "DashAIDataset" = None,
+ ):
+ """Fine-tune T5-small on the configured translation direction."""
+ from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments
+
+ from DashAI.back.models.hugging_face.metrics_callback import MetricsCallback
+
+ dataset = self.tokenize_data(x_train, y_train)
+ dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
+
+ has_validation_data = x_validation is not None and y_validation is not None
+
+ output_root = Path("DashAI/back/user_models/temp_checkpoints_t5_small")
+ output_root.mkdir(parents=True, exist_ok=True)
+ run_output_dir = tempfile.mkdtemp(prefix="t5_small_", dir=str(output_root))
+
+ training_args_obj = Seq2SeqTrainingArguments(
+ output_dir=run_output_dir,
+ save_steps=1,
+ save_total_limit=1,
+ per_device_train_batch_size=self.batch_size,
+ per_device_eval_batch_size=self.batch_size,
+ use_cpu=self.device.lower() != "gpu",
+ **self.training_args,
+ )
+
+ metrics_callback = MetricsCallback(
+ model_instance=self,
+ x_train=x_train,
+ y_train=y_train,
+ x_val=x_validation,
+ y_val=y_validation,
+ total_epochs=self.num_train_epochs,
+ log_training_every_n_epochs=self.log_train_every_n_epochs,
+ log_training_every_n_steps=self.log_train_every_n_steps,
+ log_val_every_n_epochs=(
+ self.log_validation_every_n_epochs if has_validation_data else None
+ ),
+ log_val_every_n_steps=(
+ self.log_validation_every_n_steps if has_validation_data else None
+ ),
+ )
+
+ trainer = Seq2SeqTrainer(
+ model=self.model,
+ args=training_args_obj,
+ train_dataset=dataset,
+ callbacks=[metrics_callback],
+ )
+
+ self.fitted = True
+ try:
+ trainer.train()
+ finally:
+ shutil.rmtree(run_output_dir, ignore_errors=True)
+
+ return self
+
+ def predict(self, x_pred: "DashAIDataset") -> List:
+ """Translate from English to the configured target language."""
+ if not self.fitted:
+ raise NotFittedError(
+ f"This {self.__class__.__name__} instance is not fitted yet. "
+ "Call 'train' before using this estimator."
+ )
+
+ dataset = self.tokenize_data(x_pred)
+ dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])
+
+ translations = []
+ for example in dataset:
+ inputs = {
+ k: v.unsqueeze(0).to(self.model.device) for k, v in example.items()
+ }
+ outputs = self.model.generate(**inputs)
+ translated_text = self.tokenizer.decode(
+ outputs[0], skip_special_tokens=True
+ )
+ translations.append(translated_text)
+
+ return translations
+
+ def prepare_dataset(
+ self, dataset: "DashAIDataset", is_fit: bool = False
+ ) -> "DashAIDataset":
+ """Return the dataset unchanged."""
+ return dataset
+
+ def save(self, filename: Union[str, "Path"]) -> None:
+ """Persist model weights and hyperparameters to disk."""
+ from transformers import AutoConfig
+
+ save_dir = Path(filename)
+ if save_dir.exists() and save_dir.is_file():
+ save_dir.unlink()
+ save_dir.mkdir(parents=True, exist_ok=True)
+
+ self.model.save_pretrained(save_dir)
+ config = AutoConfig.from_pretrained(save_dir)
+ config.custom_params = {
+ "num_train_epochs": self.training_args.get("num_train_epochs"),
+ "batch_size": self.batch_size,
+ "learning_rate": self.training_args.get("learning_rate"),
+ "device": self.device,
+ "weight_decay": self.training_args.get("weight_decay"),
+ "target_language": self.target_language,
+ "fitted": self.fitted,
+ }
+ config.save_pretrained(save_dir)
+
+ @classmethod
+ def load(cls, filename: Union[str, "Path"]):
+ """Restore a T5SmallTransformer instance from disk."""
+ from transformers import AutoConfig, AutoModelForSeq2SeqLM
+
+ model = AutoModelForSeq2SeqLM.from_pretrained(filename)
+ config = AutoConfig.from_pretrained(filename)
+ custom_params = getattr(config, "custom_params", {})
+
+ loaded_model = cls(
+ model=model,
+ num_train_epochs=custom_params.get("num_train_epochs"),
+ batch_size=custom_params.get("batch_size"),
+ learning_rate=custom_params.get("learning_rate"),
+ device=custom_params.get("device"),
+ weight_decay=custom_params.get("weight_decay"),
+ target_language=custom_params.get("target_language", "German"),
+ log_train_every_n_epochs=None,
+ log_train_every_n_steps=None,
+ log_validation_every_n_epochs=None,
+ log_validation_every_n_steps=None,
+ )
+ loaded_model.fitted = custom_params.get("fitted", False)
+ return loaded_model
diff --git a/DashAI/back/models/hugging_face/xlm_roberta_transformer.py b/DashAI/back/models/hugging_face/xlm_roberta_transformer.py
new file mode 100644
index 000000000..68257a344
--- /dev/null
+++ b/DashAI/back/models/hugging_face/xlm_roberta_transformer.py
@@ -0,0 +1,47 @@
+"""DashAI implementation of XLM-RoBERTa model for multilingual text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class XlmRobertaTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained XLM-RoBERTa model for multilingual text classification.
+
+ XLM-RoBERTa is a multilingual version of RoBERTa trained on 2.5 TB of
+ filtered CommonCrawl data covering 100 languages. It achieves strong
+ performance on cross-lingual classification without language-specific
+ fine-tuning. Requires the ``sentencepiece`` package for its tokeniser.
+
+ References
+ ----------
+ - [1] Conneau, A. et al. (2020). "Unsupervised Cross-lingual Representation
+ Learning at Scale." ACL 2020.
+ - [2] https://huggingface.co/xlm-roberta-base
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="XLM-RoBERTa Transformer",
+ es="Transformer XLM-RoBERTa",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Multilingual RoBERTa for cross-lingual text classification "
+ "(100 languages). "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "RoBERTa multilingüe para clasificación de texto entre idiomas "
+ "(100 idiomas). "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#6A1B9A"
+ ICON: str = "Language"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "xlm-roberta-base"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_xlm_roberta"
diff --git a/DashAI/back/models/hugging_face/xlnet_transformer.py b/DashAI/back/models/hugging_face/xlnet_transformer.py
new file mode 100644
index 000000000..bb3c4c451
--- /dev/null
+++ b/DashAI/back/models/hugging_face/xlnet_transformer.py
@@ -0,0 +1,45 @@
+"""DashAI implementation of XLNet model for English text classification."""
+
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.hugging_face.base_text_classification_transformer import (
+ HuggingFaceTextClassificationTransformer,
+)
+from DashAI.back.models.hugging_face.distilbert_transformer import (
+ DistilBertTransformerSchema,
+)
+
+
+class XlnetTransformer(HuggingFaceTextClassificationTransformer):
+ """Pre-trained XLNet model for English text classification.
+
+ XLNet is an autoregressive language model that maximises the expected
+ log-likelihood over all permutations of the factorisation order. Unlike BERT,
+ XLNet does not rely on a corrupted input and can model bidirectional context
+ without masking, often outperforming BERT on various NLP tasks.
+
+ References
+ ----------
+ - [1] Yang, Z. et al. (2019). "XLNet: Generalised Autoregressive Pretraining
+ for Language Understanding." NeurIPS 2019.
+ - [2] https://huggingface.co/xlnet-base-cased
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="XLNet Transformer",
+ es="Transformer XLNet",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Autoregressive XLNet model for English text classification. "
+ "Downloads weights from Hugging Face on first use (internet required)."
+ ),
+ es=(
+ "Modelo XLNet autorregresivo para clasificación de texto en inglés. "
+ "Descarga pesos de Hugging Face en el primer uso (requiere internet)."
+ ),
+ )
+ COLOR: str = "#37474F"
+ ICON: str = "AutoAwesome"
+ SCHEMA = DistilBertTransformerSchema
+ MODEL_NAME: str = "xlnet-base-cased"
+ TEMP_CHECKPOINT_DIR: str = "DashAI/back/user_models/temp_checkpoints_xlnet"
diff --git a/DashAI/back/models/scikit_learn/adaboost_classifier.py b/DashAI/back/models/scikit_learn/adaboost_classifier.py
new file mode 100644
index 000000000..dabe5b3ae
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/adaboost_classifier.py
@@ -0,0 +1,130 @@
+from sklearn.ensemble import AdaBoostClassifier as _AdaBoostClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class AdaBoostClassifierSchema(BaseSchema):
+ """Schema that configures the AdaBoost Classifier.
+
+ AdaBoost (Adaptive Boosting) fits a sequence of weak learners on repeatedly
+ re-weighted versions of the training data. Misclassified samples receive
+ increased weight so that subsequent learners focus on harder examples. The
+ underlying implementation is ``sklearn.ensemble.AdaBoostClassifier``.
+ """
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 50,
+ "lower_bound": 10,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en=(
+ "The maximum number of estimators at which boosting is terminated. "
+ "In case of perfect fit, the learning procedure is stopped early."
+ ),
+ es=(
+ "El número máximo de estimadores en el que se termina el boosting. "
+ "En caso de ajuste perfecto, el procedimiento de aprendizaje se "
+ "detiene antes."
+ ),
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ learning_rate: schema_field(
+ optimizer_float_field(ge=0.01),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.01,
+ "upper_bound": 2.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Weight applied to each classifier at each boosting iteration. "
+ "A higher learning rate increases the contribution of each classifier."
+ ),
+ es=(
+ "Peso aplicado a cada clasificador en cada iteración de boosting. "
+ "Una tasa de aprendizaje mayor incrementa la contribución de cada "
+ "clasificador."
+ ),
+ ),
+ alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class AdaBoostClassifier(
+ TabularClassificationModel, SklearnLikeClassifier, _AdaBoostClassifier
+):
+ """AdaBoost classifier that adapts to misclassified samples iteratively.
+
+ AdaBoost fits a sequence of weak classifiers (decision stumps by default) on
+ re-weighted training data, giving more weight to misclassified examples at each
+ round. The final prediction is a weighted majority vote of all weak classifiers.
+ AdaBoost is sensitive to noisy data and outliers.
+
+ Key hyperparameters include ``n_estimators``, ``learning_rate``, and
+ ``random_state``. The implementation wraps scikit-learn's
+ ``AdaBoostClassifier``.
+
+ References
+ ----------
+ - [1] Freund, Y. & Schapire, R.E. (1997). "A Decision-Theoretic Generalization
+ of On-Line Learning and an Application to Boosting." Journal of Computer
+ and System Sciences, 55(1), 119-139.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostClassifier.html
+ """
+
+ SCHEMA = AdaBoostClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="AdaBoost Classifier",
+ es="Clasificador AdaBoost",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Adaptive boosting that focuses on misclassified samples.",
+ es="Boosting adaptivo que se enfoca en muestras mal clasificadas.",
+ )
+ COLOR: str = "#FFA726"
+ ICON: str = "Bolt"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/adaboost_regression.py b/DashAI/back/models/scikit_learn/adaboost_regression.py
new file mode 100644
index 000000000..56fae8664
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/adaboost_regression.py
@@ -0,0 +1,137 @@
+from sklearn.ensemble import AdaBoostRegressor as _AdaBoostRegressor
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class AdaBoostRegressionSchema(BaseSchema):
+ """Schema that configures the AdaBoost Regressor.
+
+ AdaBoost Regressor fits a sequence of weak regressors on re-weighted versions
+ of the training data, concentrating on samples with high residuals. The
+ underlying implementation is ``sklearn.ensemble.AdaBoostRegressor``.
+ """
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 50,
+ "lower_bound": 10,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en=(
+ "The maximum number of estimators at which boosting is terminated. "
+ "In case of perfect fit, the learning procedure stops early."
+ ),
+ es=(
+ "El número máximo de estimadores en el que se termina el boosting. "
+ "En caso de ajuste perfecto, el procedimiento se detiene antes."
+ ),
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ learning_rate: schema_field(
+ optimizer_float_field(ge=0.01),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.01,
+ "upper_bound": 2.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Weight applied to each regressor at each boosting iteration. "
+ "There is a trade-off between learning_rate and n_estimators."
+ ),
+ es=(
+ "Peso aplicado a cada regresor en cada iteración de boosting. "
+ "Existe un trade-off entre learning_rate y n_estimators."
+ ),
+ ),
+ alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
+ ) # type: ignore
+
+ loss: schema_field(
+ enum_field(enum=["linear", "square", "exponential"]),
+ placeholder="linear",
+ description=MultilingualString(
+ en=(
+ "The loss function to use when updating the weights after each "
+ "boosting iteration."
+ ),
+ es=(
+ "La función de pérdida a usar al actualizar los pesos después de "
+ "cada iteración de boosting."
+ ),
+ ),
+ alias=MultilingualString(en="Loss", es="Pérdida"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class AdaBoostRegression(RegressionModel, SklearnLikeRegressor, _AdaBoostRegressor):
+ """AdaBoost regressor that focuses on samples with high prediction errors.
+
+ AdaBoostRegressor fits weak regressors (decision stumps by default) sequentially
+ on re-weighted training data, assigning higher weights to samples with larger
+ errors. The final prediction is a weighted median of all weak regressors.
+
+ Key hyperparameters include ``n_estimators``, ``learning_rate``, and ``loss``.
+ The implementation wraps scikit-learn's ``AdaBoostRegressor``.
+
+ References
+ ----------
+ - [1] Drucker, H. (1997). "Improving Regressors using Boosting Techniques."
+ ICML, 107-115.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostRegressor.html
+ """
+
+ SCHEMA = AdaBoostRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="AdaBoost Regression",
+ es="Regresión AdaBoost",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Adaptive boosting that focuses on samples with large residuals.",
+ es="Boosting adaptivo que se enfoca en muestras con grandes residuos.",
+ )
+ COLOR: str = "#FFA726"
+ ICON: str = "Bolt"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/bagging_classifier.py b/DashAI/back/models/scikit_learn/bagging_classifier.py
new file mode 100644
index 000000000..a1de4708f
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/bagging_classifier.py
@@ -0,0 +1,164 @@
+from sklearn.ensemble import BaggingClassifier as _BaggingClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class BaggingClassifierSchema(BaseSchema):
+ """Schema that configures the Bagging Classifier.
+
+ Bagging (Bootstrap Aggregating) fits base classifiers on random subsets of the
+ training data (drawn with replacement) and aggregates their predictions by
+ majority vote. It reduces variance and helps avoid overfitting. The underlying
+ implementation is ``sklearn.ensemble.BaggingClassifier``.
+ """
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 10,
+ "lower_bound": 5,
+ "upper_bound": 100,
+ },
+ description=MultilingualString(
+ en="The number of base estimators in the ensemble.",
+ es="El número de estimadores base en el conjunto.",
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ max_samples: schema_field(
+ optimizer_float_field(gt=0.0, le=1.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.1,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Fraction of training samples drawn for each base estimator "
+ "(0 < max_samples ≤ 1.0)."
+ ),
+ es=(
+ "Fracción de muestras de entrenamiento para cada estimador base "
+ "(0 < max_samples ≤ 1.0)."
+ ),
+ ),
+ alias=MultilingualString(en="Max samples", es="Máximas muestras"),
+ ) # type: ignore
+
+ max_features: schema_field(
+ optimizer_float_field(gt=0.0, le=1.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.1,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Fraction of features drawn for each base estimator "
+ "(0 < max_features ≤ 1.0)."
+ ),
+ es=(
+ "Fracción de características para cada estimador base "
+ "(0 < max_features ≤ 1.0)."
+ ),
+ ),
+ alias=MultilingualString(en="Max features", es="Máximas características"),
+ ) # type: ignore
+
+ bootstrap: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en="Whether to draw samples with replacement.",
+ es="Si se extraen muestras con reemplazo.",
+ ),
+ alias=MultilingualString(en="Bootstrap", es="Bootstrap"),
+ ) # type: ignore
+
+ bootstrap_features: schema_field(
+ bool_field(),
+ placeholder=False,
+ description=MultilingualString(
+ en="Whether to draw features with replacement.",
+ es="Si se extraen características con reemplazo.",
+ ),
+ alias=MultilingualString(
+ en="Bootstrap features", es="Bootstrap características"
+ ),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class BaggingClassifier(
+ TabularClassificationModel, SklearnLikeClassifier, _BaggingClassifier
+):
+ """Bagging classifier that aggregates predictions from bootstrap subsets.
+
+ Bagging builds multiple base classifiers, each trained on a bootstrap sample of
+ the training data. The final class prediction is determined by majority voting.
+ Bagging is particularly effective with high-variance, low-bias estimators such
+ as decision trees.
+
+ Key hyperparameters include ``n_estimators``, ``max_samples``,
+ ``max_features``, and ``bootstrap``. The implementation wraps scikit-learn's
+ ``BaggingClassifier``.
+
+ References
+ ----------
+ - [1] Breiman, L. (1996). "Bagging Predictors." Machine Learning, 24(2), 123-140.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.BaggingClassifier.html
+ """
+
+ SCHEMA = BaggingClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Bagging Classifier",
+ es="Clasificador Bagging",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Bootstrap aggregating ensemble to reduce variance.",
+ es="Conjunto de bootstrap aggregating para reducir la varianza.",
+ )
+ COLOR: str = "#26C6DA"
+ ICON: str = "Inventory"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py b/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py
new file mode 100644
index 000000000..b0f7e124b
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py
@@ -0,0 +1,173 @@
+from sklearn.linear_model import BayesianRidge as _BayesianRidge
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class BayesianRidgeRegressionSchema(BaseSchema):
+ """Schema that configures the Bayesian Ridge Regression model.
+
+ Bayesian Ridge estimates the parameters of a regression model using Bayesian
+ inference. It includes regularisation parameters that are estimated from the
+ data rather than set by the user. The underlying implementation is
+ ``sklearn.linear_model.BayesianRidge``.
+ """
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 300,
+ "lower_bound": 50,
+ "upper_bound": 1000,
+ },
+ description=MultilingualString(
+ en="Maximum number of iterations over the complete dataset.",
+ es="Número máximo de iteraciones sobre el conjunto de datos completo.",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ tol: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-3,
+ "lower_bound": 1e-6,
+ "upper_bound": 1e-1,
+ },
+ description=MultilingualString(
+ en="Stop the algorithm if the weight update is smaller than tol.",
+ es=("Detener el algoritmo si la actualización de pesos es menor que tol."),
+ ),
+ alias=MultilingualString(en="Tolerance", es="Tolerancia"),
+ ) # type: ignore
+
+ alpha_1: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-6,
+ "lower_bound": 1e-10,
+ "upper_bound": 1e-2,
+ },
+ description=MultilingualString(
+ en="Shape parameter for the Gamma distribution prior over alpha.",
+ es=("Parámetro de forma para la distribución Gamma previa sobre alfa."),
+ ),
+ alias=MultilingualString(en="Alpha 1", es="Alfa 1"),
+ ) # type: ignore
+
+ alpha_2: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-6,
+ "lower_bound": 1e-10,
+ "upper_bound": 1e-2,
+ },
+ description=MultilingualString(
+ en="Rate parameter for the Gamma distribution prior over alpha.",
+ es=("Parámetro de tasa para la distribución Gamma previa sobre alfa."),
+ ),
+ alias=MultilingualString(en="Alpha 2", es="Alfa 2"),
+ ) # type: ignore
+
+ lambda_1: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-6,
+ "lower_bound": 1e-10,
+ "upper_bound": 1e-2,
+ },
+ description=MultilingualString(
+ en="Shape parameter for the Gamma distribution prior over lambda.",
+ es=("Parámetro de forma para la distribución Gamma previa sobre lambda."),
+ ),
+ alias=MultilingualString(en="Lambda 1", es="Lambda 1"),
+ ) # type: ignore
+
+ lambda_2: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-6,
+ "lower_bound": 1e-10,
+ "upper_bound": 1e-2,
+ },
+ description=MultilingualString(
+ en="Rate parameter for the Gamma distribution prior over lambda.",
+ es=("Parámetro de tasa para la distribución Gamma previa sobre lambda."),
+ ),
+ alias=MultilingualString(en="Lambda 2", es="Lambda 2"),
+ ) # type: ignore
+
+ fit_intercept: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en=(
+ "Whether to calculate the intercept for this model. If False, "
+ "the data is expected to be already centred."
+ ),
+ es=(
+ "Si se calcula el intercepto para este modelo. Si es False, "
+ "se espera que los datos ya estén centrados."
+ ),
+ ),
+ alias=MultilingualString(en="Fit intercept", es="Ajustar intercepto"),
+ ) # type: ignore
+
+
+class BayesianRidgeRegression(RegressionModel, SklearnLikeRegressor, _BayesianRidge):
+ """Bayesian Ridge regression with automatic regularisation estimation.
+
+ BayesianRidge places Gamma priors over the regularisation parameters and
+ estimates them from the data using the Expectation-Maximisation algorithm.
+ This avoids the need for cross-validation to select ``alpha`` and provides
+ predictive uncertainty estimates. It tends to be robust to over-fitting.
+
+ Key hyperparameters include ``max_iter``, ``tol``, and the Gamma prior
+ parameters ``alpha_1``, ``alpha_2``, ``lambda_1``, ``lambda_2``. The
+ implementation wraps scikit-learn's ``BayesianRidge``.
+
+ References
+ ----------
+ - [1] MacKay, D.J.C. (1992). "Bayesian Interpolation." Neural Computation, 4(3).
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.BayesianRidge.html
+ """
+
+ SCHEMA = BayesianRidgeRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Bayesian Ridge Regression",
+ es="Regresión Ridge Bayesiana",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Bayesian regression with automatic regularisation estimation.",
+ es="Regresión bayesiana con estimación automática de regularización.",
+ )
+ COLOR: str = "#7E57C2"
+ ICON: str = "Psychology"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/decision_tree_regression.py b/DashAI/back/models/scikit_learn/decision_tree_regression.py
new file mode 100644
index 000000000..c8a441bac
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/decision_tree_regression.py
@@ -0,0 +1,174 @@
+from sklearn.tree import DecisionTreeRegressor as _DecisionTreeRegressor
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class DecisionTreeRegressionSchema(BaseSchema):
+ """Schema that configures the Decision Tree Regressor.
+
+ The Decision Tree Regressor builds a tree by recursively splitting the feature
+ space to minimise MSE (or MAE) at each node. The underlying implementation is
+ ``sklearn.tree.DecisionTreeRegressor``.
+ """
+
+ max_depth: schema_field(
+ union_type(optimizer_int_field(ge=1), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The maximum depth of the tree. If None, nodes are expanded until "
+ "all leaves are pure or fewer than min_samples_split samples remain."
+ ),
+ es=(
+ "La profundidad máxima del árbol. Si es None, los nodos se expanden "
+ "hasta que todas las hojas sean puras o queden menos de "
+ "min_samples_split muestras."
+ ),
+ ),
+ alias=MultilingualString(en="Max depth", es="Profundidad máxima"),
+ ) # type: ignore
+
+ min_samples_split: schema_field(
+ optimizer_int_field(ge=2),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 2,
+ "lower_bound": 2,
+ "upper_bound": 20,
+ },
+ description=MultilingualString(
+ en="Minimum number of samples required to split an internal node.",
+ es="Número mínimo de muestras requeridas para dividir un nodo interno.",
+ ),
+ alias=MultilingualString(
+ en="Min samples split", es="Mínimas muestras de división"
+ ),
+ ) # type: ignore
+
+ min_samples_leaf: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1,
+ "lower_bound": 1,
+ "upper_bound": 20,
+ },
+ description=MultilingualString(
+ en="Minimum number of samples required to be at a leaf node.",
+ es="Número mínimo de muestras requeridas para estar en una hoja.",
+ ),
+ alias=MultilingualString(
+ en="Min samples leaf", es="Mínimas muestras para hoja"
+ ),
+ ) # type: ignore
+
+ max_leaf_nodes: schema_field(
+ union_type(optimizer_int_field(ge=2), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "Grow a tree with at most max_leaf_nodes in best-first fashion. "
+ "If None, unlimited leaf nodes."
+ ),
+ es=(
+ "Crecer un árbol con a lo sumo max_leaf_nodes de manera best-first. "
+ "Si es None, nodos hoja ilimitados."
+ ),
+ ),
+ alias=MultilingualString(en="Max leaf nodes", es="Máximos nodos hoja"),
+ ) # type: ignore
+
+ min_impurity_decrease: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.0,
+ "lower_bound": 0.0,
+ "upper_bound": 0.5,
+ },
+ description=MultilingualString(
+ en=(
+ "A node is split if the split induces a decrease of the impurity "
+ "greater than or equal to this value."
+ ),
+ es=(
+ "Un nodo se divide si la división induce una disminución de la "
+ "impureza mayor o igual a este valor."
+ ),
+ ),
+ alias=MultilingualString(
+ en="Min impurity decrease", es="Disminución mínima de impureza"
+ ),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class DecisionTreeRegression(
+ RegressionModel, SklearnLikeRegressor, _DecisionTreeRegressor
+):
+ """Decision tree regressor that recursively partitions the feature space.
+
+ DecisionTreeRegressor builds a binary tree by choosing the split that most
+ reduces the MSE (default) at each internal node. Leaf nodes predict the mean
+ of the training targets in the region. Decision trees are fast, interpretable,
+ and require no feature scaling, but tend to overfit without pruning.
+
+ Key hyperparameters include ``max_depth``, ``min_samples_split``,
+ ``min_samples_leaf``, and ``max_leaf_nodes``. The implementation wraps
+ scikit-learn's ``DecisionTreeRegressor``.
+
+ References
+ ----------
+ - [1] Breiman, L. et al. (1984). Classification and Regression Trees. Wadsworth.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html
+ """
+
+ SCHEMA = DecisionTreeRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Decision Tree Regression",
+ es="Regresión Árbol de Decisión",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Interpretable tree-based regressor that partitions the feature space.",
+ es=(
+ "Regresor basado en árbol interpretable que particiona el espacio "
+ "de características."
+ ),
+ )
+ COLOR: str = "#66BB6A"
+ ICON: str = "AccountTree"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/elastic_net_regression.py b/DashAI/back/models/scikit_learn/elastic_net_regression.py
new file mode 100644
index 000000000..cf475d1ea
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/elastic_net_regression.py
@@ -0,0 +1,173 @@
+from sklearn.linear_model import ElasticNet as _ElasticNet
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class ElasticNetRegressionSchema(BaseSchema):
+ """Schema that configures the Elastic Net Regression model.
+
+ Elastic Net combines L1 and L2 penalties, inheriting Lasso's sparse solutions
+ and Ridge's grouping effect. The ``l1_ratio`` controls the balance between
+ both penalties. The underlying implementation is
+ ``sklearn.linear_model.ElasticNet``.
+ """
+
+ alpha: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.0001,
+ "upper_bound": 10.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Regularisation strength multiplier. alpha=0 is OLS; "
+ "increasing alpha increases regularisation."
+ ),
+ es=(
+ "Multiplicador de fuerza de regularización. alpha=0 es MCO; "
+ "aumentar alpha incrementa la regularización."
+ ),
+ ),
+ alias=MultilingualString(en="Alpha", es="Alfa"),
+ ) # type: ignore
+
+ l1_ratio: schema_field(
+ optimizer_float_field(ge=0.0, le=1.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.5,
+ "lower_bound": 0.0,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "The mixing parameter. l1_ratio=0 is pure Ridge; "
+ "l1_ratio=1 is pure Lasso."
+ ),
+ es=(
+ "El parámetro de mezcla. l1_ratio=0 es Ridge puro; "
+ "l1_ratio=1 es Lasso puro."
+ ),
+ ),
+ alias=MultilingualString(en="L1 ratio", es="Ratio L1"),
+ ) # type: ignore
+
+ fit_intercept: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en=(
+ "Whether to calculate the intercept for this model. If False, "
+ "the data is expected to be already centred."
+ ),
+ es=(
+ "Si se calcula el intercepto para este modelo. Si es False, "
+ "se espera que los datos ya estén centrados."
+ ),
+ ),
+ alias=MultilingualString(en="Fit intercept", es="Ajustar intercepto"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=100),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1000,
+ "lower_bound": 100,
+ "upper_bound": 10000,
+ },
+ description=MultilingualString(
+ en="The maximum number of iterations.",
+ es="El número máximo de iteraciones.",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ tol: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-4,
+ "lower_bound": 1e-6,
+ "upper_bound": 1e-1,
+ },
+ description=MultilingualString(
+ en="The tolerance for the optimisation.",
+ es="La tolerancia para la optimización.",
+ ),
+ alias=MultilingualString(en="Tolerance", es="Tolerancia"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class ElasticNetRegression(RegressionModel, SklearnLikeRegressor, _ElasticNet):
+ """Elastic Net regression combining L1 and L2 penalties.
+
+ Elastic Net minimises ``||y - Xw||^2 / (2*n) + alpha * l1_ratio * ||w||_1
+ + alpha * (1 - l1_ratio) * 0.5 * ||w||^2``. The blend of L1 and L2 penalties
+ overcomes Lasso's limitation with correlated features while still producing
+ sparse solutions. Useful when there are many correlated features.
+
+ Key hyperparameters include ``alpha``, ``l1_ratio``, ``fit_intercept``, and
+ ``max_iter``. The implementation wraps scikit-learn's ``ElasticNet``.
+
+ References
+ ----------
+ - [1] Zou, H. & Hastie, T. (2005). "Regularization and Variable Selection
+ via the Elastic Net." JRSS-B, 67(2), 301-320.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.ElasticNet.html
+ """
+
+ SCHEMA = ElasticNetRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Elastic Net Regression",
+ es="Regresión Elastic Net",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Linear regression combining L1 and L2 regularisation.",
+ es="Regresión lineal que combina regularización L1 y L2.",
+ )
+ COLOR: str = "#26A69A"
+ ICON: str = "Hub"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/extra_trees_classifier.py b/DashAI/back/models/scikit_learn/extra_trees_classifier.py
new file mode 100644
index 000000000..9f3879280
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/extra_trees_classifier.py
@@ -0,0 +1,174 @@
+from sklearn.ensemble import ExtraTreesClassifier as _ExtraTreesClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ none_type,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class ExtraTreesClassifierSchema(BaseSchema):
+ """Schema that configures the Extra-Trees Classifier.
+
+ Extra-Trees (Extremely Randomised Trees) builds an ensemble of decision trees
+ with fully random feature thresholds, which further reduces variance at the
+ cost of a slightly higher bias compared to Random Forests. The underlying
+ implementation is ``sklearn.ensemble.ExtraTreesClassifier``.
+ """
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 100,
+ "lower_bound": 50,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en="The number of trees in the forest.",
+ es="El número de árboles en el bosque.",
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ max_depth: schema_field(
+ union_type(optimizer_int_field(ge=1), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The maximum depth of the tree. If None, nodes are expanded until "
+ "all leaves are pure or contain fewer than min_samples_split samples."
+ ),
+ es=(
+ "La profundidad máxima del árbol. Si es None, los nodos se expanden "
+ "hasta que todas las hojas sean puras o tengan menos de "
+ "min_samples_split muestras."
+ ),
+ ),
+ alias=MultilingualString(en="Max depth", es="Profundidad máxima"),
+ ) # type: ignore
+
+ min_samples_split: schema_field(
+ optimizer_int_field(ge=2),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 2,
+ "lower_bound": 2,
+ "upper_bound": 10,
+ },
+ description=MultilingualString(
+ en="The minimum number of samples required to split an internal node.",
+ es="El número mínimo de muestras requeridas para dividir un nodo interno.",
+ ),
+ alias=MultilingualString(
+ en="Min samples split", es="Mínimas muestras de división"
+ ),
+ ) # type: ignore
+
+ min_samples_leaf: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1,
+ "lower_bound": 1,
+ "upper_bound": 10,
+ },
+ description=MultilingualString(
+ en="The minimum number of samples required to be at a leaf node.",
+ es="El número mínimo de muestras requeridas para estar en una hoja.",
+ ),
+ alias=MultilingualString(
+ en="Min samples leaf", es="Mínimas muestras para hoja"
+ ),
+ ) # type: ignore
+
+ bootstrap: schema_field(
+ bool_field(),
+ placeholder=False,
+ description=MultilingualString(
+ en=(
+ "Whether bootstrap samples are used when building trees. "
+ "If False, the whole dataset is used for each tree."
+ ),
+ es=(
+ "Si se usan muestras bootstrap al construir los árboles. "
+ "Si es False, se usa todo el conjunto de datos para cada árbol."
+ ),
+ ),
+ alias=MultilingualString(en="Bootstrap", es="Bootstrap"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class ExtraTreesClassifier(
+ TabularClassificationModel, SklearnLikeClassifier, _ExtraTreesClassifier
+):
+ """Extra-Trees classifier using fully randomised decision tree splits.
+
+ Extremely Randomised Trees differ from Random Forests in how splits are chosen:
+ instead of searching for the best threshold per feature, Extra-Trees picks
+ thresholds at random. This introduces additional randomness that, combined with
+ bootstrap aggregation, further reduces variance. Extra-Trees are typically faster
+ to train than Random Forests.
+
+ Key hyperparameters include ``n_estimators``, ``max_depth``,
+ ``min_samples_split``, ``min_samples_leaf``, and ``bootstrap``. The
+ implementation wraps scikit-learn's ``ExtraTreesClassifier``.
+
+ References
+ ----------
+ - [1] Geurts, P., Ernst, D. & Wehenkel, L. (2006). "Extremely randomized trees."
+ Machine Learning, 63(1), 3-42. https://doi.org/10.1007/s10994-006-6226-1
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html
+ """
+
+ SCHEMA = ExtraTreesClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Extra-Trees Classifier",
+ es="Clasificador Extra-Trees",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Ensemble of fully randomised decision trees for fast, "
+ "low-variance classification."
+ ),
+ es=(
+ "Conjunto de árboles de decisión completamente aleatorizados "
+ "para clasificación rápida."
+ ),
+ )
+ COLOR: str = "#66BB6A"
+ ICON: str = "Park"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/extra_trees_regression.py b/DashAI/back/models/scikit_learn/extra_trees_regression.py
new file mode 100644
index 000000000..6dec5bd42
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/extra_trees_regression.py
@@ -0,0 +1,169 @@
+from sklearn.ensemble import ExtraTreesRegressor as _ExtraTreesRegressor
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ none_type,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class ExtraTreesRegressionSchema(BaseSchema):
+ """Schema that configures the Extra-Trees Regressor.
+
+ Extra-Trees (Extremely Randomised Trees) builds an ensemble of decision tree
+ regressors with fully random feature thresholds, further reducing variance
+ compared to Random Forests. The underlying implementation is
+ ``sklearn.ensemble.ExtraTreesRegressor``.
+ """
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 100,
+ "lower_bound": 50,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en="The number of trees in the forest.",
+ es="El número de árboles en el bosque.",
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ max_depth: schema_field(
+ union_type(optimizer_int_field(ge=1), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The maximum depth of the tree. If None, nodes are expanded until "
+ "all leaves are pure or fewer than min_samples_split samples remain."
+ ),
+ es=(
+ "La profundidad máxima del árbol. Si es None, los nodos se expanden "
+ "hasta que todas las hojas sean puras o queden menos de "
+ "min_samples_split muestras."
+ ),
+ ),
+ alias=MultilingualString(en="Max depth", es="Profundidad máxima"),
+ ) # type: ignore
+
+ min_samples_split: schema_field(
+ optimizer_int_field(ge=2),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 2,
+ "lower_bound": 2,
+ "upper_bound": 10,
+ },
+ description=MultilingualString(
+ en="Minimum number of samples required to split an internal node.",
+ es="Número mínimo de muestras requeridas para dividir un nodo interno.",
+ ),
+ alias=MultilingualString(
+ en="Min samples split", es="Mínimas muestras de división"
+ ),
+ ) # type: ignore
+
+ min_samples_leaf: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1,
+ "lower_bound": 1,
+ "upper_bound": 10,
+ },
+ description=MultilingualString(
+ en="Minimum number of samples required to be at a leaf node.",
+ es="Número mínimo de muestras requeridas para estar en una hoja.",
+ ),
+ alias=MultilingualString(
+ en="Min samples leaf", es="Mínimas muestras para hoja"
+ ),
+ ) # type: ignore
+
+ bootstrap: schema_field(
+ bool_field(),
+ placeholder=False,
+ description=MultilingualString(
+ en=(
+ "Whether bootstrap samples are used when building trees. "
+ "If False, the whole dataset is used for each tree."
+ ),
+ es=(
+ "Si se usan muestras bootstrap al construir los árboles. "
+ "Si es False, se usa todo el conjunto de datos para cada árbol."
+ ),
+ ),
+ alias=MultilingualString(en="Bootstrap", es="Bootstrap"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class ExtraTreesRegression(RegressionModel, SklearnLikeRegressor, _ExtraTreesRegressor):
+ """Extra-Trees regressor using fully randomised decision tree splits.
+
+ Extremely Randomised Trees pick thresholds at random instead of searching for
+ the optimal split, introducing extra randomness that further reduces variance.
+ Combined with averaging over many trees, Extra-Trees can achieve very low
+ generalisation error on regression tasks while being fast to train.
+
+ Key hyperparameters include ``n_estimators``, ``max_depth``,
+ ``min_samples_split``, and ``bootstrap``. The implementation wraps
+ scikit-learn's ``ExtraTreesRegressor``.
+
+ References
+ ----------
+ - [1] Geurts, P., Ernst, D. & Wehenkel, L. (2006). "Extremely randomized trees."
+ Machine Learning, 63(1), 3-42.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesRegressor.html
+ """
+
+ SCHEMA = ExtraTreesRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Extra-Trees Regression",
+ es="Regresión Extra-Trees",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Ensemble of fully randomised decision trees for fast, "
+ "low-variance regression."
+ ),
+ es=(
+ "Conjunto de árboles de decisión completamente aleatorizados "
+ "para regresión rápida y de baja varianza."
+ ),
+ )
+ COLOR: str = "#26A69A"
+ ICON: str = "Park"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/gaussian_nb.py b/DashAI/back/models/scikit_learn/gaussian_nb.py
new file mode 100644
index 000000000..3fc5c0e33
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/gaussian_nb.py
@@ -0,0 +1,89 @@
+from sklearn.naive_bayes import GaussianNB as _GaussianNB
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ optimizer_float_field,
+ schema_field,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class GaussianNBSchema(BaseSchema):
+ """Schema that configures the Gaussian Naïve Bayes Classifier.
+
+ Gaussian Naïve Bayes assumes that the continuous features in each class follow
+ a Gaussian (normal) distribution and applies Bayes' theorem with the strong
+ independence assumption between features. The underlying implementation is
+ ``sklearn.naive_bayes.GaussianNB``.
+ """
+
+ var_smoothing: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-9,
+ "lower_bound": 1e-12,
+ "upper_bound": 1e-3,
+ },
+ description=MultilingualString(
+ en=(
+ "Portion of the largest variance of all features that is added to "
+ "variances for calculation stability."
+ ),
+ es=(
+ "Porción de la mayor varianza de todas las características que se "
+ "añade a las varianzas para estabilidad del cálculo."
+ ),
+ ),
+ alias=MultilingualString(en="Var smoothing", es="Suavizado de varianza"),
+ ) # type: ignore
+
+
+class GaussianNB(TabularClassificationModel, SklearnLikeClassifier, _GaussianNB):
+ """Gaussian Naïve Bayes classifier based on Bayes' theorem.
+
+ GaussianNB models the likelihood of features as Gaussian distributions per
+ class. It estimates the mean and variance of each feature in each class from
+ the training data, then uses Bayes' theorem to compute the posterior class
+ probability. Despite the strong independence assumption, it often performs well
+ and is very fast.
+
+ Key hyperparameter: ``var_smoothing`` (additive variance for numerical
+ stability). The implementation wraps scikit-learn's ``GaussianNB``.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html
+ """
+
+ SCHEMA = GaussianNBSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Gaussian Naïve Bayes",
+ es="Naïve Bayes Gaussiano",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "Probabilistic classifier based on Bayes' theorem "
+ "with Gaussian likelihoods."
+ ),
+ es=(
+ "Clasificador probabilístico basado en el teorema de Bayes "
+ "con verosimilitudes gaussianas."
+ ),
+ )
+ COLOR: str = "#AB47BC"
+ ICON: str = "Functions"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/gradient_boosting_classifier.py b/DashAI/back/models/scikit_learn/gradient_boosting_classifier.py
new file mode 100644
index 000000000..601a1b9ff
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/gradient_boosting_classifier.py
@@ -0,0 +1,200 @@
+from sklearn.ensemble import GradientBoostingClassifier as _GradientBoostingClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class GradientBoostingClassifierSchema(BaseSchema):
+ """Schema that configures the Gradient Boosting Classifier.
+
+ Gradient Boosting is a sequential ensemble classification method that fits a new
+ decision tree at each stage to the negative gradient of a differentiable loss
+ function. The underlying implementation is
+ ``sklearn.ensemble.GradientBoostingClassifier``.
+ """
+
+ loss: schema_field(
+ enum_field(enum=["log_loss", "exponential"]),
+ placeholder="log_loss",
+ description=MultilingualString(
+ en=(
+ "The loss function to be optimized. 'log_loss' refers to binomial and "
+ "multinomial deviance; 'exponential' is equivalent to AdaBoost."
+ ),
+ es=(
+ "La función de pérdida a optimizar. 'log_loss' refiere a la desviación "
+ "binomial y multinomial; 'exponential' es equivalente a AdaBoost."
+ ),
+ ),
+ alias=MultilingualString(en="Loss", es="Pérdida"),
+ ) # type: ignore
+
+ learning_rate: schema_field(
+ optimizer_float_field(ge=0.01),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.1,
+ "lower_bound": 0.01,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en="Learning rate shrinks the contribution of each tree.",
+ es="La tasa de aprendizaje reduce la contribución de cada árbol.",
+ ),
+ alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
+ ) # type: ignore
+
+ n_estimators: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 100,
+ "lower_bound": 10,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en="The number of boosting stages to be run.",
+ es="El número de etapas de boosting a ejecutar.",
+ ),
+ alias=MultilingualString(en="N estimators", es="N estimadores"),
+ ) # type: ignore
+
+ max_depth: schema_field(
+ union_type(optimizer_int_field(ge=1), none_type(int)),
+ placeholder=3,
+ description=MultilingualString(
+ en="Maximum depth of the individual regression estimators.",
+ es="Profundidad máxima de los estimadores de regresión individuales.",
+ ),
+ alias=MultilingualString(en="Max depth", es="Profundidad máxima"),
+ ) # type: ignore
+
+ min_samples_split: schema_field(
+ optimizer_int_field(ge=2),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 2,
+ "lower_bound": 2,
+ "upper_bound": 20,
+ },
+ description=MultilingualString(
+ en="The minimum number of samples required to split an internal node.",
+ es="El número mínimo de muestras requeridas para dividir un nodo interno.",
+ ),
+ alias=MultilingualString(
+ en="Min samples split", es="Mínimas muestras de división"
+ ),
+ ) # type: ignore
+
+ min_samples_leaf: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1,
+ "lower_bound": 1,
+ "upper_bound": 20,
+ },
+ description=MultilingualString(
+ en="The minimum number of samples required to be at a leaf node.",
+ es="El número mínimo de muestras requeridas para estar en una hoja.",
+ ),
+ alias=MultilingualString(
+ en="Min samples leaf", es="Mínimas muestras para hoja"
+ ),
+ ) # type: ignore
+
+ subsample: schema_field(
+ optimizer_float_field(ge=0.1, le=1.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.1,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "The fraction of samples to be used for fitting each base learner. "
+ "Values less than 1.0 lead to stochastic gradient boosting."
+ ),
+ es=(
+ "La fracción de muestras usadas para ajustar cada aprendiz base. "
+ "Valores menores a 1.0 llevan al gradient boosting estocástico."
+ ),
+ ),
+ alias=MultilingualString(en="Subsample", es="Submuestreo"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class GradientBoostingClassifier(
+ TabularClassificationModel, SklearnLikeClassifier, _GradientBoostingClassifier
+):
+ """Gradient boosting classifier that builds an ensemble of trees sequentially.
+
+ Gradient Boosting builds an additive model stage by stage. At each stage a
+ shallow decision tree is fitted to the negative gradient of the chosen loss
+ function. A ``learning_rate`` shrinkage factor scales each tree's contribution,
+ trading slower learning for better generalisation.
+
+ Key hyperparameters include ``n_estimators``, ``learning_rate``, ``max_depth``,
+ ``subsample``, and ``loss``. The implementation wraps scikit-learn's
+ ``GradientBoostingClassifier``.
+
+ References
+ ----------
+ - [1] Friedman, J.H. (2001). "Greedy function approximation: a gradient boosting
+ machine." Annals of Statistics, 29(5), 1189-1232.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.GradientBoostingClassifier.html
+ """
+
+ SCHEMA = GradientBoostingClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Gradient Boosting Classifier",
+ es="Clasificador Gradient Boosting",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Ensemble that builds trees sequentially to correct previous errors.",
+ es=(
+ "Conjunto que construye árboles secuencialmente para corregir "
+ "errores previos."
+ ),
+ )
+ COLOR: str = "#4CAF50"
+ ICON: str = "AutoGraph"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/hist_gradient_boosting_regression.py b/DashAI/back/models/scikit_learn/hist_gradient_boosting_regression.py
new file mode 100644
index 000000000..d9a8c435c
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/hist_gradient_boosting_regression.py
@@ -0,0 +1,173 @@
+from sklearn.ensemble import (
+ HistGradientBoostingRegressor as _HistGradientBoostingRegressor,
+)
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class HistGradientBoostingRegressionSchema(BaseSchema):
+ """Schema that configures the Histogram-based Gradient Boosting Regressor.
+
+ Histogram-based Gradient Boosting is a fast sequential ensemble method that
+ bins features into histograms before building each tree, greatly reducing cost
+ on large datasets. The underlying implementation is
+ ``sklearn.ensemble.HistGradientBoostingRegressor``.
+ """
+
+ learning_rate: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.1,
+ "lower_bound": 0.01,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "The learning rate (shrinkage). Used as a multiplicative factor "
+ "for leaf values. Use 1 for no shrinkage."
+ ),
+ es=(
+ "La tasa de aprendizaje (shrinkage). Se usa como factor multiplicativo "
+ "para los valores de las hojas. Use 1 para no aplicar shrinkage."
+ ),
+ ),
+ alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 100,
+ "lower_bound": 50,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en="Maximum number of iterations (trees) of the boosting process.",
+ es="Número máximo de iteraciones (árboles) del proceso de boosting.",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ max_depth: schema_field(
+ union_type(optimizer_int_field(ge=1), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=("Maximum depth of each tree. If None, depth is not constrained."),
+ es=(
+ "Profundidad máxima de cada árbol. Si es None, la profundidad "
+ "no está restringida."
+ ),
+ ),
+ alias=MultilingualString(en="Max depth", es="Profundidad máxima"),
+ ) # type: ignore
+
+ max_leaf_nodes: schema_field(
+ union_type(optimizer_int_field(ge=2), none_type(int)),
+ placeholder=31,
+ description=MultilingualString(
+ en=(
+ "Maximum number of leaves for each tree. Must be strictly greater "
+ "than 1. If None, no maximum limit."
+ ),
+ es=(
+ "Número máximo de hojas para cada árbol. Debe ser estrictamente "
+ "mayor que 1. Si es None, no hay límite."
+ ),
+ ),
+ alias=MultilingualString(en="Max leaf nodes", es="Máximos nodos hoja"),
+ ) # type: ignore
+
+ min_samples_leaf: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 20,
+ "lower_bound": 1,
+ "upper_bound": 100,
+ },
+ description=MultilingualString(
+ en="Minimum number of samples required to be at a leaf node.",
+ es="Número mínimo de muestras requeridas para estar en una hoja.",
+ ),
+ alias=MultilingualString(
+ en="Min samples leaf", es="Mínimas muestras para hoja"
+ ),
+ ) # type: ignore
+
+ l2_regularization: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.0,
+ "lower_bound": 0.0,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en="The L2 regularisation parameter. Use 0 for no regularisation.",
+ es=(
+ "El parámetro de regularización L2. "
+ "Use 0 para no aplicar regularización."
+ ),
+ ),
+ alias=MultilingualString(en="L2 regularization", es="Regularización L2"),
+ ) # type: ignore
+
+
+class HistGradientBoostingRegression(
+ RegressionModel, SklearnLikeRegressor, _HistGradientBoostingRegressor
+):
+ """Histogram-based gradient boosting regressor for large datasets.
+
+ This regressor discretises features into integer-valued bins before tree
+ construction. The histogram representation reduces candidate split points and
+ memory footprint, enabling efficient training on datasets with tens of thousands
+ of samples or more. It natively supports missing values and is inspired by
+ LightGBM.
+
+ Key hyperparameters include ``learning_rate``, ``max_iter``, ``max_depth``,
+ ``max_leaf_nodes``, ``min_samples_leaf``, and ``l2_regularization``. The
+ implementation wraps scikit-learn's ``HistGradientBoostingRegressor``.
+
+ References
+ ----------
+ - [1] Ke, G. et al. (2017). "LightGBM: A Highly Efficient Gradient Boosting
+ Decision Tree." NeurIPS 30.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.HistGradientBoostingRegressor.html
+ """
+
+ SCHEMA = HistGradientBoostingRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Histogram Gradient Boosting Regression",
+ es="Regresión Gradient Boosting con Histogramas",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Fast gradient boosting regression using histogram-based algorithms.",
+ es=(
+ "Regresión gradient boosting rápida usando algoritmos basados "
+ "en histogramas."
+ ),
+ )
+ COLOR: str = "#9575CD"
+ ICON: str = "RocketLaunch"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/k_neighbors_regression.py b/DashAI/back/models/scikit_learn/k_neighbors_regression.py
new file mode 100644
index 000000000..56d379f73
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/k_neighbors_regression.py
@@ -0,0 +1,143 @@
+from sklearn.neighbors import KNeighborsRegressor as _KNeighborsRegressor
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ optimizer_int_field,
+ schema_field,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class KNeighborsRegressionSchema(BaseSchema):
+ """Schema that configures the K-Nearest Neighbours Regressor.
+
+ KNeighborsRegressor predicts the target by averaging the targets of the
+ ``n_neighbors`` nearest training samples. The underlying implementation is
+ ``sklearn.neighbors.KNeighborsRegressor``.
+ """
+
+ n_neighbors: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 5,
+ "lower_bound": 1,
+ "upper_bound": 50,
+ },
+ description=MultilingualString(
+ en="Number of neighbours to use for the prediction.",
+ es="Número de vecinos a usar para la predicción.",
+ ),
+ alias=MultilingualString(en="N neighbors", es="N vecinos"),
+ ) # type: ignore
+
+ weights: schema_field(
+ enum_field(enum=["uniform", "distance"]),
+ placeholder="uniform",
+ description=MultilingualString(
+ en=(
+ "Weight function used in prediction. 'uniform' weights all "
+ "neighbours equally; 'distance' weights by inverse distance."
+ ),
+ es=(
+ "Función de pesos usada en la predicción. 'uniform' pondera igual "
+ "todos los vecinos; 'distance' pondera por distancia inversa."
+ ),
+ ),
+ alias=MultilingualString(en="Weights", es="Pesos"),
+ ) # type: ignore
+
+ algorithm: schema_field(
+ enum_field(enum=["auto", "ball_tree", "kd_tree", "brute"]),
+ placeholder="auto",
+ description=MultilingualString(
+ en=(
+ "Algorithm used to compute nearest neighbours. 'auto' selects the "
+ "best based on the values passed to fit."
+ ),
+ es=(
+ "Algoritmo para computar los vecinos más cercanos. 'auto' selecciona "
+ "el mejor en función de los valores pasados a fit."
+ ),
+ ),
+ alias=MultilingualString(en="Algorithm", es="Algoritmo"),
+ ) # type: ignore
+
+ leaf_size: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 30,
+ "lower_bound": 5,
+ "upper_bound": 100,
+ },
+ description=MultilingualString(
+ en=(
+ "Leaf size passed to BallTree or KDTree. Affects query speed "
+ "and memory required to store the tree."
+ ),
+ es=(
+ "Tamaño de hoja pasado a BallTree o KDTree. Afecta la velocidad "
+ "de consulta y la memoria requerida para almacenar el árbol."
+ ),
+ ),
+ alias=MultilingualString(en="Leaf size", es="Tamaño de hoja"),
+ ) # type: ignore
+
+ metric: schema_field(
+ enum_field(enum=["minkowski", "euclidean", "manhattan", "chebyshev"]),
+ placeholder="minkowski",
+ description=MultilingualString(
+ en="Distance metric to use for the neighbour search.",
+ es="Métrica de distancia para la búsqueda de vecinos.",
+ ),
+ alias=MultilingualString(en="Metric", es="Métrica"),
+ ) # type: ignore
+
+
+class KNeighborsRegression(RegressionModel, SklearnLikeRegressor, _KNeighborsRegressor):
+ """K-Nearest Neighbours regressor that averages the targets of nearest samples.
+
+ KNeighborsRegressor predicts the target value by computing the (weighted)
+ mean of the ``n_neighbors`` closest training points. It is a non-parametric
+ method: no training phase is needed, and predictions can capture non-linear
+ patterns. Performance degrades in high-dimensional spaces.
+
+ Key hyperparameters include ``n_neighbors``, ``weights``, ``algorithm``, and
+ ``metric``. The implementation wraps scikit-learn's ``KNeighborsRegressor``.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html
+ """
+
+ SCHEMA = KNeighborsRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="K-Nearest Neighbours Regression",
+ es="Regresión K-Vecinos Más Cercanos",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Non-parametric regression that predicts by averaging nearest neighbours.",
+ es=(
+ "Regresión no paramétrica que predice promediando los vecinos más cercanos."
+ ),
+ )
+ COLOR: str = "#FFA726"
+ ICON: str = "ScatterPlot"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/lasso_regression.py b/DashAI/back/models/scikit_learn/lasso_regression.py
new file mode 100644
index 000000000..aea7e1421
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/lasso_regression.py
@@ -0,0 +1,153 @@
+from sklearn.linear_model import Lasso as _Lasso
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class LassoRegressionSchema(BaseSchema):
+ """Schema that configures the Lasso Regression model.
+
+ Lasso (Least Absolute Shrinkage and Selection Operator) adds an L1 penalty on
+ the absolute values of coefficients, driving some of them exactly to zero,
+ which performs implicit feature selection. The underlying implementation is
+ ``sklearn.linear_model.Lasso``.
+ """
+
+ alpha: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.0001,
+ "upper_bound": 10.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Regularisation strength. Larger values specify stronger "
+ "regularisation. alpha=0 is equivalent to OLS."
+ ),
+ es=(
+ "Fuerza de regularización. Valores más grandes especifican "
+ "regularización más fuerte. alpha=0 es equivalente a MCO."
+ ),
+ ),
+ alias=MultilingualString(en="Alpha", es="Alfa"),
+ ) # type: ignore
+
+ fit_intercept: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en=(
+ "Whether to calculate the intercept for this model. If False, "
+ "the data is expected to be already centred."
+ ),
+ es=(
+ "Si se calcula el intercepto para este modelo. Si es False, "
+ "se espera que los datos ya estén centrados."
+ ),
+ ),
+ alias=MultilingualString(en="Fit intercept", es="Ajustar intercepto"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=100),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1000,
+ "lower_bound": 100,
+ "upper_bound": 10000,
+ },
+ description=MultilingualString(
+ en="The maximum number of iterations.",
+ es="El número máximo de iteraciones.",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ tol: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-4,
+ "lower_bound": 1e-6,
+ "upper_bound": 1e-1,
+ },
+ description=MultilingualString(
+ en="The tolerance for the optimisation.",
+ es="La tolerancia para la optimización.",
+ ),
+ alias=MultilingualString(en="Tolerance", es="Tolerancia"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class LassoRegression(RegressionModel, SklearnLikeRegressor, _Lasso):
+ """Lasso regression with L1 regularisation for sparse coefficient solutions.
+
+ Lasso minimises the OLS objective plus an L1 penalty
+ ``||y - Xw||^2 / (2*n) + alpha * ||w||_1``. The L1 term sets many
+ coefficients exactly to zero, performing automatic feature selection. Lasso is
+ particularly useful when there are many features but only a few are expected to
+ be relevant.
+
+ Key hyperparameters include ``alpha``, ``fit_intercept``, ``max_iter``, and
+ ``tol``. The implementation wraps scikit-learn's ``Lasso``.
+
+ References
+ ----------
+ - [1] Tibshirani, R. (1996). "Regression Shrinkage and Selection via the Lasso."
+ Journal of the Royal Statistical Society B, 58(1), 267-288.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Lasso.html
+ """
+
+ SCHEMA = LassoRegressionSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Lasso Regression",
+ es="Regresión Lasso",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Linear regression with L1 regularisation for feature selection.",
+ es="Regresión lineal con regularización L1 para selección de características.",
+ )
+ COLOR: str = "#29B6F6"
+ ICON: str = "SelectAll"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/linear_svc_classifier.py b/DashAI/back/models/scikit_learn/linear_svc_classifier.py
new file mode 100644
index 000000000..1bd00c135
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/linear_svc_classifier.py
@@ -0,0 +1,251 @@
+from sklearn.svm import LinearSVC as _LinearSVC
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ enum_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class LinearSVCClassifierSchema(BaseSchema):
+ """Schema that configures the Linear SVC Classifier.
+
+ LinearSVC implements a linear Support Vector Classification trained with a
+ linear kernel. It is faster than kernel SVC for large datasets. Because
+ LinearSVC does not natively expose class probabilities, it is calibrated with
+ Platt scaling (CalibratedClassifierCV). The underlying implementation is
+ ``sklearn.svm.LinearSVC``.
+ """
+
+ C: schema_field( # noqa: N815
+ optimizer_float_field(ge=1e-4),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.01,
+ "upper_bound": 100.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Regularisation parameter. The strength of the regularisation is "
+ "inversely proportional to C. Must be strictly positive."
+ ),
+ es=(
+ "Parámetro de regularización. La fuerza de la regularización es "
+ "inversamente proporcional a C. Debe ser estrictamente positivo."
+ ),
+ ),
+ alias=MultilingualString(en="C", es="C"),
+ ) # type: ignore
+
+ loss: schema_field(
+ enum_field(enum=["squared_hinge", "hinge"]),
+ placeholder="squared_hinge",
+ description=MultilingualString(
+ en=(
+ "Specifies the loss function. 'squared_hinge' is the default; "
+ "'hinge' is the standard SVM loss."
+ ),
+ es=(
+ "Especifica la función de pérdida. 'squared_hinge' es el "
+ "predeterminado; 'hinge' es la pérdida estándar de SVM."
+ ),
+ ),
+ alias=MultilingualString(en="Loss", es="Pérdida"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=100),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1000,
+ "lower_bound": 100,
+ "upper_bound": 10000,
+ },
+ description=MultilingualString(
+ en="The maximum number of iterations to be run.",
+ es="El número máximo de iteraciones a ejecutar.",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ tol: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-4,
+ "lower_bound": 1e-6,
+ "upper_bound": 1e-1,
+ },
+ description=MultilingualString(
+ en="Tolerance for stopping criteria.",
+ es="Tolerancia para el criterio de parada.",
+ ),
+ alias=MultilingualString(en="Tolerance", es="Tolerancia"),
+ ) # type: ignore
+
+ fit_intercept: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en=(
+ "Whether to calculate the intercept for this model. If False, "
+ "the data is expected to be already centred."
+ ),
+ es=(
+ "Si se calcula el intercepto para este modelo. Si es False, "
+ "se espera que los datos ya estén centrados."
+ ),
+ ),
+ alias=MultilingualString(en="Fit intercept", es="Ajustar intercepto"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class LinearSVCClassifier(
+ TabularClassificationModel, SklearnLikeClassifier, _LinearSVC
+):
+ """Linear SVC classifier with Platt-scaling calibration for class probabilities.
+
+ LinearSVC uses a linear kernel and is trained with coordinate descent, making
+ it considerably faster than kernel SVC on large datasets. Because LinearSVC
+ does not expose ``predict_proba`` natively, this wrapper fits a
+ ``CalibratedClassifierCV`` with sigmoid calibration so that probability
+ estimates are available to the DashAI evaluation pipeline.
+
+ Key hyperparameters include ``C`` (regularisation), ``loss``, ``max_iter``,
+ and ``fit_intercept``. The implementation wraps scikit-learn's ``LinearSVC``.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.svm.LinearSVC.html
+ - [2] https://scikit-learn.org/stable/modules/calibration.html
+ """
+
+ SCHEMA = LinearSVCClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Linear SVC",
+ es="SVC Lineal",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Fast linear support vector classifier with probability calibration.",
+ es=(
+ "Clasificador de vectores de soporte lineal rápido con "
+ "calibración de probabilidades."
+ ),
+ )
+ COLOR: str = "#FF7043"
+ ICON: str = "LinearScale"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
+ self._calibrated = None
+
+ def __sklearn_is_fitted__(self) -> bool:
+ return self._calibrated is not None
+
+ def train(self, x_train, y_train, x_validation=None, y_validation=None):
+ """Train using CalibratedClassifierCV to expose predict_proba.
+
+ Parameters
+ ----------
+ x_train : DashAIDataset
+ The input features for training.
+ y_train : DashAIDataset
+ The target labels for training.
+ x_validation : DashAIDataset, optional
+ Unused (sklearn models ignore validation split).
+ y_validation : DashAIDataset, optional
+ Unused.
+
+ Returns
+ -------
+ self
+ """
+ from sklearn.calibration import CalibratedClassifierCV
+ from sklearn.svm import LinearSVC as _LinearSVCRaw
+
+ x_processed = self.prepare_dataset(x_train, is_fit=True).to_pandas()
+ y_processed = self.prepare_output(y_train, is_fit=True).to_pandas()
+ y_arr = y_processed.values.ravel()
+
+ params = {
+ k: getattr(self, k)
+ for k in ["C", "loss", "max_iter", "tol", "fit_intercept", "random_state"]
+ if hasattr(self, k)
+ }
+ base = _LinearSVCRaw(**params)
+ self._calibrated = CalibratedClassifierCV(base, method="sigmoid", cv=3)
+ self._calibrated.fit(x_processed, y_arr)
+ return self
+
+ def predict(self, x_pred) -> "ndarray": # noqa: F821
+ """Return class-probability matrix using the calibrated model.
+
+ Parameters
+ ----------
+ x_pred : DashAIDataset or pd.DataFrame
+ Input data.
+
+ Returns
+ -------
+ np.ndarray
+ Class probability matrix.
+ """
+ import pandas as pd
+
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+ if isinstance(x_pred, DashAIDataset):
+ try:
+ x_prepared = self.prepare_dataset(x_pred, is_fit=False)
+ except ValueError:
+ x_prepared = x_pred
+ x_pred = x_prepared.to_pandas()
+ elif isinstance(x_pred, pd.DataFrame):
+ pass
+
+ from sklearn.exceptions import NotFittedError
+
+ if self._calibrated is None:
+ raise NotFittedError(
+ f"This {self.__class__.__name__} instance is not fitted yet. "
+ "Call 'train' with appropriate arguments before using this estimator."
+ )
+ return self._calibrated.predict_proba(x_pred)
diff --git a/DashAI/back/models/scikit_learn/mlp_classifier.py b/DashAI/back/models/scikit_learn/mlp_classifier.py
new file mode 100644
index 000000000..bb7b4030b
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/mlp_classifier.py
@@ -0,0 +1,187 @@
+from sklearn.neural_network import MLPClassifier as _MLPClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class MLPClassifierSchema(BaseSchema):
+ """Schema that configures the MLP Classifier.
+
+ The Multi-layer Perceptron Classifier is a feedforward neural network trained
+ with backpropagation. It supports multiple hidden layers and several activation
+ functions. The underlying implementation is
+ ``sklearn.neural_network.MLPClassifier``.
+ """
+
+ hidden_layer_size: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 100,
+ "lower_bound": 10,
+ "upper_bound": 500,
+ },
+ description=MultilingualString(
+ en=(
+ "Number of neurons in the single hidden layer. The model uses one "
+ "hidden layer of this size."
+ ),
+ es=(
+ "Número de neuronas en la capa oculta única. El modelo utiliza una "
+ "capa oculta de este tamaño."
+ ),
+ ),
+ alias=MultilingualString(en="Hidden layer size", es="Tamaño de capa oculta"),
+ ) # type: ignore
+
+ activation: schema_field(
+ enum_field(enum=["relu", "tanh", "logistic", "identity"]),
+ placeholder="relu",
+ description=MultilingualString(
+ en="Activation function for the hidden layer.",
+ es="Función de activación para la capa oculta.",
+ ),
+ alias=MultilingualString(en="Activation", es="Activación"),
+ ) # type: ignore
+
+ solver: schema_field(
+ enum_field(enum=["adam", "lbfgs", "sgd"]),
+ placeholder="adam",
+ description=MultilingualString(
+ en=(
+ "The solver for weight optimisation. 'adam' works well for large "
+ "datasets; 'lbfgs' converges faster on small datasets; 'sgd' "
+ "requires more tuning."
+ ),
+ es=(
+ "El solucionador para la optimización de pesos. 'adam' funciona bien "
+ "para datasets grandes; 'lbfgs' converge más rápido en datasets "
+ "pequeños; 'sgd' requiere más ajuste."
+ ),
+ ),
+ alias=MultilingualString(en="Solver", es="Solucionador"),
+ ) # type: ignore
+
+ alpha: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.0001,
+ "lower_bound": 1e-6,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en="L2 regularisation term (penalty parameter).",
+ es="Término de regularización L2 (parámetro de penalización).",
+ ),
+ alias=MultilingualString(en="Alpha", es="Alfa"),
+ ) # type: ignore
+
+ learning_rate_init: schema_field(
+ optimizer_float_field(ge=1e-6),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.001,
+ "lower_bound": 1e-5,
+ "upper_bound": 0.1,
+ },
+ description=MultilingualString(
+ en="The initial learning rate used for weight updates.",
+ es="La tasa de aprendizaje inicial usada para actualizar los pesos.",
+ ),
+ alias=MultilingualString(
+ en="Learning rate init", es="Tasa de aprendizaje inicial"
+ ),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 200,
+ "lower_bound": 50,
+ "upper_bound": 1000,
+ },
+ description=MultilingualString(
+ en=(
+ "Maximum number of iterations. The solver iterates until "
+ "convergence or this limit."
+ ),
+ es=(
+ "Número máximo de iteraciones. El solucionador itera hasta "
+ "convergencia o este límite."
+ ),
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class MLPClassifier(TabularClassificationModel, SklearnLikeClassifier, _MLPClassifier):
+ """Multi-layer Perceptron classifier trained with backpropagation.
+
+ MLPClassifier is a fully-connected feedforward neural network. The network
+ uses a single hidden layer whose size is controlled by ``hidden_layer_size``.
+ Training uses backpropagation with the selected ``solver``. Supports ReLU,
+ tanh, logistic, and identity activations.
+
+ Key hyperparameters include ``hidden_layer_size``, ``activation``, ``solver``,
+ ``alpha`` (L2 regularisation), ``learning_rate_init``, and ``max_iter``. The
+ implementation wraps scikit-learn's ``MLPClassifier``.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html
+ """
+
+ SCHEMA = MLPClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="MLP Classifier",
+ es="Clasificador MLP",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Multi-layer perceptron neural network for tabular classification.",
+ es="Red neuronal perceptrón multicapa para clasificación tabular.",
+ )
+ COLOR: str = "#EF5350"
+ ICON: str = "AccountTree"
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model, converting hidden_layer_size to a tuple for sklearn.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values; ``hidden_layer_size`` is converted to a tuple
+ ``(hidden_layer_size,)`` before being forwarded to sklearn's MLPClassifier.
+ """
+ hidden_size = kwargs.pop("hidden_layer_size", 100)
+ kwargs["hidden_layer_sizes"] = (hidden_size,)
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/sgd_classifier.py b/DashAI/back/models/scikit_learn/sgd_classifier.py
new file mode 100644
index 000000000..45b6ec2eb
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/sgd_classifier.py
@@ -0,0 +1,270 @@
+from sklearn.linear_model import SGDClassifier as _SGDClassifier
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ none_type,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+ union_type,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.scikit_learn.sklearn_like_classifier import (
+ SklearnLikeClassifier,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+
+class SGDClassifierSchema(BaseSchema):
+ """Schema that configures the SGD Classifier.
+
+ SGDClassifier implements regularised linear classifiers (SVM, logistic
+ regression, etc.) with Stochastic Gradient Descent training. The loss function
+ determines the model type. Because not all loss functions expose
+ ``predict_proba``, this wrapper always uses CalibratedClassifierCV for
+ consistent probability estimates. The underlying implementation is
+ ``sklearn.linear_model.SGDClassifier``.
+ """
+
+ loss: schema_field(
+ enum_field(
+ enum=[
+ "hinge",
+ "log_loss",
+ "modified_huber",
+ "squared_hinge",
+ "perceptron",
+ ]
+ ),
+ placeholder="hinge",
+ description=MultilingualString(
+ en=(
+ "The loss function to use. 'hinge' gives a linear SVM; 'log_loss' "
+ "gives logistic regression; 'modified_huber' is smoother; "
+ "'squared_hinge' is like hinge but quadratically penalised; "
+ "'perceptron' is the linear loss used by the perceptron algorithm."
+ ),
+ es=(
+ "La función de pérdida a usar. 'hinge' da un SVM lineal; 'log_loss' "
+ "da regresión logística; 'modified_huber' es más suave; "
+ "'squared_hinge' es como hinge pero penalizado cuadráticamente; "
+ "'perceptron' es la pérdida lineal usada por el algoritmo perceptrón."
+ ),
+ ),
+ alias=MultilingualString(en="Loss", es="Pérdida"),
+ ) # type: ignore
+
+ alpha: schema_field(
+ optimizer_float_field(ge=1e-6),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.0001,
+ "lower_bound": 1e-6,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Regularisation parameter. Higher values result in stronger "
+ "regularisation."
+ ),
+ es=(
+ "Parámetro de regularización. Valores más altos resultan en "
+ "regularización más fuerte."
+ ),
+ ),
+ alias=MultilingualString(en="Alpha", es="Alfa"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1000,
+ "lower_bound": 100,
+ "upper_bound": 5000,
+ },
+ description=MultilingualString(
+ en="The maximum number of passes over the training data (epochs).",
+ es="El número máximo de pasadas sobre los datos de entrenamiento (épocas).",
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+ tol: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1e-3,
+ "lower_bound": 1e-6,
+ "upper_bound": 1e-1,
+ },
+ description=MultilingualString(
+ en=("The stopping criterion. Training stops when loss > best_loss - tol."),
+ es=(
+ "El criterio de parada. El entrenamiento se detiene cuando "
+ "pérdida > mejor_pérdida - tol."
+ ),
+ ),
+ alias=MultilingualString(en="Tolerance", es="Tolerancia"),
+ ) # type: ignore
+
+ learning_rate: schema_field(
+ enum_field(enum=["constant", "optimal", "invscaling", "adaptive"]),
+ placeholder="optimal",
+ description=MultilingualString(
+ en=(
+ "The learning rate schedule. 'optimal' uses 1/(alpha*(t+t0)); "
+ "'constant' keeps eta0 constant; 'invscaling' decreases as "
+ "1/t^power; 'adaptive' halves the rate when training stops."
+ ),
+ es=(
+ "El programa de tasa de aprendizaje. 'optimal' usa "
+ "1/(alpha*(t+t0)); 'constant' mantiene eta0 constante; "
+ "'invscaling' decrece como 1/t^power; 'adaptive' reduce a la "
+ "mitad la tasa cuando el entrenamiento deja de mejorar."
+ ),
+ ),
+ alias=MultilingualString(en="Learning rate", es="Tasa de aprendizaje"),
+ ) # type: ignore
+
+ random_state: schema_field(
+ union_type(optimizer_int_field(ge=0), none_type(int)),
+ placeholder=None,
+ description=MultilingualString(
+ en=(
+ "The seed of the pseudo-random number generator. Pass an int for "
+ "reproducible output, or None to not set a specific seed."
+ ),
+ es=(
+ "La semilla del generador de números pseudoaleatorios. Pase un int "
+ "para salida reproducible, o None para no fijar una semilla."
+ ),
+ ),
+ alias=MultilingualString(en="Random state", es="Estado aleatorio"),
+ ) # type: ignore
+
+
+class SGDClassifier(TabularClassificationModel, SklearnLikeClassifier, _SGDClassifier):
+ """SGD classifier with probability calibration for consistent predict_proba output.
+
+ SGDClassifier supports multiple loss functions that correspond to different
+ linear models (SVM with 'hinge', logistic regression with 'log_loss', etc.).
+ Stochastic Gradient Descent allows efficient training on large datasets. Because
+ not all loss functions expose ``predict_proba`` natively, this wrapper
+ consistently calibrates the model with ``CalibratedClassifierCV``.
+
+ Key hyperparameters include ``loss``, ``alpha``, ``max_iter``, ``tol``, and
+ ``learning_rate``. The implementation wraps scikit-learn's ``SGDClassifier``.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html
+ """
+
+ SCHEMA = SGDClassifierSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="SGD Classifier",
+ es="Clasificador SGD",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Linear classifier trained with stochastic gradient descent.",
+ es="Clasificador lineal entrenado con descenso de gradiente estocástico.",
+ )
+ COLOR: str = "#78909C"
+ ICON: str = "TrendingDown"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
+ self._calibrated = None
+
+ def __sklearn_is_fitted__(self) -> bool:
+ return self._calibrated is not None
+
+ def train(self, x_train, y_train, x_validation=None, y_validation=None):
+ """Train using CalibratedClassifierCV to guarantee predict_proba availability.
+
+ Parameters
+ ----------
+ x_train : DashAIDataset
+ The input features for training.
+ y_train : DashAIDataset
+ The target labels for training.
+ x_validation : DashAIDataset, optional
+ Unused (sklearn models ignore validation split).
+ y_validation : DashAIDataset, optional
+ Unused.
+
+ Returns
+ -------
+ self
+ """
+ from sklearn.calibration import CalibratedClassifierCV
+ from sklearn.linear_model import SGDClassifier as _SGDClassifierRaw
+
+ x_processed = self.prepare_dataset(x_train, is_fit=True).to_pandas()
+ y_processed = self.prepare_output(y_train, is_fit=True).to_pandas()
+ y_arr = y_processed.values.ravel()
+
+ params = {
+ k: getattr(self, k)
+ for k in [
+ "loss",
+ "alpha",
+ "max_iter",
+ "tol",
+ "learning_rate",
+ "random_state",
+ ]
+ if hasattr(self, k)
+ }
+ base = _SGDClassifierRaw(**params)
+ self._calibrated = CalibratedClassifierCV(base, method="sigmoid", cv=3)
+ self._calibrated.fit(x_processed, y_arr)
+ return self
+
+ def predict(self, x_pred) -> "ndarray": # noqa: F821
+ """Return class-probability matrix using the calibrated model.
+
+ Parameters
+ ----------
+ x_pred : DashAIDataset or pd.DataFrame
+ Input data.
+
+ Returns
+ -------
+ np.ndarray
+ Class probability matrix.
+ """
+ import pandas as pd
+
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+ if isinstance(x_pred, DashAIDataset):
+ try:
+ x_prepared = self.prepare_dataset(x_pred, is_fit=False)
+ except ValueError:
+ x_prepared = x_pred
+ x_pred = x_prepared.to_pandas()
+ elif isinstance(x_pred, pd.DataFrame):
+ pass
+
+ from sklearn.exceptions import NotFittedError
+
+ if self._calibrated is None:
+ raise NotFittedError(
+ f"This {self.__class__.__name__} instance is not fitted yet. "
+ "Call 'train' with appropriate arguments before using this estimator."
+ )
+ return self._calibrated.predict_proba(x_pred)
diff --git a/DashAI/back/models/scikit_learn/svr.py b/DashAI/back/models/scikit_learn/svr.py
new file mode 100644
index 000000000..266a44c32
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/svr.py
@@ -0,0 +1,161 @@
+from sklearn.svm import SVR as _SVR
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ optimizer_float_field,
+ optimizer_int_field,
+ schema_field,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.regression_model import RegressionModel
+from DashAI.back.models.scikit_learn.sklearn_like_model import (
+ CategoricalEncodingStrategy,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor
+
+
+class SVRSchema(BaseSchema):
+ """Schema that configures the Support Vector Regressor.
+
+ SVR (Support Vector Regression) finds a function that deviates from the
+ observed targets by at most ``epsilon`` while being as flat as possible. It
+ uses kernel functions to handle non-linear relationships. The underlying
+ implementation is ``sklearn.svm.SVR``.
+ """
+
+ kernel: schema_field(
+ enum_field(enum=["rbf", "linear", "poly", "sigmoid"]),
+ placeholder="rbf",
+ description=MultilingualString(
+ en=(
+ "Specifies the kernel type to be used in the algorithm. "
+ "'rbf' is the default radial basis function."
+ ),
+ es=(
+ "Especifica el tipo de kernel a usar. "
+ "'rbf' es la función de base radial predeterminada."
+ ),
+ ),
+ alias=MultilingualString(en="Kernel", es="Kernel"),
+ ) # type: ignore
+
+ C: schema_field( # noqa: N815
+ optimizer_float_field(ge=1e-4),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 1.0,
+ "lower_bound": 0.01,
+ "upper_bound": 100.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Regularisation parameter. Inversely proportional to the "
+ "strength of the regularisation."
+ ),
+ es=(
+ "Parámetro de regularización. Inversamente proporcional a la "
+ "fuerza de la regularización."
+ ),
+ ),
+ alias=MultilingualString(en="C", es="C"),
+ ) # type: ignore
+
+ epsilon: schema_field(
+ optimizer_float_field(ge=0.0),
+ placeholder={
+ "optimize": False,
+ "fixed_value": 0.1,
+ "lower_bound": 0.0,
+ "upper_bound": 1.0,
+ },
+ description=MultilingualString(
+ en=(
+ "Specifies the epsilon-tube within which no penalty is associated "
+ "in the training loss function."
+ ),
+ es=(
+ "Especifica el tubo epsilon dentro del cual no se asocia penalización "
+ "en la función de pérdida de entrenamiento."
+ ),
+ ),
+ alias=MultilingualString(en="Epsilon", es="Épsilon"),
+ ) # type: ignore
+
+ gamma: schema_field(
+ enum_field(enum=["scale", "auto"]),
+ placeholder="scale",
+ description=MultilingualString(
+ en=(
+ "Kernel coefficient for 'rbf', 'poly' and 'sigmoid'. "
+ "'scale' uses 1/(n_features * X.var()); 'auto' uses 1/n_features."
+ ),
+ es=(
+ "Coeficiente del kernel para 'rbf', 'poly' y 'sigmoid'. "
+ "'scale' usa 1/(n_features * X.var()); 'auto' usa 1/n_features."
+ ),
+ ),
+ alias=MultilingualString(en="Gamma", es="Gamma"),
+ ) # type: ignore
+
+ max_iter: schema_field(
+ optimizer_int_field(ge=-1),
+ placeholder={
+ "optimize": False,
+ "fixed_value": -1,
+ "lower_bound": 100,
+ "upper_bound": 10000,
+ },
+ description=MultilingualString(
+ en=("Hard limit on iterations within solver. -1 means no limit."),
+ es=(
+ "Límite en iteraciones dentro del solucionador. "
+ "-1 significa sin límite."
+ ),
+ ),
+ alias=MultilingualString(en="Max iterations", es="Máximas iteraciones"),
+ ) # type: ignore
+
+
+class SVR(RegressionModel, SklearnLikeRegressor, _SVR):
+ """Support Vector Regressor using kernel-based function estimation.
+
+ SVR seeks a function that deviates from the targets by at most ``epsilon``
+ (the insensitive tube) while maintaining flatness (controlled by ``C``).
+ Kernel functions allow SVR to capture non-linear relationships. The RBF kernel
+ is effective in many practical scenarios.
+
+ Key hyperparameters include ``kernel``, ``C``, ``epsilon``, ``gamma``, and
+ ``max_iter``. The implementation wraps scikit-learn's ``SVR``.
+
+ References
+ ----------
+ - [1] Vapnik, V.N. (1995). The Nature of Statistical Learning Theory. Springer.
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html
+ """
+
+ SCHEMA = SVRSchema
+ DISPLAY_NAME: str = MultilingualString(
+ en="Support Vector Regression",
+ es="Regresión de Vectores de Soporte",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en="Kernel-based SVR that finds a function within an epsilon-insensitive tube.",
+ es=(
+ "SVR basado en kernel que encuentra una función dentro de un tubo "
+ "insensible a épsilon."
+ ),
+ )
+ COLOR: str = "#EF5350"
+ ICON: str = "ControlPoint"
+ CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT
+
+ def __init__(self, **kwargs) -> None:
+ """Initialise the model by forwarding all kwargs to the parent class.
+
+ Parameters
+ ----------
+ **kwargs : dict
+ Hyperparameter values forwarded to the parent sklearn wrapper.
+ """
+ super().__init__(**kwargs)
diff --git a/DashAI/back/models/scikit_learn/tfidf_logreg_text_classification_model.py b/DashAI/back/models/scikit_learn/tfidf_logreg_text_classification_model.py
new file mode 100644
index 000000000..6e15a5e9c
--- /dev/null
+++ b/DashAI/back/models/scikit_learn/tfidf_logreg_text_classification_model.py
@@ -0,0 +1,186 @@
+"""DashAI TF-IDF + Logistic Regression text classification model."""
+
+from typing import TYPE_CHECKING, Union
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ bool_field,
+ enum_field,
+ float_field,
+ int_field,
+ schema_field,
+)
+from DashAI.back.core.utils import MultilingualString
+from DashAI.back.models.text_classification_model import TextClassificationModel
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
+
+
+class TfIdfLogRegTextClassificationModelSchema(BaseSchema):
+ """Configuration schema for TF-IDF + Logistic Regression text classifier."""
+
+ ngram_min_n: schema_field(
+ int_field(ge=1),
+ placeholder=1,
+ description=MultilingualString(
+ en="Minimum n-gram size for the TF-IDF vectorizer (≥ 1).",
+ es="Tamaño mínimo de n-grama para el vectorizador TF-IDF (≥ 1).",
+ ),
+ alias=MultilingualString(en="Min n-gram", es="N-grama mínimo"),
+ ) # type: ignore
+ ngram_max_n: schema_field(
+ int_field(ge=1),
+ placeholder=1,
+ description=MultilingualString(
+ en="Maximum n-gram size for the TF-IDF vectorizer (≥ 1).",
+ es="Tamaño máximo de n-grama para el vectorizador TF-IDF (≥ 1).",
+ ),
+ alias=MultilingualString(en="Max n-gram", es="N-grama máximo"),
+ ) # type: ignore
+ use_idf: schema_field(
+ bool_field(),
+ placeholder=True,
+ description=MultilingualString(
+ en="Enable inverse-document-frequency re-weighting.",
+ es="Activar re-ponderación por frecuencia inversa de documento.",
+ ),
+ alias=MultilingualString(en="Use IDF", es="Usar IDF"),
+ ) # type: ignore
+ sublinear_tf: schema_field(
+ bool_field(),
+ placeholder=False,
+ description=MultilingualString(
+ en=("Apply sublinear TF scaling (replace TF with 1 + log(TF))."),
+ es=("Aplicar escalado sublineal de TF (reemplazar TF con 1 + log(TF))."),
+ ),
+ alias=MultilingualString(en="Sublinear TF", es="TF sublineal"),
+ ) # type: ignore
+ C: schema_field(
+ float_field(gt=0.0),
+ placeholder=1.0,
+ description=MultilingualString(
+ en=(
+ "Regularization parameter for logistic regression. "
+ "Smaller values mean stronger regularization."
+ ),
+ es=(
+ "Parámetro de regularización para regresión logística. "
+ "Valores más pequeños significan mayor regularización."
+ ),
+ ),
+ alias=MultilingualString(en="C (Regularization)", es="C (Regularización)"),
+ ) # type: ignore
+ max_iter: schema_field(
+ int_field(ge=100),
+ placeholder=1000,
+ description=MultilingualString(
+ en="Maximum number of iterations for the logistic regression solver.",
+ es=("Número máximo de iteraciones para el solver de regresión logística."),
+ ),
+ alias=MultilingualString(en="Max iterations", es="Iteraciones máximas"),
+ ) # type: ignore
+ solver: schema_field(
+ enum_field(["lbfgs", "liblinear", "saga"]),
+ placeholder="lbfgs",
+ description=MultilingualString(
+ en="Optimization algorithm for logistic regression.",
+ es="Algoritmo de optimización para regresión logística.",
+ ),
+ alias=MultilingualString(en="Solver", es="Solver"),
+ ) # type: ignore
+
+
+class TfIdfLogRegTextClassificationModel(TextClassificationModel):
+ """TF-IDF vectorizer combined with Logistic Regression for text classification.
+
+ This model converts raw text into TF-IDF feature vectors using scikit-learn's
+ ``TfidfVectorizer`` with a configurable n-gram range and IDF weighting, then
+ trains a ``LogisticRegression`` classifier on the resulting sparse matrix.
+ It is a strong baseline for text classification tasks, particularly when
+ training data is limited or computational resources are constrained.
+
+ References
+ ----------
+ - [1] https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html
+ - [2] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html
+ """
+
+ DISPLAY_NAME: str = MultilingualString(
+ en="TF-IDF + Logistic Regression",
+ es="TF-IDF + Regresión Logística",
+ )
+ DESCRIPTION: str = MultilingualString(
+ en=(
+ "TF-IDF vectorizer combined with logistic regression "
+ "for text classification."
+ ),
+ es=(
+ "Vectorizador TF-IDF combinado con regresión logística "
+ "para clasificación de texto."
+ ),
+ )
+ COLOR: str = "#00695C"
+ ICON: str = "Article"
+ SCHEMA = TfIdfLogRegTextClassificationModelSchema
+
+ def __init__(self, **kwargs) -> None:
+ from sklearn.feature_extraction.text import TfidfVectorizer
+ from sklearn.linear_model import LogisticRegression
+ from sklearn.preprocessing import LabelEncoder
+
+ self.vectorizer = TfidfVectorizer(
+ ngram_range=(kwargs["ngram_min_n"], kwargs["ngram_max_n"]),
+ use_idf=kwargs["use_idf"],
+ sublinear_tf=kwargs["sublinear_tf"],
+ )
+ self.classifier = LogisticRegression(
+ C=kwargs["C"],
+ max_iter=kwargs["max_iter"],
+ solver=kwargs["solver"],
+ )
+ self.label_encoder = LabelEncoder()
+
+ def train(
+ self,
+ x,
+ y,
+ x_validation=None,
+ y_validation=None,
+ ):
+ input_col = x.column_names[0]
+ output_col = y.column_names[0]
+
+ X_tfidf = self.vectorizer.fit_transform(x[input_col])
+ y_enc = self.label_encoder.fit_transform(y[output_col])
+ self.classifier.fit(X_tfidf, y_enc)
+
+ def predict(self, x):
+ input_col = x.column_names[0]
+ X_tfidf = self.vectorizer.transform(x[input_col])
+ return self.classifier.predict_proba(X_tfidf)
+
+ def prepare_output(self, dataset: "DashAIDataset", is_fit: bool = False):
+ from datasets import Dataset as HFDataset
+
+ from DashAI.back.dataloaders.classes.dashai_dataset import to_dashai_dataset
+
+ col = dataset.column_names[0]
+ if is_fit:
+ encoded = self.label_encoder.fit_transform(dataset[col]).tolist()
+ else:
+ encoded = self.label_encoder.transform(dataset[col]).tolist()
+ return to_dashai_dataset(HFDataset.from_dict({col: encoded}))
+
+ def save(self, filename: Union[str, "Path"]) -> None:
+ import joblib
+
+ joblib.dump(self, filename)
+
+ @staticmethod
+ def load(filename: Union[str, "Path"]):
+ import joblib
+
+ return joblib.load(filename)
diff --git a/DashAI/back/tasks/regression_task.py b/DashAI/back/tasks/regression_task.py
index ccff6c780..1ea8c31b4 100644
--- a/DashAI/back/tasks/regression_task.py
+++ b/DashAI/back/tasks/regression_task.py
@@ -23,14 +23,8 @@ class RegressionTask(BaseTask):
"""
DESCRIPTION: str = MultilingualString(
- en=(
- "Predict continuous numeric values from structured tabular data "
- "using trained regression models."
- ),
- es=(
- "Predice valores numéricos continuos a partir de datos tabulares "
- "estructurados usando modelos de regresión."
- ),
+ en="Predict continuous numeric values from tabular data.",
+ es="Predice valores numéricos continuos a partir de datos tabulares.",
)
DISPLAY_NAME: str = MultilingualString(en="Regression", es="Regresión")
diff --git a/DashAI/back/tasks/tabular_classification_task.py b/DashAI/back/tasks/tabular_classification_task.py
index 9e80757d6..3b941b512 100644
--- a/DashAI/back/tasks/tabular_classification_task.py
+++ b/DashAI/back/tasks/tabular_classification_task.py
@@ -22,13 +22,10 @@ class TabularClassificationTask(ClassificationTask):
"""
DESCRIPTION: str = MultilingualString(
- en=(
- "Predict categorical labels from structured tabular data "
- "(rows and columns) using trained classification models."
- ),
+ en="Predict categorical labels from tabular data (rows and columns).",
es=(
"Predice etiquetas categóricas a partir de datos tabulares "
- "estructurados (filas y columnas) usando modelos de clasificación."
+ "(filas y columnas)."
),
)
DISPLAY_NAME: str = MultilingualString(
diff --git a/DashAI/back/tasks/text_classification_task.py b/DashAI/back/tasks/text_classification_task.py
index cade0f4c4..a1eeb12f3 100644
--- a/DashAI/back/tasks/text_classification_task.py
+++ b/DashAI/back/tasks/text_classification_task.py
@@ -22,6 +22,25 @@ class TextClassificationTask(ClassificationTask):
a predicted class label for each sample.
"""
+ SCORING_PROFILES = {
+ "text_balanced": {
+ "description": "Balanced",
+ "weights": {"Accuracy": 0.3, "F1": 0.4, "ROCAUC": 0.3},
+ },
+ "text_detectPositives": {
+ "description": "Detect Positives",
+ "weights": {"Recall": 0.6, "F1": 0.3, "Precision": 0.1},
+ },
+ "text_avoidFalseAlarms": {
+ "description": "Avoid False Alarms",
+ "weights": {"Precision": 0.6, "F1": 0.3, "Recall": 0.1},
+ },
+ "text_probabilityQuality": {
+ "description": "Probability Quality",
+ "weights": {"ROCAUC": 0.5, "LogLoss": 0.5},
+ },
+ }
+
metadata: dict = {
"inputs_types": [Text],
"outputs_types": [Categorical],
@@ -31,13 +50,12 @@ class TextClassificationTask(ClassificationTask):
DESCRIPTION: str = MultilingualString(
en=(
- "Assign predefined labels to text inputs using NLP models. "
- "Common uses: sentiment analysis, spam detection, intent recognition."
+ "Classify text into predefined categories. "
+ "E.g.: sentiment, spam, intent detection."
),
es=(
- "Asigna etiquetas predefinidas a textos mediante modelos de PLN. "
- "Usos comunes: análisis de sentimientos, detección de spam, "
- "reconocimiento de intenciones."
+ "Clasifica textos en categorías predefinidas. "
+ "Ej.: sentimiento, spam, intención."
),
)
DISPLAY_NAME: str = MultilingualString(
diff --git a/DashAI/back/tasks/translation_task.py b/DashAI/back/tasks/translation_task.py
index fb32fa65e..98e17542c 100644
--- a/DashAI/back/tasks/translation_task.py
+++ b/DashAI/back/tasks/translation_task.py
@@ -42,14 +42,8 @@ class TranslationTask(BaseTask):
"outputs_cardinality": 1,
}
DESCRIPTION: str = MultilingualString(
- en=(
- "Convert text from one language to another while "
- "preserving meaning and context."
- ),
- es=(
- "Convierte texto de un idioma a otro preservando "
- "el significado y el contexto."
- ),
+ en="Convert text from one language to another preserving meaning.",
+ es="Convierte texto de un idioma a otro preservando el significado.",
)
DISPLAY_NAME: str = MultilingualString(en="Translation", es="Traducción")
diff --git a/DashAI/front/src/App.jsx b/DashAI/front/src/App.jsx
index f92a875a6..7bbc3db4c 100644
--- a/DashAI/front/src/App.jsx
+++ b/DashAI/front/src/App.jsx
@@ -28,7 +28,7 @@ function App() {
} />
} />
}
/>
} />
@@ -41,16 +41,13 @@ function App() {
path="/app/models/sessions/new/:taskName"
element={ }
/>
- }
- />
} />
- } />
+ } />
}
/>
+ } />
} />
} />
-
-
-
-
-
- 🇨🇱
- Español
-
-
-
-
- 🇺🇸
- English
-
-
-
-
-
+
+
+
+
+ 🇨🇱
+ Español
+
+
+
+
+ 🇺🇸
+ English
+
+
+
+
);
}
diff --git a/DashAI/front/src/components/ResponsiveAppBar.jsx b/DashAI/front/src/components/ResponsiveAppBar.jsx
index 577df3c2a..d829b16b3 100644
--- a/DashAI/front/src/components/ResponsiveAppBar.jsx
+++ b/DashAI/front/src/components/ResponsiveAppBar.jsx
@@ -13,8 +13,9 @@ import HomeIcon from "@mui/icons-material/HomeOutlined";
import { useTranslation } from "react-i18next";
import LanguageSelector from "./LanguageSelector";
import { ColorModeContext } from "../contexts/ThemeContext";
-import Brightness4Icon from "@mui/icons-material/Brightness4";
-import Brightness7Icon from "@mui/icons-material/Brightness7";
+import DarkModeOutlinedIcon from "@mui/icons-material/DarkModeOutlined";
+import LightModeOutlinedIcon from "@mui/icons-material/LightModeOutlined";
+import Tooltip from "@mui/material/Tooltip";
import HardwareMonitorButton from "./hardware/HardwareMonitorButton";
import NavbarTourButton from "./tour/NavbarTourButton";
@@ -41,8 +42,8 @@ function ResponsiveAppBar() {
const iconBtnSx = React.useMemo(
() => ({
- width: 28,
- height: 28,
+ width: 32,
+ height: 32,
borderRadius: "4px",
border: `1px solid ${theme.palette.divider}`,
color: theme.palette.text.secondary,
@@ -86,9 +87,8 @@ function ResponsiveAppBar() {
}}
>
-
- {theme.palette.mode === "dark" ? (
-
- ) : (
-
- )}
-
+
+ {theme.palette.mode === "dark" ? (
+
+ ) : (
+
+ )}
+
+
diff --git a/DashAI/front/src/components/configurableObject/FormTooltip.jsx b/DashAI/front/src/components/configurableObject/FormTooltip.jsx
index 1c54a1d24..ed6a5d905 100644
--- a/DashAI/front/src/components/configurableObject/FormTooltip.jsx
+++ b/DashAI/front/src/components/configurableObject/FormTooltip.jsx
@@ -1,7 +1,9 @@
import React from "react";
-import { Tooltip, IconButton, Typography } from "@mui/material";
+import { Tooltip, IconButton, Typography, Link } from "@mui/material";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import PropTypes from "prop-types";
+import ReactMarkdown from "react-markdown";
+
/**
* This component renders a tooltip containing a description for each parameter in a form,
* providing users with additional information to better understand the purpose of each input field.
@@ -10,7 +12,31 @@ import PropTypes from "prop-types";
function FormTooltip({ contentStr = "", error }) {
return (
{contentStr}}
+ title={
+
+ (
+ {children}
+ ),
+ a: ({ href, children }) => (
+ e.stopPropagation()}
+ >
+ {children}
+
+ ),
+ }}
+ >
+ {contentStr}
+
+
+ }
placement="bottom"
arrow
>
diff --git a/DashAI/front/src/components/custom/ComponentDetailsPanel.jsx b/DashAI/front/src/components/custom/ComponentDetailsPanel.jsx
new file mode 100644
index 000000000..f2ef101da
--- /dev/null
+++ b/DashAI/front/src/components/custom/ComponentDetailsPanel.jsx
@@ -0,0 +1,237 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Box, Stack, Typography, Chip, Divider, Link } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import SideBar from "../threeSectionLayout/panelContainers/SideBar";
+import { useTheme } from "@mui/material/styles";
+
+function getLabel(component) {
+ return component.display_name || component.name;
+}
+
+function getDescription(component) {
+ return component.description ?? component.schema?.description ?? "";
+}
+
+const URL_PATTERN = /(\[([^\]]+)\]\((https?:\/\/[^)]+)\))|(https?:\/\/\S+)/g;
+const TRAILING_PUNCT = /[.,;:!?)\]>'"]+$/;
+
+function DescriptionText({ text }) {
+ if (!text) return null;
+ const parts = [];
+ let last = 0;
+ let match;
+ URL_PATTERN.lastIndex = 0;
+ while ((match = URL_PATTERN.exec(text)) !== null) {
+ if (match.index > last) parts.push(text.slice(last, match.index));
+ if (match[1]) {
+ // Markdown link [label](url) — closing ) already excluded by the pattern
+ parts.push(
+
+ {match[2]}
+ ,
+ );
+ } else {
+ // Bare URL — strip trailing punctuation that belongs to surrounding prose
+ const rawUrl = match[0];
+ const cleanUrl = rawUrl.replace(TRAILING_PUNCT, "");
+ const trailing = rawUrl.slice(cleanUrl.length);
+ parts.push(
+
+ {cleanUrl}
+ ,
+ );
+ if (trailing) parts.push(trailing);
+ }
+ last = match.index + match[0].length;
+ }
+ if (last < text.length) parts.push(text.slice(last));
+ return parts;
+}
+
+function ComponentDetailsPanel({
+ component,
+ getIcon,
+ extraSections,
+ categoryKey = "type",
+}) {
+ const { t } = useTranslation("custom");
+ const theme = useTheme();
+
+ return (
+
+
+ {/* Title */}
+
+
+ {t("componentDetails")}
+
+
+
+ {/* Content */}
+ {!component || !component.name ? (
+
+
+ {t("selectAnItemToShowInfo")}
+
+
+ ) : (
+
+
+
+ {getIcon?.(component) && (
+
+ {getIcon(component)}
+
+ )}
+
+
+ {component.display_name || component.name}
+
+ {component[categoryKey] && (
+
+ {component[categoryKey]}
+
+ )}
+
+
+
+
+
+ {t("description")}
+
+
+ {getDescription(component) ? (
+
+ ) : (
+ t("noDescriptionAvailable")
+ )}
+
+
+
+ {(component.schema?.tags || component.metadata?.tags || [])
+ .length > 0 && (
+
+
+ {t("tags")}
+
+
+ {(component.schema?.tags || component.metadata?.tags).map(
+ (tag) => (
+
+ ),
+ )}
+
+
+ )}
+
+ {extraSections &&
+ extraSections.map((section) => (
+
+
+
+ {section.title}
+
+ {section.content}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+ComponentDetailsPanel.propTypes = {
+ component: PropTypes.shape({
+ name: PropTypes.string,
+ display_name: PropTypes.string,
+ type: PropTypes.string,
+ description: PropTypes.string,
+ schema: PropTypes.object,
+ metadata: PropTypes.object,
+ }),
+ getIcon: PropTypes.func,
+ categoryKey: PropTypes.string,
+ extraSections: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string.isRequired,
+ content: PropTypes.node.isRequired,
+ }),
+ ),
+};
+
+export default ComponentDetailsPanel;
diff --git a/DashAI/front/src/components/custom/ComponentSelector.jsx b/DashAI/front/src/components/custom/ComponentSelector.jsx
new file mode 100644
index 000000000..e2b2ad2bb
--- /dev/null
+++ b/DashAI/front/src/components/custom/ComponentSelector.jsx
@@ -0,0 +1,347 @@
+import React, { useMemo, useState } from "react";
+import PropTypes from "prop-types";
+import {
+ Box,
+ Stack,
+ TextField,
+ InputAdornment,
+ IconButton,
+ Chip,
+ Typography,
+ Collapse,
+ Paper,
+} from "@mui/material";
+import {
+ Search as SearchIcon,
+ Clear as ClearIcon,
+ ExpandMore as ExpandMoreIcon,
+ Check as CheckIcon,
+} from "@mui/icons-material";
+import { useTranslation } from "react-i18next";
+
+const ALL_CATEGORY = "All";
+
+function getLabel(component) {
+ return component.display_name || component.name;
+}
+
+function getDescription(component, fallback = "") {
+ return component.description ?? fallback;
+}
+
+function ComponentSelector({
+ components,
+ selected = null,
+ onSelect,
+ categoryKey = "type",
+ searchPlaceholder,
+ emptyText,
+ getIcon,
+}) {
+ const { t } = useTranslation("custom");
+ const [search, setSearch] = useState("");
+ const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY);
+
+ const categories = useMemo(() => {
+ const set = new Set();
+ components.forEach((c) => {
+ const cat = c[categoryKey];
+ if (cat) set.add(cat);
+ });
+ return [ALL_CATEGORY, ...Array.from(set).sort()];
+ }, [components, categoryKey]);
+
+ const [expanded, setExpanded] = useState(() => new Set(categories));
+
+ React.useEffect(() => {
+ setExpanded(new Set(categories));
+ }, [categories]);
+
+ const toggleCategory = (cat) => {
+ setExpanded((prev) => {
+ const next = new Set(prev);
+ if (next.has(cat)) next.delete(cat);
+ else next.add(cat);
+ return next;
+ });
+ };
+
+ const filtered = useMemo(() => {
+ const q = search.trim().toLowerCase();
+ return components.filter((c) => {
+ const matchesCategory =
+ activeCategory === ALL_CATEGORY || c[categoryKey] === activeCategory;
+ if (!matchesCategory) return false;
+ if (q === "") return true;
+ const label = getLabel(c).toLowerCase();
+ const desc = getDescription(c, t("noDescriptionAvailable")).toLowerCase();
+ return label.includes(q) || desc.includes(q);
+ });
+ }, [components, search, activeCategory, categoryKey]);
+
+ const grouped = useMemo(() => {
+ const groups = {};
+ filtered.forEach((c) => {
+ const cat = c[categoryKey] || "Other";
+ if (!groups[cat]) groups[cat] = [];
+ groups[cat].push(c);
+ });
+ return groups;
+ }, [filtered, categoryKey]);
+
+ const counts = useMemo(() => {
+ const map = {};
+ components.forEach((c) => {
+ const cat = c[categoryKey];
+ if (cat) map[cat] = (map[cat] || 0) + 1;
+ });
+ return map;
+ }, [components, categoryKey]);
+
+ const handleSelect = (component) => onSelect?.(component);
+
+ return (
+
+ setSearch(e.target.value)}
+ slotProps={{
+ input: {
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: search ? (
+
+ setSearch("")}>
+
+
+
+ ) : null,
+ },
+ }}
+ />
+
+
+ {categories.map((cat) => {
+ const isActive = activeCategory === cat;
+ const count = cat === ALL_CATEGORY ? components.length : counts[cat];
+ return (
+ setActiveCategory(cat)}
+ size="small"
+ />
+ );
+ })}
+
+
+
+
+ {Object.entries(grouped).map(([cat, items]) => {
+ const isOpen = expanded.has(cat);
+ return (
+
+ toggleCategory(cat)}
+ sx={{
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ py: 1,
+ cursor: "pointer",
+ color: "text.secondary",
+ "&:hover": { color: "text.primary" },
+ }}
+ >
+
+
+ {cat}
+
+
+
+
+
+
+
+ {items.map((component) => {
+ const isSelected = selected?.name === component.name;
+ const icon = getIcon?.(component);
+ return (
+ handleSelect(component)}
+ sx={{
+ p: 1.5,
+ display: "flex",
+ gap: 1.5,
+ alignItems: "flex-start",
+ cursor: "pointer",
+ border: 1,
+ borderColor: isSelected
+ ? "primary.main"
+ : "divider",
+ bgcolor: isSelected
+ ? "action.selected"
+ : "background.paper",
+ transition: "border-color 0.15s, background 0.15s",
+ "&:hover": {
+ borderColor: "primary.light",
+ },
+ }}
+ >
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ {getLabel(component)}
+
+
+ {getDescription(
+ component,
+ t("noDescriptionAvailable"),
+ )}
+
+
+ {isSelected && (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+ {filtered.length === 0 && (
+
+
+
+ {emptyText ?? t("noItemsFound")}
+
+
+ {t("tryAdjustingSearch")}
+
+
+ )}
+
+
+
+
+
+ {t("componentsAvailable", { count: filtered.length })}
+
+ {selected && (
+ }
+ label={getLabel(selected)}
+ color="primary"
+ variant="outlined"
+ size="small"
+ />
+ )}
+
+
+ );
+}
+
+ComponentSelector.propTypes = {
+ components: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ type: PropTypes.string,
+ description: PropTypes.string,
+ schema: PropTypes.object,
+ }),
+ ).isRequired,
+ selected: PropTypes.shape({ name: PropTypes.string }),
+ onSelect: PropTypes.func.isRequired,
+ categoryKey: PropTypes.string,
+ searchPlaceholder: PropTypes.string,
+ emptyText: PropTypes.string,
+ getIcon: PropTypes.func,
+};
+
+export default ComponentSelector;
diff --git a/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx b/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx
index 771498120..4fdbd8775 100644
--- a/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx
+++ b/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx
@@ -48,6 +48,7 @@ export const DatasetsAndNotebooksProvider = ({ children }) => {
const [selectedOption, setSelectedOption] = useState(OptionsEnum.NEW); // "datasets" or "notebooks"
const [rightBarContent, setRightBarContent] = useState(null);
+ const [uploadDataloader, setUploadDataloader] = useState(null);
const [datasetInfo, setDatasetInfo] = useState(null);
const [datasetTab, setDatasetTab] = useState(0);
@@ -88,6 +89,8 @@ export const DatasetsAndNotebooksProvider = ({ children }) => {
setDatasetInfo,
datasetTab,
setDatasetTab,
+ uploadDataloader,
+ setUploadDataloader,
};
return (
diff --git a/DashAI/front/src/components/datasets/DatasetModal.jsx b/DashAI/front/src/components/datasets/DatasetModal.jsx
index 50478c287..2c3193d3f 100644
--- a/DashAI/front/src/components/datasets/DatasetModal.jsx
+++ b/DashAI/front/src/components/datasets/DatasetModal.jsx
@@ -265,7 +265,7 @@ function DatasetModal({ open, setOpen, updateDatasets }) {
{steps.map((step, index) => (
index}
+ completed={false}
disabled={activeStep < index}
>
diff --git a/DashAI/front/src/components/explainers/InlineExplainerCreator.jsx b/DashAI/front/src/components/explainers/InlineExplainerCreator.jsx
index d24543e8d..01c1a69d7 100644
--- a/DashAI/front/src/components/explainers/InlineExplainerCreator.jsx
+++ b/DashAI/front/src/components/explainers/InlineExplainerCreator.jsx
@@ -315,7 +315,7 @@ export default function InlineExplainerCreator({
{steps.map((step, index) => (
index}
+ completed={false}
disabled={activeStep < index}
>
diff --git a/DashAI/front/src/components/explainers/NewGlobalExplainerModal.jsx b/DashAI/front/src/components/explainers/NewGlobalExplainerModal.jsx
index 48dbd0211..9ac225d5b 100644
--- a/DashAI/front/src/components/explainers/NewGlobalExplainerModal.jsx
+++ b/DashAI/front/src/components/explainers/NewGlobalExplainerModal.jsx
@@ -295,7 +295,7 @@ export default function NewGlobalExplainerModal({
{steps.map((step, index) => (
index}
+ completed={false}
disabled={activeStep < index}
>
diff --git a/DashAI/front/src/components/explainers/NewLocalExplainerModal.jsx b/DashAI/front/src/components/explainers/NewLocalExplainerModal.jsx
index a85e559fe..c22c30637 100644
--- a/DashAI/front/src/components/explainers/NewLocalExplainerModal.jsx
+++ b/DashAI/front/src/components/explainers/NewLocalExplainerModal.jsx
@@ -301,7 +301,7 @@ export default function NewLocalExplainerModal({
{steps.map((step, index) => (
index}
+ completed={false}
disabled={activeStep < index}
>
diff --git a/DashAI/front/src/components/explorations/ExplorationEditor.jsx b/DashAI/front/src/components/explorations/ExplorationEditor.jsx
index 1b9b41dfe..db8e07797 100644
--- a/DashAI/front/src/components/explorations/ExplorationEditor.jsx
+++ b/DashAI/front/src/components/explorations/ExplorationEditor.jsx
@@ -230,7 +230,7 @@ function ExplorationEditor({ open = true, handleCloseDialog = () => {} }) {
{steps.map((step, index) => (
index}
+ completed={false}
disabled={activeStep < index}
>
diff --git a/DashAI/front/src/components/explorations/Steps/ConfigureExplorersStep.jsx b/DashAI/front/src/components/explorations/Steps/ConfigureExplorersStep.jsx
index 5de494f3e..e227bafad 100644
--- a/DashAI/front/src/components/explorations/Steps/ConfigureExplorersStep.jsx
+++ b/DashAI/front/src/components/explorations/Steps/ConfigureExplorersStep.jsx
@@ -37,8 +37,8 @@ const renderOption = (props, option, _, ownerState) => {
title={
{option.tooltip}
diff --git a/DashAI/front/src/components/generative/ChatBubble.jsx b/DashAI/front/src/components/generative/ChatBubble.jsx
index ac72d8292..735706689 100644
--- a/DashAI/front/src/components/generative/ChatBubble.jsx
+++ b/DashAI/front/src/components/generative/ChatBubble.jsx
@@ -1,4 +1,4 @@
-import { Box } from "@mui/material";
+import { Box, Typography } from "@mui/material";
import { ChatAvatar } from "./ChatAvatar";
import { ChatTimestamp } from "./ChatTimeStamp";
import { MessageContent } from "./MessageContent";
@@ -25,18 +25,18 @@ export function ChatBubble({
{!isUser && sender && (
-
{sender}
-
+
)}
+
+
+
+ {step === 0
+ ? t("generative:label.selectModel")
+ : t("generative:label.configureSession")}
+
+
+ {step === 0
+ ? t("generative:label.pickAModelGroupedByTask")
+ : t("generative:label.nameAndDescribeYourSession")}
+
+
+
+
+ {step === 0 ? (
+ loadingModels ? (
+
+
+
+ ) : (
+
+ )
+ ) : (
+
+
+
+
+ )}
+
+
+
+
+ {t("common:back")}
+
+ {step === 0 ? (
+
+ {t("common:next")}
+
+ ) : (
+
+ {submitting
+ ? t("common:saving")
+ : t("generative:button.createSession")}
+
+ )}
+
+
+ );
+}
diff --git a/DashAI/front/src/components/generative/CreateSessionContext.jsx b/DashAI/front/src/components/generative/CreateSessionContext.jsx
new file mode 100644
index 000000000..e544f2883
--- /dev/null
+++ b/DashAI/front/src/components/generative/CreateSessionContext.jsx
@@ -0,0 +1,223 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useFormik } from "formik";
+import { useSnackbar } from "notistack";
+import { useTranslation } from "react-i18next";
+import {
+ createGenerativeSession,
+ getRelatedComponents,
+} from "../../api/generativeTask";
+import { generateSequentialName } from "../../utils/nameGenerator";
+import {
+ buildYupSchema,
+ formatTaskNameForSession,
+ preprocessSchema,
+} from "./utils";
+import { useGenerative } from "./GenerativeContext";
+
+const CreateSessionContext = createContext(null);
+
+export const useCreateSession = () => useContext(CreateSessionContext);
+
+export function CreateSessionProvider({ children }) {
+ const navigate = useNavigate();
+ const { modelName } = useParams();
+ const { enqueueSnackbar } = useSnackbar();
+ const { t } = useTranslation(["generative", "common"]);
+ const { tasks, sessions: existingSessions, setSessions } = useGenerative();
+
+ const step = modelName ? 1 : 0;
+ const [models, setModels] = useState([]);
+ const [loadingModels, setLoadingModels] = useState(true);
+ const [selectedModel, setSelectedModel] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+
+ // Load all generative models grouped by their compatible task.
+ // Re-fetches when language changes so display_name/description are translated.
+ useEffect(() => {
+ if (!tasks || tasks.length === 0) return;
+ let cancelled = false;
+ setLoadingModels(true);
+
+ Promise.all(
+ tasks.map((task) =>
+ getRelatedComponents(task.name).then((components) =>
+ components.map((c) => ({
+ ...c,
+ task_name: task.name,
+ task_display_name: task.display_name || task.name,
+ })),
+ ),
+ ),
+ )
+ .then((perTaskLists) => {
+ if (cancelled) return;
+ // Deduplicate by model name (a model may appear under several tasks).
+ const seen = new Set();
+ const flat = [];
+ perTaskLists.flat().forEach((m) => {
+ if (seen.has(m.name)) return;
+ seen.add(m.name);
+ flat.push(m);
+ });
+ setModels(flat);
+ })
+ .catch((err) => {
+ console.error("Failed to load generative models", err);
+ enqueueSnackbar(t("generative:error.failedToLoadModels"), {
+ variant: "error",
+ });
+ })
+ .finally(() => {
+ if (!cancelled) setLoadingModels(false);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [tasks, enqueueSnackbar, t]);
+
+ const processedProperties = useMemo(
+ () =>
+ selectedModel?.schema?.properties
+ ? preprocessSchema(selectedModel.schema.properties)
+ : {},
+ [selectedModel],
+ );
+
+ const validationSchema = useMemo(
+ () =>
+ Object.keys(processedProperties).length > 0
+ ? buildYupSchema(processedProperties)
+ : null,
+ [processedProperties],
+ );
+
+ const formik = useFormik({
+ initialValues: { name: "", description: "" },
+ validationSchema,
+ enableReinitialize: false,
+ validate: (values) => {
+ const errors = {};
+ if (!values.name || values.name.trim() === "") {
+ errors.name = t("generative:error.nameRequired");
+ }
+ return errors;
+ },
+ onSubmit: async (values) => {
+ if (!selectedModel) return;
+ setSubmitting(true);
+ try {
+ const created = await createGenerativeSession({
+ name: values.name,
+ description: values.description,
+ task_name: selectedModel.task_name,
+ model_name: selectedModel.name,
+ parameters: values,
+ });
+ setSessions((prev) => [...prev, created]);
+ enqueueSnackbar(t("generative:message.sessionCreatedSuccess"), {
+ variant: "success",
+ });
+ navigate(`/app/generative/sessions/${created.id}`);
+ } catch (error) {
+ console.error("Error creating session:", error);
+ const detail = error?.response?.data?.detail || "";
+ if (
+ error?.response?.status === 409 ||
+ detail.includes("already exists")
+ ) {
+ enqueueSnackbar(t("generative:error.sessionNameExists"), {
+ variant: "error",
+ });
+ } else {
+ enqueueSnackbar(t("generative:error.failedToCreateSession"), {
+ variant: "error",
+ });
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ },
+ });
+
+ // When a model is selected, seed formik with its parameter defaults and a
+ // freshly computed default session name. Computing the name here (rather than
+ // in an effect) avoids the render-ordering race where the "fill empty name"
+ // effect fires before resetForm has cleared the previous model's name.
+ const handleSelectModel = useCallback(
+ (model) => {
+ setSelectedModel(model);
+ const props = model?.schema?.properties
+ ? preprocessSchema(model.schema.properties)
+ : {};
+ const paramDefaults = Object.keys(props).reduce((acc, key) => {
+ acc[key] = props[key].placeholder ?? "";
+ return acc;
+ }, {});
+ const { defaultName } = generateSequentialName({
+ base: `${formatTaskNameForSession(model.task_name)}_Session`,
+ items: existingSessions,
+ getName: (session) => session.name,
+ filter: (session) => session.task_name === model.task_name,
+ });
+ formik.resetForm({
+ values: {
+ name: defaultName,
+ description: "",
+ ...paramDefaults,
+ },
+ });
+ },
+ [existingSessions],
+ );
+
+ // Sync selectedModel from URL param on load and after language-triggered
+ // model refetch so display_name / description reflect the active language.
+ useEffect(() => {
+ if (!modelName || models.length === 0) return;
+ const match = models.find((m) => m.name === modelName);
+ if (match) handleSelectModel(match);
+ }, [modelName, models]);
+
+ const handleNext = () => {
+ if (step === 0 && selectedModel)
+ navigate(`/app/generative/sessions/new/${selectedModel.name}`);
+ };
+
+ const handleBack = () => {
+ if (step === 1) navigate("/app/generative/sessions/new");
+ else navigate("/app/generative");
+ };
+
+ const handleCreate = () => {
+ formik.submitForm();
+ };
+
+ const value = {
+ step,
+ models,
+ loadingModels,
+ selectedModel,
+ handleSelectModel,
+ formik,
+ processedProperties,
+ submitting,
+ handleNext,
+ handleBack,
+ handleCreate,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/DashAI/front/src/components/generative/CreateSessionLanding.jsx b/DashAI/front/src/components/generative/CreateSessionLanding.jsx
new file mode 100644
index 000000000..91d52f45b
--- /dev/null
+++ b/DashAI/front/src/components/generative/CreateSessionLanding.jsx
@@ -0,0 +1,27 @@
+import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import SelectOptionMenu from "../threeSectionLayout/SelectOptionMenu";
+
+export default function CreateSessionLanding() {
+ const navigate = useNavigate();
+ const { t } = useTranslation(["generative", "common"]);
+
+ return (
+ navigate("/app/generative/sessions/new")}
+ title={t("generative:label.generativeModule")}
+ subtitle={t("generative:label.createNewSessionDescription")}
+ options={[
+ {
+ name: "new_session",
+ display_name: t("generative:label.createNewSession"),
+ description: t("generative:label.createNewSessionDescription"),
+ Icon: AutoAwesomeIcon,
+ },
+ ]}
+ searchBar={false}
+ dataTour="create-session-landing"
+ >
+ );
+}
diff --git a/DashAI/front/src/components/generative/CreateSessionRight.jsx b/DashAI/front/src/components/generative/CreateSessionRight.jsx
new file mode 100644
index 000000000..c155cc8e9
--- /dev/null
+++ b/DashAI/front/src/components/generative/CreateSessionRight.jsx
@@ -0,0 +1,75 @@
+import { Box, Divider, Typography } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import SideBar from "../threeSectionLayout/panelContainers/SideBar";
+import ComponentDetailsPanel from "../custom/ComponentDetailsPanel";
+import FormSchemaRenderFields from "../shared/FormSchemaRenderFields";
+import { useTheme } from "@mui/material/styles";
+import { useCreateSession } from "./CreateSessionContext";
+
+export default function CreateSessionRight() {
+ const { t } = useTranslation(["generative", "common"]);
+ const theme = useTheme();
+ const { step, selectedModel, formik, processedProperties } =
+ useCreateSession();
+
+ if (step === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Title */}
+
+
+ {t("common:modelParameters")}
+
+
+
+ {/* Content */}
+ {Object.keys(processedProperties).length === 0 ? (
+
+
+ {t("generative:label.modelHasNoParameters")}
+
+
+ ) : (
+
+ {
+ formik.setValues((prev) => ({ ...prev, ...updatedValues }));
+ }}
+ onFormSubmit={formik.handleSubmit}
+ setError={(error) => console.error(error)}
+ errorsMessage={formik.errors || {}}
+ spacing={2}
+ />
+
+ )}
+
+ );
+}
diff --git a/DashAI/front/src/components/generative/GenerativeBreadcrumbs.jsx b/DashAI/front/src/components/generative/GenerativeBreadcrumbs.jsx
index 6305e3412..fab27fdd6 100644
--- a/DashAI/front/src/components/generative/GenerativeBreadcrumbs.jsx
+++ b/DashAI/front/src/components/generative/GenerativeBreadcrumbs.jsx
@@ -7,23 +7,37 @@ import Box from "@mui/material/Box";
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useGenerative } from "./GenerativeContext";
+import { useCreateSession } from "./CreateSessionContext";
export default function GenerativeBreadcrumbs() {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
const { t } = useTranslation(["generative", "common"]);
- const { tasks, sessions } = useGenerative();
+ const { sessions } = useGenerative();
+ const createSession = useCreateSession();
const rootCrumb = { label: t("common:generative"), path: "/app/generative" };
const getBreadcrumbs = () => {
const path = location.pathname;
- if (path.startsWith("/app/generative/sessions/new/") && params.taskName) {
- const task = tasks.find((tk) => tk.name === params.taskName);
- const taskLabel = task?.display_name ?? params.taskName;
- return [rootCrumb, { label: taskLabel, path: null, current: true }];
+ if (path.startsWith("/app/generative/sessions/new")) {
+ const modelNameParam = params.modelName;
+ const selectedModel = createSession?.selectedModel;
+ const modelLabel = selectedModel?.display_name || modelNameParam;
+ const crumbs = [
+ rootCrumb,
+ {
+ label: t("generative:label.selectModelCrumb"),
+ path: modelNameParam ? "/app/generative/sessions/new" : null,
+ current: !modelNameParam,
+ },
+ ];
+ if (modelNameParam) {
+ crumbs.push({ label: modelLabel, path: null, current: true });
+ }
+ return crumbs;
}
if (path.startsWith("/app/generative/sessions/") && params.id) {
diff --git a/DashAI/front/src/components/generative/GenerativeChat.jsx b/DashAI/front/src/components/generative/GenerativeChat.jsx
index 3e9b1690e..338edb3e7 100644
--- a/DashAI/front/src/components/generative/GenerativeChat.jsx
+++ b/DashAI/front/src/components/generative/GenerativeChat.jsx
@@ -208,7 +208,6 @@ export default function GenerativeChat() {
flexDirection="column"
justifyContent="flex-start"
alignItems="center"
- gap={1}
width={"100%"}
height={"100%"}
sx={{ overflow: "hidden", minHeight: 0 }}
@@ -223,7 +222,6 @@ export default function GenerativeChat() {
alignItems: "center",
borderRadius: 1,
opacity: 0.5,
- mb: 0.8,
}}
>
{sessionInfo?.name ? sessionInfo.name : "Untitled Session"}{" "}
@@ -254,7 +254,7 @@ export default function GenerativeChat() {
-
+
{/* Chat display */}
{message.type === "history" ? (
-
+
Parameters updated: {message.changedMessage}
diff --git a/DashAI/front/src/components/generative/ModelCard.jsx b/DashAI/front/src/components/generative/ModelCard.jsx
deleted file mode 100644
index cfc7dd465..000000000
--- a/DashAI/front/src/components/generative/ModelCard.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { Box, Link, Paper, Typography } from "@mui/material";
-import { alpha, useTheme } from "@mui/material/styles";
-import CheckCircleIcon from "@mui/icons-material/CheckCircle";
-import { useTranslation } from "react-i18next";
-
-const DESCRIPTION_LINE_CLAMP = 3;
-const URL_SPLIT_REGEX = /(https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]}])/g;
-const URL_TEST_REGEX = /^https?:\/\//;
-
-function renderDescription(text) {
- return text.split(URL_SPLIT_REGEX).map((part, i) => {
- if (URL_TEST_REGEX.test(part)) {
- return (
- e.stopPropagation()}
- sx={{ wordBreak: "break-all" }}
- >
- {part}
-
- );
- }
- return part;
- });
-}
-
-export default function ModelCard({
- model,
- color,
- isSelected,
- onClick,
- dataTour,
-}) {
- const theme = useTheme();
- const [expanded, setExpanded] = useState(false);
- const [isClamped, setIsClamped] = useState(false);
- const { t } = useTranslation(["generative"]);
- const descRef = useRef(null);
-
- useEffect(() => {
- const el = descRef.current;
- if (el) {
- setIsClamped(el.scrollHeight > el.clientHeight + 1);
- }
- }, [model.description]);
-
- const handleToggleExpand = (e) => {
- e.stopPropagation();
- setExpanded((prev) => !prev);
- };
-
- return (
-
-
-
- {model.display_name ? model.display_name : model.name}
-
- {isSelected && (
-
- )}
-
-
-
- {model.description
- ? renderDescription(model.description)
- : t("generative:label.noDescriptionAvailable")}
-
-
- {(isClamped || expanded) && (
-
- {expanded
- ? t("generative:label.showLess")
- : t("generative:label.readMore")}
-
- )}
-
- );
-}
diff --git a/DashAI/front/src/components/generative/ModelGrid.jsx b/DashAI/front/src/components/generative/ModelGrid.jsx
deleted file mode 100644
index 020ebcce0..000000000
--- a/DashAI/front/src/components/generative/ModelGrid.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Box, Grid, Pagination } from "@mui/material";
-import ModelCard from "./ModelCard";
-
-export const MODEL_COLORS = [
- "#1976d2",
- "#7b1fa2",
- "#2e7d32",
- "#e65100",
- "#0288d1",
- "#c62828",
-];
-
-export const MODELS_PER_PAGE = 4;
-
-export default function ModelGrid({
- models,
- selectedModelName,
- page,
- onSelect,
- onPageChange,
-}) {
- const pageModels = models.slice(
- (page - 1) * MODELS_PER_PAGE,
- page * MODELS_PER_PAGE,
- );
-
- return (
- <>
-
- {pageModels.map((model, indexOnPage) => {
- const globalIndex = (page - 1) * MODELS_PER_PAGE + indexOnPage;
- const color =
- model.color ?? MODEL_COLORS[globalIndex % MODEL_COLORS.length];
- return (
-
- onSelect(model, globalIndex)}
- dataTour={
- model.name.toLowerCase().includes("qwen")
- ? "model-card-qwen"
- : undefined
- }
- />
-
- );
- })}
-
-
- {models.length > MODELS_PER_PAGE && (
-
- onPageChange(value)}
- color="primary"
- size="small"
- />
-
- )}
- >
- );
-}
diff --git a/DashAI/front/src/components/generative/ParamsBar.jsx b/DashAI/front/src/components/generative/ParamsBar.jsx
index 4e199e067..acf075cc2 100644
--- a/DashAI/front/src/components/generative/ParamsBar.jsx
+++ b/DashAI/front/src/components/generative/ParamsBar.jsx
@@ -123,119 +123,108 @@ export default function ParamsBar({ onToggle }) {
-
-
- {t("common:modelParameters")}
-
+
+ {t("common:modelParameters")}
+
- {/* Parameter History Modal */}
- {selectedSessionId && (
- {
- getHistory();
- setHistoryInfoVisible(true);
- }}
- sx={{
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- }}
- >
-
-
- )}
-
- {/* Divider */}
-
- {selectedSessionId ? (
-
-
-
- ) : (
- {
+ getHistory();
+ setHistoryInfoVisible(true);
+ }}
sx={{
- flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
- p: 2,
}}
>
-
- {t("generative:label.selectSessionToViewParameters")}
-
-
+
+
)}
-
- {/* Parameter History Modal */}
-
+
+ {selectedSessionId ? (
+
+
+
+ ) : (
+
+
+ {t("generative:label.selectSessionToViewParameters")}
+
+
+ )}
+
+ {/* Parameter History Modal */}
+
);
}
diff --git a/DashAI/front/src/components/generative/SelectModelMenu.jsx b/DashAI/front/src/components/generative/SelectModelMenu.jsx
deleted file mode 100644
index 76532b6f3..000000000
--- a/DashAI/front/src/components/generative/SelectModelMenu.jsx
+++ /dev/null
@@ -1,232 +0,0 @@
-import { useEffect, useState, useMemo, useRef } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box, Button, Typography } from "@mui/material";
-import { useFormik } from "formik";
-import { useSnackbar } from "notistack";
-import { useTranslation } from "react-i18next";
-import { useTourContext } from "../tour/TourProvider";
-import {
- getRelatedComponents,
- createGenerativeSession,
-} from "../../api/generativeTask";
-import {
- preprocessSchema,
- buildYupSchema,
- formatTaskNameForSession,
-} from "./utils";
-import { generateSequentialName } from "../../utils/nameGenerator";
-import { useGenerative } from "./GenerativeContext";
-import ModelGrid from "./ModelGrid";
-import SessionForm from "./SessionForm";
-
-export default function SelectModelMenu() {
- const navigate = useNavigate();
- const {
- selectedTaskName,
- selectedDisplayName,
- sessions: existingSessions,
- setSessions,
- } = useGenerative();
-
- const [relatedComponents, setRelatedComponents] = useState([]);
- const [selectedModel, setSelectedModel] = useState(null);
- const [validationSchema, setValidationSchema] = useState(null);
- const [page, setPage] = useState(1);
- const [nameError, setNameError] = useState(false);
- const [nameErrorMessage, setNameErrorMessage] = useState("");
-
- const { t } = useTranslation(["generative", "common"]);
- const { enqueueSnackbar } = useSnackbar();
- const tourContext = useTourContext();
- const hasAdvancedTourRef = useRef(false);
-
- const { defaultName } = useMemo(() => {
- if (!selectedTaskName) return { defaultName: "" };
- return generateSequentialName({
- base: `${formatTaskNameForSession(selectedTaskName)}_Session`,
- items: existingSessions,
- getName: (session) => session.name,
- filter: (session) => session.task_name === selectedTaskName,
- });
- }, [selectedTaskName, existingSessions]);
-
- useEffect(() => {
- if (!selectedTaskName) return;
- getRelatedComponents(selectedTaskName).then((components) => {
- setRelatedComponents(components);
- setPage(1);
- setSelectedModel((prev) =>
- prev ? (components.find((c) => c.name === prev.name) ?? prev) : null,
- );
- });
- }, [selectedTaskName, t]);
-
- useEffect(() => {
- if (!selectedModel?.schema?.properties) return;
- const processedProps = preprocessSchema(selectedModel.schema.properties);
- setValidationSchema(buildYupSchema(processedProps));
- const initialValues = Object.keys(processedProps).reduce(
- (acc, key) => {
- acc[key] = processedProps[key].placeholder || "";
- return acc;
- },
- { name: defaultName || "", description: "" },
- );
- formik.setValues(initialValues);
- }, [selectedModel, defaultName]);
-
- const formik = useFormik({
- initialValues: { name: "", description: "" },
- validationSchema,
- enableReinitialize: true,
- onSubmit: async (values) => {
- if (!values.name || values.name.trim() === "") {
- setNameError(true);
- setNameErrorMessage(t("generative:error.nameRequired"));
- return;
- }
- try {
- const createdSession = await createGenerativeSession({
- name: values.name,
- description: values.description,
- task_name: selectedTaskName,
- model_name: selectedModel?.name || "",
- parameters: values,
- });
- setSessions((prev) => [...prev, createdSession]);
- navigate(`/app/generative/sessions/${createdSession.id}`);
- enqueueSnackbar(t("generative:message.sessionCreatedSuccess"), {
- variant: "success",
- });
- if (tourContext?.run && tourContext?.stepIndex === 5) {
- const waitForElement = () => {
- const el = document.querySelector(
- '[data-tour="sessions-left-panel"]',
- );
- if (el) setTimeout(() => tourContext.nextStep(), 100);
- else setTimeout(waitForElement, 100);
- };
- setTimeout(waitForElement, 100);
- }
- } catch (error) {
- console.error("Error creating session:", error);
- const errorDetail = error?.response?.data?.detail || "";
- if (
- error?.response?.status === 409 ||
- errorDetail.includes("already exists")
- ) {
- enqueueSnackbar(t("generative:error.sessionNameExists"), {
- variant: "error",
- });
- } else {
- enqueueSnackbar(t("generative:error.failedToCreateSession"), {
- variant: "error",
- });
- }
- }
- },
- });
-
- const handleNameInputChange = (event) => {
- formik.handleChange(event);
- const empty = event.target.value.trim() === "";
- setNameError(empty);
- setNameErrorMessage(empty ? t("generative:error.nameRequired") : "");
- };
-
- const handleModelSelect = (model) => {
- setSelectedModel(model);
- if (
- tourContext?.run &&
- tourContext?.stepIndex === 3 &&
- !hasAdvancedTourRef.current
- ) {
- hasAdvancedTourRef.current = true;
- const waitForElement = () => {
- const el = document.querySelector('[data-tour="model-parameters"]');
- if (el) {
- const distanceToViewportCenter = Math.abs(
- el.getBoundingClientRect().top - window.innerHeight / 2,
- );
- const scrollWaitMs = Math.min(
- 800,
- Math.max(300, Math.round(distanceToViewportCenter * 0.7)),
- );
-
- el.scrollIntoView({ behavior: "smooth", block: "center" });
- setTimeout(() => tourContext.nextStep(), scrollWaitMs);
- return;
- }
-
- setTimeout(waitForElement, 100);
- };
- setTimeout(waitForElement, 200);
- }
- };
-
- const processedProperties = selectedModel?.schema?.properties
- ? preprocessSchema(selectedModel.schema.properties)
- : {};
-
- return (
-
-
- {selectedDisplayName}:{" "}
- {t("generative:label.selectModelAndConfigureParameters")}
-
-
-
- {t("generative:label.selectModel")}
-
-
-
-
-
-
- {!selectedModel && (
-
- navigate("/app/generative")}
- sx={{ mr: 1 }}
- >
- {t("generative:button.backToTaskSelection")}
-
-
- {t("generative:button.createSession")}
-
-
- )}
-
- {selectedModel?.schema && (
- navigate("/app/generative")}
- />
- )}
-
- );
-}
diff --git a/DashAI/front/src/components/generative/SelectTaskMenu.jsx b/DashAI/front/src/components/generative/SelectTaskMenu.jsx
deleted file mode 100644
index bf28530fc..000000000
--- a/DashAI/front/src/components/generative/SelectTaskMenu.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { useNavigate } from "react-router-dom";
-import SelectOptionMenu from "../threeSectionLayout/SelectOptionMenu";
-import { useGenerative } from "./GenerativeContext";
-import { useTourContext } from "../tour/TourProvider";
-import {
- ChatBubbleOutline as TextToTextIcon,
- Image as TextToImageIcon,
- Tune as ControlNetIcon,
- AutoAwesome as DefaultGenerativeIcon,
-} from "@mui/icons-material";
-
-const GENERATIVE_TASK_ICONS = {
- TextToTextGenerationTask: TextToTextIcon,
- TextToImageGenerationTask: TextToImageIcon,
- ControlNetTask: ControlNetIcon,
-};
-
-export default function SelectTaskMenu() {
- const { t } = useTranslation(["generative", "common"]);
- const navigate = useNavigate();
- const { tasks } = useGenerative();
- const tourContext = useTourContext();
-
- const goToNextStep = (taskName) => {
- navigate(`/app/generative/sessions/new/${taskName}`);
-
- if (tourContext?.run && tourContext?.stepIndex === 2) {
- const waitForElement = () => {
- const element = document.querySelector('[data-tour="model-selection"]');
- if (element) {
- tourContext.nextStep();
- } else {
- setTimeout(waitForElement, 100);
- }
- };
- setTimeout(waitForElement, 100);
- }
- };
-
- return (
- ({
- name: task.name,
- display_name: task.display_name,
- description: task.description,
- Icon: GENERATIVE_TASK_ICONS[task.name] || DefaultGenerativeIcon,
- }))}
- searchBar={false}
- dataTour="task-selection"
- dataTourTarget="TextToTextGenerationTask"
- />
- );
-}
diff --git a/DashAI/front/src/components/generative/SessionBox.jsx b/DashAI/front/src/components/generative/SessionBox.jsx
index 3e5eb746c..596abaea2 100644
--- a/DashAI/front/src/components/generative/SessionBox.jsx
+++ b/DashAI/front/src/components/generative/SessionBox.jsx
@@ -42,18 +42,18 @@ export default function SessionBox({
>
{name ? name : t("generative:label.untitledSession")}
{modelName}
diff --git a/DashAI/front/src/components/generative/SessionForm.jsx b/DashAI/front/src/components/generative/SessionForm.jsx
deleted file mode 100644
index 0703833bd..000000000
--- a/DashAI/front/src/components/generative/SessionForm.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Box, Button, TextField, Typography } from "@mui/material";
-import { useTranslation } from "react-i18next";
-import FormSchemaRenderFields from "../shared/FormSchemaRenderFields";
-
-export default function SessionForm({
- formik,
- processedProperties,
- nameError,
- nameErrorMessage,
- onNameChange,
- onBack,
-}) {
- const { t } = useTranslation(["generative", "common"]);
-
- return (
-
- );
-}
diff --git a/DashAI/front/src/components/generative/mediaInput/MediaOnlyPlaceholder.jsx b/DashAI/front/src/components/generative/mediaInput/MediaOnlyPlaceholder.jsx
index 10eb59877..43ff663de 100644
--- a/DashAI/front/src/components/generative/mediaInput/MediaOnlyPlaceholder.jsx
+++ b/DashAI/front/src/components/generative/mediaInput/MediaOnlyPlaceholder.jsx
@@ -1,4 +1,4 @@
-import { Box, Stack } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import {
MEDIA_KINDS,
@@ -38,12 +38,12 @@ export function MediaOnlyPlaceholder({
>
{hasAnyMedia ? (
<>
-
+
{t(
"generative:label.attachMediaToContinue",
"Attach media to continue",
)}
-
+
{MEDIA_ORDER.map((kind) => {
const { icon, tooltipKey } = MEDIA_KINDS[kind];
@@ -65,12 +65,12 @@ export function MediaOnlyPlaceholder({
>
) : (
-
+
{t(
"generative:label.noInputAvailable",
"No input available for this task",
)}
-
+
)}
);
diff --git a/DashAI/front/src/components/generative/mediaInput/MediaPreviewList.jsx b/DashAI/front/src/components/generative/mediaInput/MediaPreviewList.jsx
index 93a63790a..0df66d7f5 100644
--- a/DashAI/front/src/components/generative/mediaInput/MediaPreviewList.jsx
+++ b/DashAI/front/src/components/generative/mediaInput/MediaPreviewList.jsx
@@ -1,4 +1,4 @@
-import { Box, IconButton } from "@mui/material";
+import { Box, IconButton, Typography } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
export function MediaPreviewList({ activeKinds, previewsByKind, onRemove }) {
@@ -26,7 +26,9 @@ export function MediaPreviewList({ activeKinds, previewsByKind, onRemove }) {
}}
/>
) : (
-
{kind} #{index + 1}
-
+
)}
-
+
{language || "code"}
diff --git a/DashAI/front/src/components/hardware/HardwareMonitorButton.jsx b/DashAI/front/src/components/hardware/HardwareMonitorButton.jsx
index 11eb0d3e3..b9d0bc592 100644
--- a/DashAI/front/src/components/hardware/HardwareMonitorButton.jsx
+++ b/DashAI/front/src/components/hardware/HardwareMonitorButton.jsx
@@ -31,8 +31,8 @@ export default function HardwareMonitorButton() {
onClick={handleClick}
aria-label={t("common:hardwareMonitor.title")}
sx={{
- width: 28,
- height: 28,
+ width: 32,
+ height: 32,
borderRadius: "4px",
border: `1px solid ${theme.palette.divider}`,
color: theme.palette.text.secondary,
@@ -42,7 +42,7 @@ export default function HardwareMonitorButton() {
},
}}
>
-
+
{
? theme.palette.grey[900]
: theme.palette.grey[100],
p: 2,
- fontSize: "0.875rem",
overflow: "auto",
maxHeight: "200px",
}}
>
- {displayJob.error_msg}
+
+ {displayJob.error_msg}
+
diff --git a/DashAI/front/src/components/models/AddModelDialog.jsx b/DashAI/front/src/components/models/AddModelDialog.jsx
index 965bbd261..751b1f3c3 100644
--- a/DashAI/front/src/components/models/AddModelDialog.jsx
+++ b/DashAI/front/src/components/models/AddModelDialog.jsx
@@ -294,7 +294,7 @@ function AddModelDialog({
{steps.map((label) => (
-
+
{label}
))}
diff --git a/DashAI/front/src/components/models/CreateSessionSteps.jsx b/DashAI/front/src/components/models/CreateSessionSteps.jsx
index 37b39f96f..c96e1d079 100644
--- a/DashAI/front/src/components/models/CreateSessionSteps.jsx
+++ b/DashAI/front/src/components/models/CreateSessionSteps.jsx
@@ -1,17 +1,17 @@
import { useState, useMemo, useEffect, useRef } from "react";
import PropTypes from "prop-types";
-import { useLocation, useNavigate } from "react-router-dom";
-import { Box, Stepper, Step, StepLabel } from "@mui/material";
+import { Box, Button, Typography } from "@mui/material";
import { useSnackbar } from "notistack";
import { useFormik } from "formik";
import { useTourContext } from "../tour/TourProvider";
import SetNameAndDatasetStep from "./SetNameAndDatasetStep";
import PrepareDatasetStep from "./modelSession/PrepareDatasetStep";
-import FormSchemaButtonGroup from "../shared/FormSchemaButtonGroup";
+import DatasetAutocomplete from "../notebooks/notebookCreation/DatasetAutocomplete";
import { createModelSession } from "../../api/modelSession";
import { getComponents } from "../../api/component";
import { generateSequentialName } from "../../utils/nameGenerator";
import { useTranslation } from "react-i18next";
+import { useModels } from "./ModelsContext";
function CreateSessionSteps({
backHome,
@@ -21,18 +21,11 @@ function CreateSessionSteps({
existingSessions = [],
preselectedDatasetId = null,
}) {
- const navigate = useNavigate();
- const location = useLocation();
- const taskName = selectedTask?.name;
- const baseSessionPath = taskName
- ? `/app/models/sessions/new/${taskName}`
- : "/app/models";
- const preparePath = `${baseSessionPath}/prepare`;
- const activeStep = location.pathname.startsWith(preparePath) ? 1 : 0;
const { enqueueSnackbar } = useSnackbar();
const { t } = useTranslation(["models", "common"]);
const tourContext = useTourContext();
const hasAdvancedTourRef = useRef(false);
+ const { setSessionRightContent } = useModels();
const [selectedDataset, setSelectedDataset] = useState(
preselectedDatasetId
@@ -40,8 +33,24 @@ function CreateSessionSteps({
: null,
);
+ const [newExp, setNewExp] = useState({
+ name: "",
+ dataset: null,
+ task_name: selectedTask?.name || "",
+ input_columns: [],
+ output_columns: [],
+ train_metrics: [],
+ validation_metrics: [],
+ test_metrics: [],
+ splits: {},
+ runs: [],
+ });
+
+ const [nextEnabled, setNextEnabled] = useState(false);
+
const handleDatasetChange = (newDataset) => {
setSelectedDataset(newDataset);
+ setNewExp((prev) => ({ ...prev, dataset: newDataset }));
if (
tourContext?.run &&
tourContext?.stepIndex === 5 &&
@@ -51,12 +60,10 @@ function CreateSessionSteps({
hasAdvancedTourRef.current = true;
const waitForElement = () => {
const element = document.querySelector(
- '[data-tour="models-next-button"]',
+ '[data-tour="models-validation-alert"]',
);
if (element) {
- setTimeout(() => {
- tourContext.nextStep();
- }, 100);
+ tourContext.nextStep();
} else {
setTimeout(waitForElement, 100);
}
@@ -65,26 +72,6 @@ function CreateSessionSteps({
}
};
- const [newExp, setNewExp] = useState({
- name: "",
- dataset: null,
- task_name: selectedTask?.name || "",
- input_columns: [],
- output_columns: [],
- train_metrics: [],
- validation_metrics: [],
- test_metrics: [],
- splits: {},
- runs: [],
- });
-
- const [nextEnabled, setNextEnabled] = useState(false);
-
- const steps = [
- t("models:label.selectDataset"),
- t("models:label.prepareDataset"),
- ];
-
const { defaultName } = useMemo(() => {
if (!selectedTask) {
return { defaultName: "" };
@@ -110,34 +97,7 @@ function CreateSessionSteps({
},
enableReinitialize: true,
onSubmit: async (values) => {
- if (activeStep === 0) {
- setNewExp((prev) => ({
- ...prev,
- name: values.name.trim(),
- dataset: selectedDataset,
- task_name: selectedTask?.name || "",
- }));
- navigate(preparePath);
- setNextEnabled(false);
-
- if (tourContext?.run && tourContext?.stepIndex === 6) {
- const waitForElement = () => {
- const element = document.querySelector(
- '[data-tour="models-validation-alert"]',
- );
- if (element) {
- setTimeout(() => {
- tourContext.nextStep();
- }, 100);
- } else {
- setTimeout(waitForElement, 100);
- }
- };
- setTimeout(waitForElement, 300);
- }
- } else if (activeStep === 1) {
- await createSession();
- }
+ await createSession(values.name.trim());
},
});
@@ -147,23 +107,30 @@ function CreateSessionSteps({
}
}, [selectedTask, defaultName, formik]);
- const isNextEnabled = (() => {
- if (activeStep === 0) {
- const isNameValid = formik.values.name.trim().length >= 4;
- const isDatasetValid = selectedDataset !== null;
- return isNameValid && isDatasetValid;
- }
- return nextEnabled;
- })();
+ useEffect(() => {
+ if (selectedDataset) return;
+ setSessionRightContent(
+
+
+ {t("models:label.selectDatasetFirst")}
+
+ ,
+ );
+ return () => setSessionRightContent(null);
+ }, [selectedDataset]);
const getNameError = () => {
- if (!selectedDataset) {
- return null;
- }
-
const currentName = formik.values.name.trim();
- if (!currentName) {
- return t("models:error.nameRequired");
+ if (!currentName || currentName.length < 4) {
+ return null;
}
const nameExists = existingSessions.some(
@@ -180,15 +147,13 @@ function CreateSessionSteps({
const nameError = getNameError();
- const handleBack = () => {
- if (activeStep === 0) {
- backHome();
- } else {
- navigate(baseSessionPath);
- }
- };
+ const isNextEnabled =
+ formik.values.name.trim().length >= 4 &&
+ !nameError &&
+ selectedDataset !== null &&
+ nextEnabled;
- const createSession = async () => {
+ const createSession = async (sessionName) => {
try {
setNextEnabled(false);
@@ -211,19 +176,15 @@ function CreateSessionSteps({
const hasTest =
newExp.splits.test !== undefined && newExp.splits.test !== 0;
- const trainMetrics = hasTrain ? allMetricNames : [];
- const validationMetrics = hasValidation ? allMetricNames : [];
- const testMetrics = hasTest ? allMetricNames : [];
-
const response = await createModelSession(
- newExp.dataset.id,
- newExp.task_name,
- newExp.name,
+ selectedDataset.id,
+ selectedTask?.name || newExp.task_name,
+ sessionName,
newExp.input_columns,
newExp.output_columns,
- trainMetrics,
- validationMetrics,
- testMetrics,
+ hasTrain ? allMetricNames : [],
+ hasValidation ? allMetricNames : [],
+ hasTest ? allMetricNames : [],
JSON.stringify(newExp.splits),
);
@@ -231,6 +192,8 @@ function CreateSessionSteps({
variant: "success",
});
+ formik.resetForm();
+
if (tourContext?.run) {
tourContext.stopTour();
sessionStorage.setItem("startModelsSessionTour", "true");
@@ -248,66 +211,78 @@ function CreateSessionSteps({
};
return (
- <>
-
-
- {activeStep === 0 && (
-
- )}
- {activeStep === 1 && (
-
- )}
-
+
+
+
+ {t("models:label.prepareDataset")}
+
+
+ {t("models:label.selectDatasetAndPrepare")}
+
+
-
-
+
+
+ {selectedDataset && (
+
-
+ )}
- >
+ >
+
+ {t("common:back")}
+
+
+ {t("models:button.createSession")}
+
+
+
);
}
+
CreateSessionSteps.propTypes = {
backHome: PropTypes.func.isRequired,
selectedTask: PropTypes.object.isRequired,
diff --git a/DashAI/front/src/components/models/ModelCenterContent.jsx b/DashAI/front/src/components/models/ModelCenterContent.jsx
index a2349718c..c3cfed07b 100644
--- a/DashAI/front/src/components/models/ModelCenterContent.jsx
+++ b/DashAI/front/src/components/models/ModelCenterContent.jsx
@@ -93,7 +93,7 @@ export default function ModelsCenterContent() {
flexDirection: "column",
width: "100%",
height: "100%",
- overflow: "auto",
+ overflow: "hidden",
px: 2,
pt: 2,
}}
diff --git a/DashAI/front/src/components/models/ModelComparisonTable.jsx b/DashAI/front/src/components/models/ModelComparisonTable.jsx
index 4a0ae5072..92349e750 100644
--- a/DashAI/front/src/components/models/ModelComparisonTable.jsx
+++ b/DashAI/front/src/components/models/ModelComparisonTable.jsx
@@ -315,17 +315,21 @@ function ModelComparisonTable({
const isBest = bestScore !== null && Math.abs(score - bestScore) < 1e-6;
const tooltipContent = (
-
-
+
+
{t("models:label.score")}: {score.toFixed(1)}/100
-
+
{breakdown.map(({ metric_name, value, normalized_weight }, i) => (
-
+
{i === 0 ? "=" : "+"} {metric_name} ({value.toFixed(4)}) ×{" "}
{(normalized_weight * 100).toFixed(0)}%
-
+
))}
-
+
);
return (
diff --git a/DashAI/front/src/components/models/ModelsBreadcrumbs.jsx b/DashAI/front/src/components/models/ModelsBreadcrumbs.jsx
index b8243d526..f0aaee6b7 100644
--- a/DashAI/front/src/components/models/ModelsBreadcrumbs.jsx
+++ b/DashAI/front/src/components/models/ModelsBreadcrumbs.jsx
@@ -37,22 +37,6 @@ export default function ModelsBreadcrumbs() {
if (path.startsWith("/app/models/sessions/new/") && params.taskName) {
const task = tasks.find((tk) => tk.name === params.taskName);
const taskLabel = taskDisplayName(task) ?? params.taskName;
-
- if (path.endsWith("/prepare")) {
- return [
- rootCrumb,
- {
- label: taskLabel,
- path: `/app/models/sessions/new/${params.taskName}`,
- },
- {
- label: t("models:label.prepareDataset"),
- path: null,
- current: true,
- },
- ];
- }
-
return [
rootCrumb,
{
diff --git a/DashAI/front/src/components/models/ModelsContext.jsx b/DashAI/front/src/components/models/ModelsContext.jsx
index a94f1c50f..198c160a8 100644
--- a/DashAI/front/src/components/models/ModelsContext.jsx
+++ b/DashAI/front/src/components/models/ModelsContext.jsx
@@ -76,6 +76,7 @@ export function ModelsProvider({ children }) {
const [selectedOption, setSelectedOption] = useState(OptionsEnum.NEW);
const [datasetInfo, setDatasetInfo] = useState(null);
const [datasetTab, setDatasetTab] = useState(0);
+ const [sessionRightContent, setSessionRightContent] = useState(null);
const selectModel = useCallback((model) => {
setSelectedModel(model);
@@ -152,6 +153,8 @@ export function ModelsProvider({ children }) {
setDatasetInfo,
datasetTab,
setDatasetTab,
+ sessionRightContent,
+ setSessionRightContent,
};
return (
diff --git a/DashAI/front/src/components/models/ModelsRightBar.jsx b/DashAI/front/src/components/models/ModelsRightBar.jsx
index 12e138a3a..df89936dd 100644
--- a/DashAI/front/src/components/models/ModelsRightBar.jsx
+++ b/DashAI/front/src/components/models/ModelsRightBar.jsx
@@ -32,6 +32,7 @@ export default function ModelsRightBar({ onToggle }) {
closeConfig,
datasetInfo,
setDatasetTab,
+ sessionRightContent,
} = useModels();
const fetchModels = React.useCallback(async () => {
@@ -102,6 +103,30 @@ export default function ModelsRightBar({ onToggle }) {
}
};
+ if (sessionRightContent) {
+ return (
+
+
+
+ {t("models:label.configureSession")}
+
+
+
+ {sessionRightContent}
+
+
+ );
+ }
+
return (
{
- setSelectedDataset(newDataset);
- if (onDatasetChange) {
- onDatasetChange(newDataset);
- }
- };
return (
-
-
-
- {t("models:label.selectDatasetForSession")}
-
-
-
-
-
-
- {t("models:label.nameYourSession")}
-
-
-
-
+
+
+
);
}
SetNameAndDatasetStep.propTypes = {
formik: PropTypes.object.isRequired,
- selectedDataset: PropTypes.object,
- setSelectedDataset: PropTypes.func.isRequired,
- datasets: PropTypes.array.isRequired,
nameError: PropTypes.string,
- selectedTask: PropTypes.object,
- onDatasetChange: PropTypes.func,
};
export default SetNameAndDatasetStep;
diff --git a/DashAI/front/src/components/models/modelSession/PrepareDatasetStep.jsx b/DashAI/front/src/components/models/modelSession/PrepareDatasetStep.jsx
index 035afce27..aba9c81c7 100644
--- a/DashAI/front/src/components/models/modelSession/PrepareDatasetStep.jsx
+++ b/DashAI/front/src/components/models/modelSession/PrepareDatasetStep.jsx
@@ -21,6 +21,7 @@ import { useSnackbar } from "notistack";
import { getColorByColumnType } from "../../../utils";
import { useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
+import { useModels } from "../ModelsContext";
/**
* Step of the experiment modal: Set the input and output columns to use for clasification
* and the splits for training, validation and testing
@@ -28,7 +29,8 @@ import { Trans } from "react-i18next";
* @param {function} setNewExp updates the Eperimento Modal state (newExp)
* @param {function} setNextEnabled function to enable or disable the "Next" button in the modal
*/
-function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled }) {
+function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled, dataset }) {
+ const { setSessionRightContent } = useModels();
const [datasetInfo, setDatasetInfo] = useState({});
const [datasetTypes, setDatasetTypes] = useState({});
const { enqueueSnackbar } = useSnackbar();
@@ -87,11 +89,14 @@ function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled }) {
const [splitsReady, setSplitsReady] = useState(false);
const getDatasetInfo = async () => {
+ if (!dataset?.id) return;
setInfoLoading(true);
+ setInputColumnNames([]);
+ setOutputColumnNames([]);
try {
const [fetchedDatasetInfo, fetchedDatasetTypes] = await Promise.all([
- getDatasetInfoRequest(newExp.dataset.id),
- getDatasetTypesRequest(newExp.dataset.id),
+ getDatasetInfoRequest(dataset.id),
+ getDatasetTypesRequest(dataset.id),
]);
setDatasetInfo(fetchedDatasetInfo);
setDatasetTypes(fetchedDatasetTypes);
@@ -201,7 +206,7 @@ function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled }) {
try {
const validation = await validateColumnsRequest(
newExp.task_name,
- newExp.dataset.id,
+ dataset.id,
inputColumnNames,
outputColumnNames,
);
@@ -305,9 +310,60 @@ function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled }) {
useEffect(() => {
getDatasetInfo();
+ }, [dataset?.id]);
+
+ useEffect(() => {
getTaskRequirements();
}, []);
+ // Push SplitDatasetRows (or loading spinner) into the right bar
+ useEffect(() => {
+ if (infoLoading) {
+ setSessionRightContent(
+
+
+ ,
+ );
+ return () => setSessionRightContent(null);
+ }
+ setSessionRightContent(
+ ,
+ );
+ return () => setSessionRightContent(null);
+ }, [
+ infoLoading,
+ datasetInfo,
+ rowsPartitionsIndex,
+ rowsPartitionsPercentage,
+ splitType,
+ shuffle,
+ stratify,
+ seed,
+ ]);
+
const renderTypesAsChips = (typesList) => {
if (!typesList || typesList.length === 0) {
return {t("common:any")} ;
@@ -463,24 +519,6 @@ function PrepareDatasetStep({ newExp, setNewExp, setNextEnabled }) {
infoLoading || (datasetInfo.column_names || []).length === 0
}
/>
-
-
) : (
@@ -507,5 +545,6 @@ PrepareDatasetStep.propTypes = {
}),
setNewExp: PropTypes.func.isRequired,
setNextEnabled: PropTypes.func.isRequired,
+ dataset: PropTypes.object.isRequired,
};
export default PrepareDatasetStep;
diff --git a/DashAI/front/src/components/models/modelSession/SplitDatasetRows.jsx b/DashAI/front/src/components/models/modelSession/SplitDatasetRows.jsx
index 821bcfaa0..c02e73e5d 100644
--- a/DashAI/front/src/components/models/modelSession/SplitDatasetRows.jsx
+++ b/DashAI/front/src/components/models/modelSession/SplitDatasetRows.jsx
@@ -2,17 +2,62 @@ import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { parseRangeToIndex } from "../../../utils/parseRange";
import {
+ Box,
+ FormHelperText,
Grid,
+ Paper,
+ Stack,
TextField,
+ ToggleButton,
+ ToggleButtonGroup,
Typography,
- FormControlLabel,
- Radio,
- RadioGroup,
- FormHelperText,
} from "@mui/material";
import BooleanInput from "../../configurableObject/Inputs/BooleanInput";
+import FormSchemaFieldCard from "../../shared/FormSchemaFieldCard";
import { useTranslation } from "react-i18next";
+/**
+ * Splits card shell — same Paper/header visual as FormSchemaFieldCard but WITHOUT
+ * the label-hiding CSS so Train / Validation / Test TextField labels stay visible.
+ */
+function SplitsCard({ label, description, errorMessage, children }) {
+ return (
+
+
+
+ {label}
+
+
+
+ {children}
+
+ {(description || errorMessage) && (
+
+
+ {errorMessage ?? description}
+
+
+ )}
+
+ );
+}
+
function SplitDatasetRows({
datasetInfo,
rowsPartitionsIndex,
@@ -30,7 +75,7 @@ function SplitDatasetRows({
seed,
setSeed,
}) {
- const { t } = useTranslation(["experiments"]);
+ const { t } = useTranslation(["experiments", "common"]);
const totalRows = datasetInfo.total_rows;
const trainDatasetPercentage = (datasetInfo.train_size / totalRows).toFixed(
@@ -46,28 +91,16 @@ function SplitDatasetRows({
validationDatasetPercentage > 0 ||
testDatasetPercentage > 0;
- const checkSplit = (train, validation, test) => {
- const sum = train + validation + test;
- const tolerance = 0.0001; // Allow small floating point errors
- return Math.abs(sum - 1) < tolerance;
- };
+ const checkSplit = (train, validation, test) =>
+ Math.abs(train + validation + test - 1) < 0.0001;
- // handle rows numbers change state
- const disabledTextFieldStyle = {
- "& .MuiInputBase-input.Mui-disabled": {
- WebkitTextFillColor: "#999",
- },
- "& .MuiInputLabel-root.Mui-disabled": {
- color: "#bbb",
- },
- };
const [randomSplitError, setRandomSplitError] = useState(false);
const [randomSplitErrorText, setRandomSplitErrorText] = useState("");
const [manualSplitError, setManualSplitError] = useState(false);
const [manualSplitErrorText, setManualSplitErrorText] = useState("");
- const handleSplitTypeChange = (event) => {
- const newType = event.target.value;
+ const handleSplitTypeChange = (_e, newType) => {
+ if (!newType) return;
setSplitType(newType);
if (newType === SPLIT_TYPES.PREDEFINED) {
@@ -76,15 +109,12 @@ function SplitDatasetRows({
if (newType === SPLIT_TYPES.RANDOM) {
const newSplit = { train: 0.6, test: 0.2, validation: 0.2 };
setRowsPartitionsPercentage(newSplit);
-
- // Validate the random split
const hasZero = newSplit.train === 0;
const sumsToOne = checkSplit(
newSplit.train,
newSplit.validation,
newSplit.test,
);
-
if (hasZero) {
setRandomSplitErrorText(
t("experiments:error.trainSplitMustBeGreaterThanZero"),
@@ -100,16 +130,10 @@ function SplitDatasetRows({
if (newType === SPLIT_TYPES.MANUAL) {
const newIndex = { train: [], test: [], validation: [] };
setRowsPartitionsIndex(newIndex);
-
- // Validate the manual split
- if (newIndex.train.length === 0) {
- setManualSplitErrorText(
- t("experiments:error.trainSplitMustHaveAtLeastOneRow"),
- );
- setManualSplitError(true);
- } else {
- setManualSplitError(false);
- }
+ setManualSplitErrorText(
+ t("experiments:error.trainSplitMustHaveAtLeastOneRow"),
+ );
+ setManualSplitError(true);
}
};
@@ -120,23 +144,8 @@ function SplitDatasetRows({
if (splitType === SPLIT_TYPES.MANUAL) {
try {
const rowsIndex = parseRangeToIndex(value, totalRows);
- let updatedIndex = { ...rowsPartitionsIndex };
-
- switch (id) {
- case "train":
- updatedIndex.train = rowsIndex;
- break;
- case "validation":
- updatedIndex.validation = rowsIndex;
- break;
- case "test":
- updatedIndex.test = rowsIndex;
- break;
- }
-
+ const updatedIndex = { ...rowsPartitionsIndex, [id]: rowsIndex };
setRowsPartitionsIndex(updatedIndex);
-
- // Validate after update
if (updatedIndex.train.length === 0) {
setManualSplitErrorText(
t("experiments:error.trainSplitMustHaveAtLeastOneRow"),
@@ -150,31 +159,15 @@ function SplitDatasetRows({
setManualSplitError(true);
}
} else {
- let newSplit = { ...rowsPartitionsPercentage };
const numValue = parseFloat(value) || 0;
-
- switch (id) {
- case "train":
- newSplit = { ...newSplit, train: numValue };
- break;
- case "validation":
- newSplit = { ...newSplit, validation: numValue };
- break;
- case "test":
- newSplit = { ...newSplit, test: numValue };
- break;
- }
-
+ const newSplit = { ...rowsPartitionsPercentage, [id]: numValue };
setRowsPartitionsPercentage(newSplit);
-
- // Check if any value is 0 or if sum is not 1
const hasZero = newSplit.train === 0;
const sumsToOne = checkSplit(
newSplit.train,
newSplit.validation,
newSplit.test,
);
-
if (hasZero) {
setRandomSplitErrorText(
t("experiments:error.trainSplitMustBeGreaterThanZero"),
@@ -191,17 +184,12 @@ function SplitDatasetRows({
const handleShuffleChange = (value) => {
setShuffle(value);
- if (!value) {
- setStratify(false);
- }
+ if (!value) setStratify(false);
};
const handleStratifyChange = (value) => {
- if (shuffle) {
- setStratify(value);
- } else {
- setStratify(false);
- }
+ if (shuffle) setStratify(value);
+ else setStratify(false);
};
const handleSeedChange = (event) => {
@@ -218,7 +206,6 @@ function SplitDatasetRows({
}, [hasPredefinedSplits]);
useEffect(() => {
- // check if splits doesnt have errors and arent empty
if (splitType === SPLIT_TYPES.PREDEFINED) {
setSplitsReady(true);
} else if (
@@ -244,229 +231,208 @@ function SplitDatasetRows({
splitType,
]);
+ const splitOptions = [
+ {
+ value: SPLIT_TYPES.PREDEFINED,
+ label: t("experiments:label.predefined"),
+ disabled: !hasPredefinedSplits,
+ },
+ { value: SPLIT_TYPES.RANDOM, label: t("experiments:label.random") },
+ { value: SPLIT_TYPES.MANUAL, label: t("experiments:label.manual") },
+ ];
+
+ const splitFields = [
+ { id: "train", label: t("common:train") },
+ { id: "validation", label: t("common:validation") },
+ { id: "test", label: t("common:test") },
+ ];
+
return (
-
-
-
-
+
+ {/* Split type selector */}
+
+
+
+ {t("experiments:label.splitType")}
+
+
+
+
+ {splitOptions.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
{t("experiments:label.selectHowToDivideDataset")}
-
-
-
- }
- label={
- hasPredefinedSplits
- ? t("experiments:label.usePredefinedSplitsFromDataset")
- : t(
- "experiments:label.usePredefinedSplitsFromDatasetNotAvailable",
- )
- }
- sx={{ my: 1 }}
- disabled={!hasPredefinedSplits}
- />
- {splitType === SPLIT_TYPES.PREDEFINED && (
-
-
-
-
-
-
-
-
-
-
-
- )}
- }
- label={t("experiments:label.useRandomRowsBySpecifyingPortion")}
- sx={{ my: 1 }}
- />
- {splitType === SPLIT_TYPES.RANDOM && (
- <>
-
-
-
-
-
-
-
-
+
+
+
+ {/* Predefined */}
+ {splitType === SPLIT_TYPES.PREDEFINED && (
+
+
+ {[
+ { id: "train", value: trainDatasetPercentage },
+ { id: "validation", value: validationDatasetPercentage },
+ { id: "test", value: testDatasetPercentage },
+ ].map(({ id, value }) => (
+
- {randomSplitError && (
-
- {randomSplitErrorText}
+ ))}
+
+
+ )}
+
+ {/* Random */}
+ {splitType === SPLIT_TYPES.RANDOM && (
+ <>
+
+
+ {splitFields.map(({ id, label }) => (
+
+
- )}
-
-
-
-
-
+ ))}
- >
- )}
- }
- label={t(
- "experiments:label.useManualSplittingBySpecifyingRowIndexes",
- )}
- sx={{ my: 1 }}
- />
- {splitType === SPLIT_TYPES.MANUAL && (
- <>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {/* Manual */}
+ {splitType === SPLIT_TYPES.MANUAL && (
+
+
+ {splitFields.map(({ id, label }) => (
+
- {manualSplitError && (
-
- {manualSplitErrorText}
-
- )}
-
- >
- )}
-
-
+ ))}
+
+
+ )}
+
);
}
SplitDatasetRows.propTypes = {
- datasetInfo: PropTypes.shape({
- test_size: PropTypes.number,
- total_columns: PropTypes.number,
- total_rows: PropTypes.number,
- train_size: PropTypes.number,
- val_size: PropTypes.number,
- }),
- rowsPartitionsIndex: PropTypes.shape({
- train: PropTypes.arrayOf(PropTypes.number),
- validation: PropTypes.arrayOf(PropTypes.number),
- test: PropTypes.arrayOf(PropTypes.number),
- }),
+ datasetInfo: PropTypes.object.isRequired,
+ rowsPartitionsIndex: PropTypes.object.isRequired,
setRowsPartitionsIndex: PropTypes.func.isRequired,
- rowsPartitionsPercentage: PropTypes.shape({
- train: PropTypes.number,
- validation: PropTypes.number,
- test: PropTypes.number,
- }),
+ rowsPartitionsPercentage: PropTypes.object.isRequired,
setRowsPartitionsPercentage: PropTypes.func.isRequired,
setSplitsReady: PropTypes.func.isRequired,
splitType: PropTypes.string.isRequired,
diff --git a/DashAI/front/src/components/notebooks/ColumnSelector.jsx b/DashAI/front/src/components/notebooks/ColumnSelector.jsx
index 6df58337c..6ff78a76a 100644
--- a/DashAI/front/src/components/notebooks/ColumnSelector.jsx
+++ b/DashAI/front/src/components/notebooks/ColumnSelector.jsx
@@ -194,6 +194,15 @@ function ColumnSelector({
[getValidColumnIds, rowSelectionModel, inputCardinality],
);
+ const handleSelectAllRows = useCallback(() => {
+ const validIds = getValidColumnIds();
+ const allValidSelected =
+ validIds.length > 0 &&
+ validIds.every((id) => rowSelectionModel.includes(id));
+
+ handleSelection(allValidSelected ? {} : toMRT(validIds));
+ }, [getValidColumnIds, rowSelectionModel]);
+
// Effect to update selection data and validation whenever rowSelectionModel changes
useEffect(() => {
if (rows.length > 0) {
@@ -270,9 +279,9 @@ function ColumnSelector({
enableFullScreenToggle: false,
enableHiding: false,
enablePagination: true,
- muiPaginationProps: { rowsPerPageOptions: [5, 10, 20] },
+ muiPaginationProps: { rowsPerPageOptions: [10, 15, 20] },
initialState: {
- pagination: { pageSize: 5, pageIndex: 0 },
+ pagination: { pageSize: 10, pageIndex: 0 },
density: "compact",
},
mrtTheme: {
@@ -292,6 +301,15 @@ function ColumnSelector({
}
: {},
}),
+ muiSelectAllCheckboxProps: () => ({
+ checked:
+ getValidColumnIds().length > 0 &&
+ getValidColumnIds().every((id) => rowSelectionModel.includes(id)),
+ indeterminate:
+ rowSelectionModel.length > 0 &&
+ rowSelectionModel.length < getValidColumnIds().length,
+ onChange: handleSelectAllRows,
+ }),
localization,
});
diff --git a/DashAI/front/src/components/notebooks/DataBreadcrumbs.jsx b/DashAI/front/src/components/notebooks/DataBreadcrumbs.jsx
index 9fbc441bb..7e19195dc 100644
--- a/DashAI/front/src/components/notebooks/DataBreadcrumbs.jsx
+++ b/DashAI/front/src/components/notebooks/DataBreadcrumbs.jsx
@@ -13,14 +13,17 @@ export default function DataBreadcrumbs() {
const location = useLocation();
const params = useParams();
const { t } = useTranslation(["datasets", "common"]);
- const { datasets, notebooks } = useDatasetsAndNotebooks();
+ const { datasets, notebooks, uploadDataloader } = useDatasetsAndNotebooks();
const rootCrumb = { label: t("common:datasets"), path: "/app/data" };
const getBreadcrumbs = () => {
const path = location.pathname;
- if (path.startsWith("/app/data/datasets/new/configure")) {
+ if (path.startsWith("/app/data/datasets/new/") && params.dataloaderName) {
+ const dataloaderLabel = uploadDataloader
+ ? uploadDataloader.display_name || uploadDataloader.name
+ : params.dataloaderName;
return [
rootCrumb,
{
@@ -28,7 +31,7 @@ export default function DataBreadcrumbs() {
path: "/app/data/datasets/new",
},
{
- label: t("datasets:label.configureAndUpload"),
+ label: dataloaderLabel,
path: null,
current: true,
},
diff --git a/DashAI/front/src/components/notebooks/NoteBox.jsx b/DashAI/front/src/components/notebooks/NoteBox.jsx
index 596cced8f..b94f1d58e 100644
--- a/DashAI/front/src/components/notebooks/NoteBox.jsx
+++ b/DashAI/front/src/components/notebooks/NoteBox.jsx
@@ -10,12 +10,10 @@ export default function NoteBox({ message, className = "", ...props }) {
className={className}
{...props}
sx={{
- mt: 2,
p: 2,
bgcolor: theme.palette.background.box,
borderRadius: 1,
border: `1px solid ${theme.palette.ui.divider}`,
- mb: 2,
}}
>
{
cancelled = true;
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [explorersAndConverters]);
+ }, [explorersAndConverters, t]);
// Clear search when the selected notebook changes
useEffect(() => {
@@ -287,7 +286,6 @@ export default function RightBar({ notebook, onToggle }) {
notebook,
};
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
[explorers, datasetColumns, notebook?.id],
);
@@ -303,7 +301,6 @@ export default function RightBar({ notebook, onToggle }) {
notebook,
};
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
[converters, datasetColumns, notebook?.id],
);
diff --git a/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx b/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx
index 34db7a56c..0fb739890 100644
--- a/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx
+++ b/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx
@@ -60,7 +60,7 @@ export default function ParameterStepConverter({
variant="h6"
sx={{ fontWeight: 700, color: "primary.main", mb: 1 }}
>
- {t("datasets:label.configureParametersStep", { step: 2 })}
+ {t("datasets:label.configureParameters")}
-
- {t("datasets:label.selectScopeStep", { step: 1 })}
-
{error && (
-
+
{error}
)}
-
+
{isLoading ? t("common:loading") : columnType || t("common:unknown")}
@@ -196,11 +188,10 @@ export default function EditableColumnHeader({
>
-
+
{columnType || t("common:unknown")}
@@ -239,11 +226,7 @@ export default function EditableColumnHeader({
{encoderError && (
-
+
{encoderError}
)}
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
index 77fb19edd..f6594ac7c 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
import { Box, Typography, CardContent } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import TitleIcon from "@mui/icons-material/Title";
@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
export const CategoricalTab = ({ categoricalStats }) => {
const { t } = useTranslation(["datasets", "common"]);
const theme = useTheme();
+ const [activeIndices, setActiveIndices] = useState({});
return (
@@ -85,6 +86,7 @@ export const CategoricalTab = ({ categoricalStats }) => {
/>
{
/>
+ activeBar={false}
+ onMouseEnter={(_, index) =>
+ setActiveIndices((prev) => ({
+ ...prev,
+ [column]: index,
+ }))
+ }
+ onMouseLeave={() =>
+ setActiveIndices((prev) => ({
+ ...prev,
+ [column]: null,
+ }))
+ }
+ >
+ {stats.top_5.map((_, index) => {
+ const activeIndex = activeIndices[column] ?? null;
+ return (
+ |
+ );
+ })}
+
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
index c0cccbf3f..db96c69b2 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
@@ -83,11 +83,11 @@ export const NumericTab = ({ numericStats }) => {
{/* Distribution Metrics */}
{t("datasets:label.distributionMetrics")}
@@ -126,11 +126,11 @@ export const NumericTab = ({ numericStats }) => {
{/* Shape Indicators */}
{t("datasets:label.shapeIndicators")}
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
index dccf4ce2c..673075203 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
import { Box, Typography, Card, CardContent, Chip } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import {
@@ -9,6 +9,7 @@ import {
YAxis,
Tooltip,
Bar,
+ Cell,
} from "recharts";
import DatasetTable from "../DatasetTable";
import ExportableCard from "../ExportableCard";
@@ -27,6 +28,7 @@ const OverviewTab = ({
const { t } = useTranslation(["datasets", "common"]);
const theme = useTheme();
+ const [activeBarIndex, setActiveBarIndex] = useState(null);
const missingData = Object.entries(nan).map(([col, count]) => ({
column: col,
missing: count,
@@ -134,6 +136,7 @@ const OverviewTab = ({
/>
+ activeBar={false}
+ onMouseEnter={(_, index) => setActiveBarIndex(index)}
+ onMouseLeave={() => setActiveBarIndex(null)}
+ >
+ {missingData.map((_, index) => (
+ |
+ ))}
+
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
index eb0d73015..742ba6215 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
import {
Box,
Typography,
@@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next";
export const TextTab = ({ textStats }) => {
const theme = useTheme();
const { t } = useTranslation(["datasets", "common"]);
+ const [activeIndices, setActiveIndices] = useState({});
return (
{Object.entries(textStats).map(([column, stats]) => {
@@ -139,11 +140,11 @@ export const TextTab = ({ textStats }) => {
{t("datasets:label.lengthMetrics")}
@@ -196,6 +197,7 @@ export const TextTab = ({ textStats }) => {
{
/>
+ activeBar={false}
+ onMouseEnter={(_, index) =>
+ setActiveIndices((prev) => ({
+ ...prev,
+ [column]: index,
+ }))
+ }
+ onMouseLeave={() =>
+ setActiveIndices((prev) => ({
+ ...prev,
+ [column]: null,
+ }))
+ }
+ >
+ {lengthData.map((entry, index) => {
+ const activeIndex = activeIndices[column] ?? null;
+ return (
+ |
+ );
+ })}
+
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
index f89ea00ce..72e3bee39 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/ConfigureAndUploadDatasetStep.jsx
@@ -1,12 +1,11 @@
-import { useState, useEffect, useCallback, useRef } from "react";
-import { Grid, CircularProgress } from "@mui/material";
-import FormSchemaButtonGroup from "../../shared/FormSchemaButtonGroup";
+import { useState, useEffect, useCallback, useMemo, useRef } from "react";
+import { Box, Button, Grid, TextField } from "@mui/material";
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 { generateSequentialName } from "../../../utils/nameGenerator";
import { createDataset } from "../../../api/datasets";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
@@ -20,7 +19,14 @@ export default function ConfigureAndUploadDatasetStep({
formValues,
onPreviewError,
formHasErrors,
+ existingDatasets = [],
}) {
+ const { defaultName } = useMemo(
+ () => generateSequentialName({ base: "Dataset", items: existingDatasets }),
+ [existingDatasets],
+ );
+ const [datasetName, setDatasetName] = useState("");
+ const lastAutoFilledRef = useRef(null);
const [uploadEnabled, setUploadEnabled] = useState(false);
const [uploading, setUploading] = useState(false);
const [previewError, setPreviewError] = useState(false);
@@ -34,6 +40,14 @@ export default function ConfigureAndUploadDatasetStep({
const { t } = useTranslation(["common", "datasets"]);
const theme = useTheme();
+ useEffect(() => {
+ if (!defaultName) return;
+ if (!datasetName || datasetName === lastAutoFilledRef.current) {
+ setDatasetName(defaultName);
+ lastAutoFilledRef.current = defaultName;
+ }
+ }, [defaultName]);
+
useEffect(() => {
if (onPreviewError) {
onPreviewError(previewError);
@@ -67,7 +81,7 @@ export default function ConfigureAndUploadDatasetStep({
// Merge values coming from the form schema and the onValuesChange callback
const params = { ...refValues, ...(formValues || {}) };
- const name = params.name || datasetFileToUpload.file.name;
+ const name = datasetName.trim() || datasetFileToUpload.file.name;
params["name"] = name;
// Ensure dataloader is passed as a string (backend expects the dataloader name)
@@ -176,7 +190,24 @@ export default function ConfigureAndUploadDatasetStep({
]);
return (
-
+
+
+ setDatasetName(e.target.value)}
+ fullWidth
+ />
+
+
- {/* Form buttons */}
-
- {uploading ? (
-
- ) : (
-
- )}
-
-
+
+
+ {t("common:back")}
+
+
+ {t("common:upload")}
+
+
+
);
}
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/DataloaderConfigBar.jsx b/DashAI/front/src/components/notebooks/datasetCreation/DataloaderConfigBar.jsx
index ff970efb9..9e1339339 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/DataloaderConfigBar.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/DataloaderConfigBar.jsx
@@ -1,12 +1,15 @@
-import { Box, Typography } from "@mui/material";
+import { Box, TextField, Typography } from "@mui/material";
import PropTypes from "prop-types";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { useCallback, useRef, useState } from "react";
+import { useTheme } from "@mui/material/styles";
import FormSchema from "../../shared/FormSchema";
import FormSchemaContainer from "../../shared/FormSchemaContainer";
-import { generateSequentialName } from "../../../utils/nameGenerator";
import FormInputWrapper from "../../configurableObject/Inputs/FormInputWrapper";
import InputWithDebounce from "../../shared/InputWithDebounce";
+import { generateSequentialName } from "../../../utils/nameGenerator";
+import FormSchemaFieldCard from "../../shared/FormSchemaFieldCard";
import { useTranslation } from "react-i18next";
+import SideBar from "../../threeSectionLayout/panelContainers/SideBar";
/**
* Right sidebar component for configuring dataloader parameters
@@ -22,22 +25,12 @@ export default function DataloaderConfigBar({
selectedDataloader,
formSubmitRef,
setError,
- existingDatasets = [],
onValuesChange,
}) {
const [inferenceRows, setInferenceRows] = useState(1000);
- // Track FormSchema values separately so we can merge with inference_rows
const schemaValuesRef = useRef({});
const { t } = useTranslation(["common", "datasets"]);
-
- const { defaultName } = useMemo(
- () =>
- generateSequentialName({
- base: "Dataset",
- items: existingDatasets,
- }),
- [existingDatasets],
- );
+ const theme = useTheme();
// Handler for when FormSchema values change - merge with inference_rows
const handleFormSchemaValuesChange = useCallback(() => {
@@ -51,7 +44,7 @@ export default function DataloaderConfigBar({
// Handler for when inference_rows changes - merge with schema values
const handleInferenceRowsChange = useCallback(
(val) => {
- const numeric = val ? Number(val) : undefined;
+ const numeric = val ? Math.max(2, Number(val)) : 2;
setInferenceRows(numeric);
if (onValuesChange) {
onValuesChange({ ...schemaValuesRef.current, inference_rows: numeric });
@@ -70,7 +63,7 @@ export default function DataloaderConfigBar({
justifyContent="center"
alignItems="center"
bgcolor="background.box"
- borderRadius={2}
+ borderBottom={`0.1px solid ${(theme) => theme.palette.divider}`}
p={3}
>
- {/* Inferred configuration - displayed above the autogenerated schema form
- This section is not part of the dataloader schema and can be used to
- expose inferred/auxiliary parameters (like inference_rows) to the UI.
- */}
-
-
-
- {t("datasets:label.inferredConfiguration")}
-
-
-
-
-
+
+
+
+ {t("datasets:label.dataloaderConfiguration")}
+
+
- {/* Header */}
+
-
- {t("datasets:label.dataloaderConfiguration")}
-
-
-
- {/* Configuration Form */}
-
-
- {selectedDataloader}
-
-
-
-
-
+
+
+
+
+
+
+
-
+
);
}
@@ -184,6 +137,5 @@ DataloaderConfigBar.propTypes = {
selectedDataloader: PropTypes.string,
formSubmitRef: PropTypes.shape({ current: PropTypes.any }),
setError: PropTypes.func,
- existingDatasets: PropTypes.array,
onValuesChange: PropTypes.func,
};
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx
index 2db0b818b..2b3e7941d 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/PreviewDatasetTable.jsx
@@ -245,11 +245,10 @@ export default function PreviewDatasetTable({
) : (
handleStartEdit(field)}
sx={{
fontWeight: 600,
- fontSize: "0.875rem",
cursor: "pointer",
transition: "all 0.2s",
"&:hover": {
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/SelectDataloaderStep.jsx b/DashAI/front/src/components/notebooks/datasetCreation/SelectDataloaderStep.jsx
index 9e327db34..1a5271182 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/SelectDataloaderStep.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/SelectDataloaderStep.jsx
@@ -1,9 +1,6 @@
-import { useEffect, useState } from "react";
-import { useSnackbar } from "notistack";
-import { getComponents as getComponentsRequest } from "../../../api/component";
-import ItemSelectorWithInfo from "../../custom/ItemSelectorWithInfo";
-import { Grid } from "@mui/material";
-import FormSchemaButtonGroup from "../../shared/FormSchemaButtonGroup";
+import { useEffect } from "react";
+import ComponentSelector from "../../custom/ComponentSelector";
+import { Box, Button, CircularProgress, Stack } from "@mui/material";
import { useTourContext } from "../../tour/TourProvider";
import { useTranslation } from "react-i18next";
@@ -13,41 +10,19 @@ import { useTranslation } from "react-i18next";
* @param {function} goToPrevStep - Function to navigate back to the previous step in the dataset creation flow.
* @param {object} selectedDataloader - The currently selected dataloader
* @param {function} setSelectedDataloader - Function to update the selected dataloader
+ * @param {Array} dataloaders - List of available dataloaders (fetched by parent)
+ * @param {boolean} loadingDataloaders - Whether dataloaders are still loading
*/
export default function SelectDataloaderStep({
goToNextStep,
goToPrevStep,
selectedDataloader,
setSelectedDataloader,
+ dataloaders = [],
+ loadingDataloaders = false,
}) {
const tourContext = useTourContext();
- const { enqueueSnackbar } = useSnackbar();
const { t } = useTranslation(["datasets", "common"]);
- const [dataloaders, setDataloaders] = useState([]);
- const [loading, setLoading] = useState(true);
-
- async function getCompatibleDataloaders() {
- setLoading(true);
- try {
- const dataloaders = await getComponentsRequest({
- selectTypes: ["DataLoader"],
- });
- setDataloaders(dataloaders);
- } catch (error) {
- enqueueSnackbar(t("datasets:error.fetchingDataloaders"), {
- variant: "error",
- });
- if (error.response) {
- console.error("Response error:", error.message);
- } else if (error.request) {
- console.error("Request error", error.request);
- } else {
- console.error("Unknown Error", error.message);
- }
- } finally {
- setLoading(false);
- }
- }
const handleNext = () => {
if (tourContext?.run) {
@@ -65,7 +40,7 @@ export default function SelectDataloaderStep({
};
useEffect(() => {
- if (!loading && tourContext?.run) {
+ if (!loadingDataloaders && tourContext?.run) {
setTimeout(() => {
const cards = document.querySelectorAll('[role="button"]');
cards.forEach((card) => {
@@ -76,27 +51,31 @@ export default function SelectDataloaderStep({
});
}, 100);
}
- }, [loading, tourContext]);
+ }, [loadingDataloaders, tourContext]);
- // fetches the available dataloaders
- useEffect(() => {
- getCompatibleDataloaders();
- }, [t]);
return (
-
- {/* List of dataloaders */}
-
- {!loading && (
- {
+
+
+ {loadingDataloaders ? (
+
+
+
+ ) : (
+ ({
+ ...d,
+ category: d.metadata?.category,
+ }))}
+ categoryKey="category"
+ selected={selectedDataloader || null}
+ onSelect={(item) => {
setSelectedDataloader(item);
if (
tourContext?.run &&
@@ -105,24 +84,36 @@ export default function SelectDataloaderStep({
tourContext.nextStep();
}
}}
- data-tour="csv-dataloader-option"
+ searchPlaceholder={t("datasets:searchDataloaders", {
+ defaultValue: "Search data loaders...",
+ })}
/>
)}
-
-
-
-
-
+
+
+
+
+ {t("common:back")}
+
+
+ {t("common:next")}
+
+
+
);
}
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/Upload.jsx b/DashAI/front/src/components/notebooks/datasetCreation/Upload.jsx
index 15ea33a80..d506b2663 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/Upload.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/Upload.jsx
@@ -356,7 +356,12 @@ function Upload({
container
direction="column"
rowSpacing={1}
- sx={{ width: "100%", bgcolor: theme.palette.ui.box }}
+ sx={{
+ width: "100%",
+ bgcolor: theme.palette.ui.box,
+ p: 2,
+ borderRadius: 2,
+ }}
data-tour="upload-area"
>
{/* state text */}
diff --git a/DashAI/front/src/components/notebooks/datasetCreation/UploadDatasetSteps.jsx b/DashAI/front/src/components/notebooks/datasetCreation/UploadDatasetSteps.jsx
index 063bf46af..631165778 100644
--- a/DashAI/front/src/components/notebooks/datasetCreation/UploadDatasetSteps.jsx
+++ b/DashAI/front/src/components/notebooks/datasetCreation/UploadDatasetSteps.jsx
@@ -1,15 +1,17 @@
import { useState, useRef, useEffect } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
import SelectDataloaderStep from "./SelectDataloaderStep";
import ConfigureAndUploadDatasetStep from "./ConfigureAndUploadDatasetStep";
import DataloaderConfigBar from "./DataloaderConfigBar";
-import CustomLayout from "../../custom/CustomLayout";
+import { Box, Typography } from "@mui/material";
+import ComponentDetailsPanel from "../../custom/ComponentDetailsPanel";
import { useTranslation } from "react-i18next";
import { useDatasetsAndNotebooks } from "../../custom/contexts/DatasetsAndNotebooksContext";
import { useTourContext } from "../../tour/TourProvider";
+import { getComponents as getComponentsRequest } from "../../../api/component";
+import { useSnackbar } from "notistack";
const UPLOAD_BASE_PATH = "/app/data/datasets/new";
-const UPLOAD_CONFIGURE_PATH = `${UPLOAD_BASE_PATH}/configure`;
export default function UploadDatasetSteps({ backHome }) {
const {
@@ -17,22 +19,58 @@ export default function UploadDatasetSteps({ backHome }) {
addDatasetOptimistically,
startDatasetPolling,
setRightBarContent,
+ setUploadDataloader,
} = useDatasetsAndNotebooks();
const navigate = useNavigate();
- const location = useLocation();
- const step = location.pathname.startsWith(UPLOAD_CONFIGURE_PATH) ? 1 : 0;
- const [selectedDataloader, setSelectedDataloader] = useState({});
+ const { dataloaderName } = useParams();
+ const step = dataloaderName ? 1 : 0;
+
+ const [selectedDataloader, setSelectedDataloader] = useState();
+ const [dataloaders, setDataloaders] = useState([]);
+ const [loadingDataloaders, setLoadingDataloaders] = useState(true);
const [formValues, setFormValues] = useState({});
const [error, setError] = useState(false);
const [previewError, setPreviewError] = useState(false);
const { t } = useTranslation(["datasets"]);
+ const { enqueueSnackbar } = useSnackbar();
const tourContext = useTourContext();
const formSubmitRef = useRef(null);
+ useEffect(() => {
+ async function fetchDataloaders() {
+ setLoadingDataloaders(true);
+ try {
+ const list = await getComponentsRequest({
+ selectTypes: ["DataLoader"],
+ });
+ setDataloaders(list);
+ } catch (error) {
+ enqueueSnackbar(t("datasets:error.fetchingDataloaders"), {
+ variant: "error",
+ });
+ } finally {
+ setLoadingDataloaders(false);
+ }
+ }
+ fetchDataloaders();
+ }, [t]);
+
+ // Sync selected dataloader from URL param, or redirect if unknown
+ useEffect(() => {
+ if (!dataloaderName || loadingDataloaders || dataloaders.length === 0)
+ return;
+ const match = dataloaders.find((d) => d.name === dataloaderName);
+ if (match) {
+ setSelectedDataloader(match);
+ } else {
+ navigate(UPLOAD_BASE_PATH, { replace: true });
+ }
+ }, [dataloaderName, loadingDataloaders, dataloaders, navigate]);
+
const goToNextStep = () => {
- navigate(UPLOAD_CONFIGURE_PATH);
+ navigate(`${UPLOAD_BASE_PATH}/${selectedDataloader.name}`);
};
const goToPrevStep = () => {
@@ -40,10 +78,20 @@ export default function UploadDatasetSteps({ backHome }) {
backHome();
return;
}
-
navigate(UPLOAD_BASE_PATH);
};
+ const getTitle = () => {
+ switch (step) {
+ case 0:
+ return t("datasets:label.selectDataloader");
+ case 1:
+ return t("datasets:label.uploadDataset");
+ default:
+ return t("datasets:label.createDataset");
+ }
+ };
+
const getSubtitle = () => {
switch (step) {
case 0:
@@ -55,28 +103,40 @@ export default function UploadDatasetSteps({ backHome }) {
}
};
+ // Sync selected dataloader to context so DataBreadcrumbs can show display_name
useEffect(() => {
- if (step === 1 && Object.keys(selectedDataloader).length === 0) {
- navigate(UPLOAD_BASE_PATH, { replace: true });
+ if (setUploadDataloader) {
+ setUploadDataloader(selectedDataloader?.name ? selectedDataloader : null);
}
- }, [step, selectedDataloader, navigate]);
+ }, [selectedDataloader, setUploadDataloader]);
+
+ // Clear right sidebar and dataloader on unmount
+ useEffect(() => {
+ return () => {
+ if (setRightBarContent) setRightBarContent(null);
+ if (setUploadDataloader) setUploadDataloader(null);
+ };
+ }, [setRightBarContent, setUploadDataloader]);
// Update the right sidebar based on current step
useEffect(() => {
- if (setRightBarContent) {
- if (step === 1 && Object.entries(selectedDataloader).length !== 0) {
- setRightBarContent(
- ,
- );
- } else {
- setRightBarContent(null);
- }
+ if (!setRightBarContent) return;
+
+ if (step === 0) {
+ setRightBarContent(
+ ,
+ );
+ } else if (step === 1 && selectedDataloader?.name) {
+ setRightBarContent(
+ ,
+ );
+ } else {
+ setRightBarContent(null);
}
}, [step, selectedDataloader, datasets, setRightBarContent]);
@@ -93,20 +153,34 @@ export default function UploadDatasetSteps({ backHome }) {
};
return (
-
+
+
+ {getTitle()}
+
+
+ {getSubtitle()}
+
+
+
{step === 0 && (
)}
- {step === 1 && Object.entries(selectedDataloader).length !== 0 && (
+ {step === 1 && selectedDataloader?.name && (
)}
-
+
);
}
diff --git a/DashAI/front/src/components/notebooks/explorer/ExplorerDetailsModal.jsx b/DashAI/front/src/components/notebooks/explorer/ExplorerDetailsModal.jsx
index 9bd82e845..432995214 100644
--- a/DashAI/front/src/components/notebooks/explorer/ExplorerDetailsModal.jsx
+++ b/DashAI/front/src/components/notebooks/explorer/ExplorerDetailsModal.jsx
@@ -193,13 +193,7 @@ export default function ExplorerDetailsModal({
{t("common:created")}
-
+
{formatDate(explorer.created)}
diff --git a/DashAI/front/src/components/notebooks/explorer/plotLayout/ColorscaleSelector.jsx b/DashAI/front/src/components/notebooks/explorer/plotLayout/ColorscaleSelector.jsx
index 0d69fa74c..2032285bb 100644
--- a/DashAI/front/src/components/notebooks/explorer/plotLayout/ColorscaleSelector.jsx
+++ b/DashAI/front/src/components/notebooks/explorer/plotLayout/ColorscaleSelector.jsx
@@ -102,12 +102,11 @@ export default function ColorscaleSelector({ value, onChange }) {
{/* Stop Index Label */}
-
{i + 1}
-
+
- {name}
+
+ {name}
+
- {key}
+
+ {key}
+
- {formatValue(value)}
+
+ {formatValue(value)}
+
))}
diff --git a/DashAI/front/src/components/notebooks/explorerCreation/ParameterStepExplorer.jsx b/DashAI/front/src/components/notebooks/explorerCreation/ParameterStepExplorer.jsx
index d0739604a..50c299e1d 100644
--- a/DashAI/front/src/components/notebooks/explorerCreation/ParameterStepExplorer.jsx
+++ b/DashAI/front/src/components/notebooks/explorerCreation/ParameterStepExplorer.jsx
@@ -50,9 +50,7 @@ export default function ParameterStepExplorer({
variant="h6"
sx={{ fontWeight: 700, color: "primary.main", mb: 1 }}
>
- {t("datasets:label.configureParametersStep", {
- step: 2,
- })}
+ {t("datasets:label.configureParameters")}
{/* Content */}
-
- {t("datasets:label.selectScopeStep", {
- step: 1,
- })}
-
-
- {t("datasets:label.selectedDataset")}
-
diff --git a/DashAI/front/src/components/notebooks/notebookCreation/UploadNotebookSteps.jsx b/DashAI/front/src/components/notebooks/notebookCreation/UploadNotebookSteps.jsx
index b08c78edb..90e6fac0b 100644
--- a/DashAI/front/src/components/notebooks/notebookCreation/UploadNotebookSteps.jsx
+++ b/DashAI/front/src/components/notebooks/notebookCreation/UploadNotebookSteps.jsx
@@ -1,12 +1,9 @@
-import { useState, useMemo, useEffect } from "react";
-import { Typography, TextField, Box } from "@mui/material";
+import { useState, useMemo, useEffect, useRef } from "react";
+import { Typography, TextField, Box, Button } from "@mui/material";
import { useFormik } from "formik";
-import CustomLayout from "../../custom/CustomLayout";
-import FormSchemaButtonGroup from "../../shared/FormSchemaButtonGroup";
import DatasetAutocomplete from "./DatasetAutocomplete";
import { createNotebook } from "../../../api/notebook";
import { useSnackbar } from "notistack";
-import { generateSequentialName } from "../../../utils/nameGenerator";
import NoteBox from "../NoteBox";
import { useTourContext } from "../../tour/TourProvider";
import { useTranslation } from "react-i18next";
@@ -27,17 +24,15 @@ export default function UploadNotebookSteps({
const tourContext = useTourContext();
const { t } = useTranslation(["datasets", "common"]);
- const { defaultName } = useMemo(() => {
- if (!selectedDataset) {
- return { defaultName: "" };
- }
+ const defaultName = useMemo(() => {
+ const maxId = existingNotebooks.reduce(
+ (max, nb) => Math.max(max, nb.id ?? 0),
+ 0,
+ );
+ return `Notebook_${maxId + 1}`;
+ }, [existingNotebooks]);
- return generateSequentialName({
- base: `Notebook_${selectedDataset.name}`,
- items: existingNotebooks,
- filter: (notebook) => notebook.dataset_id === selectedDataset.id,
- });
- }, [selectedDataset, existingNotebooks]);
+ const lastAutoFilledRef = useRef(null);
const formik = useFormik({
initialValues: {
@@ -79,117 +74,109 @@ export default function UploadNotebookSteps({
});
useEffect(() => {
- if (selectedDataset && defaultName && !formik.values.name.trim()) {
- formik.setValues({
- name: defaultName,
- description: formik.values.description,
- });
- }
- }, [
- selectedDataset,
- defaultName,
- formik.values.name,
- formik.values.description,
- ]);
-
- const getNameError = () => {
- if (!selectedDataset) {
- return null;
- }
-
+ if (!defaultName) return;
const currentName = formik.values.name.trim();
- if (!currentName) {
- return t("common:nameRequired");
+ if (!currentName || currentName === lastAutoFilledRef.current) {
+ formik.setFieldValue("name", defaultName);
+ lastAutoFilledRef.current = defaultName;
}
- return null;
- };
+ }, [defaultName]);
- const nameError = getNameError();
+ const nameError = formik.values.name.trim() ? null : t("common:nameRequired");
+ const isValid = selectedDataset && !nameError;
return (
-
-
-
+
+ {t("datasets:label.createNewNotebook")}
+
+
+ {t("datasets:label.createNewNotebookDescription")}
+
+
+
+
- {t("datasets:label.selectDatasetForNotebook")}
-
-
-
+
+
+
+
+
+
+
+
+
- {t("datasets:label.nameYourNotebook")}
-
- {/* Notebook name */}
-
- {/* Notebook description */}
-
-
-
+
+ {t("common:back")}
+
+
+ {t("datasets:button.createNotebook")}
+
-
+
);
}
diff --git a/DashAI/front/src/components/notebooks/tool/ConfigureToolModal.jsx b/DashAI/front/src/components/notebooks/tool/ConfigureToolModal.jsx
index 9464d5e82..3596ce13a 100644
--- a/DashAI/front/src/components/notebooks/tool/ConfigureToolModal.jsx
+++ b/DashAI/front/src/components/notebooks/tool/ConfigureToolModal.jsx
@@ -121,7 +121,7 @@ export default function ConfigureToolModal({
{steps.map((label) => (
-
+
{label}
))}
diff --git a/DashAI/front/src/components/pipelines/CustomNode.jsx b/DashAI/front/src/components/pipelines/CustomNode.jsx
index d49542e0e..9e765079b 100644
--- a/DashAI/front/src/components/pipelines/CustomNode.jsx
+++ b/DashAI/front/src/components/pipelines/CustomNode.jsx
@@ -123,7 +123,8 @@ const CustomNode = ({ data, isConnectable }) => {
}}
>
{data.name || data.label}
diff --git a/DashAI/front/src/components/predictions/DatasetSelector.jsx b/DashAI/front/src/components/predictions/DatasetSelector.jsx
index 77b083825..268ef3799 100644
--- a/DashAI/front/src/components/predictions/DatasetSelector.jsx
+++ b/DashAI/front/src/components/predictions/DatasetSelector.jsx
@@ -74,9 +74,9 @@ function DatasetSelector({
{selectedDataset && (
<>
-
+
{t("prediction:label.predictionInfo")}
-
+
{t("prediction:label.inputColumns")}:
diff --git a/DashAI/front/src/components/predictions/PredictionsTable.jsx b/DashAI/front/src/components/predictions/PredictionsTable.jsx
index ef3007573..2252e0076 100644
--- a/DashAI/front/src/components/predictions/PredictionsTable.jsx
+++ b/DashAI/front/src/components/predictions/PredictionsTable.jsx
@@ -92,7 +92,7 @@ function PredictionsTable({ predictions, onItemClick, onItemDelete }) {
{t("prediction:label.manualInput")}
diff --git a/DashAI/front/src/components/shared/FormSchemaButtonGroup.jsx b/DashAI/front/src/components/shared/FormSchemaButtonGroup.jsx
index ed03723ae..9046a6cdd 100644
--- a/DashAI/front/src/components/shared/FormSchemaButtonGroup.jsx
+++ b/DashAI/front/src/components/shared/FormSchemaButtonGroup.jsx
@@ -1,4 +1,4 @@
-import { Button, ButtonGroup } from "@mui/material";
+import { Button, Box } from "@mui/material";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -29,7 +29,17 @@ function FormSchemaButtonGroup({
: undefined);
return (
-
+
{onCancel && (
{finalBackText}
@@ -45,7 +55,7 @@ function FormSchemaButtonGroup({
{finalSaveText}
)}
-
+
);
}
diff --git a/DashAI/front/src/components/shared/FormSchemaDialog.jsx b/DashAI/front/src/components/shared/FormSchemaDialog.jsx
index 24386ac33..966397b7d 100644
--- a/DashAI/front/src/components/shared/FormSchemaDialog.jsx
+++ b/DashAI/front/src/components/shared/FormSchemaDialog.jsx
@@ -38,7 +38,7 @@ function FormSchemaDialog({
},
}}
>
-
+
-
{count}
-
+
{open ? (
{title}
-
{totalCount}
-
+
{/* Groups - Scrollable */}
@@ -176,7 +177,7 @@ export default function GroupedCollapsibleList({
{groupName}
-
{items?.length || 0}
-
+
{/* Group Items */}
diff --git a/DashAI/front/src/components/threeSectionLayout/ItemBox.jsx b/DashAI/front/src/components/threeSectionLayout/ItemBox.jsx
index e81fe5a97..f63164f35 100644
--- a/DashAI/front/src/components/threeSectionLayout/ItemBox.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/ItemBox.jsx
@@ -135,20 +135,15 @@ const ItemBox = forwardRef(function ItemBox(
}}
/>
) : (
-
+
{editedName}
)}
{description ? description : ""}
diff --git a/DashAI/front/src/components/threeSectionLayout/OptionBox.jsx b/DashAI/front/src/components/threeSectionLayout/OptionBox.jsx
index b2684ca9a..bed9019e3 100644
--- a/DashAI/front/src/components/threeSectionLayout/OptionBox.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/OptionBox.jsx
@@ -94,8 +94,8 @@ export default function OptionBox({
{/* Title */}
))}
-
→
-
+
);
diff --git a/DashAI/front/src/components/threeSectionLayout/SelectOptionMenu.jsx b/DashAI/front/src/components/threeSectionLayout/SelectOptionMenu.jsx
index eaecd88a7..64d4b3e5d 100644
--- a/DashAI/front/src/components/threeSectionLayout/SelectOptionMenu.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/SelectOptionMenu.jsx
@@ -64,7 +64,6 @@ export default function SelectOptionMenu({
(
@@ -86,7 +85,7 @@ export default function SelectOptionMenu({
return (
diff --git a/DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx b/DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx
index 11a5c1bcc..3d42ba7c2 100644
--- a/DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx
@@ -1,21 +1,21 @@
import { Box } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+
+export default function SideBar({ children, ...props }) {
+ const theme = useTheme();
-export default function SideBar({ children }) {
return (
{children}
diff --git a/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx b/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx
index 927217220..488122339 100644
--- a/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx
@@ -22,8 +22,7 @@ export default function LeftPanel({ children, "data-tour": dataTour }) {
transform: "translateY(-50%)",
bgcolor: "primary.main",
color: "primary.contrastText",
- border: "1px solid",
- borderColor: "primary.dark",
+ // border: "1px solid",
boxShadow: 2,
width: 18,
height: 32,
diff --git a/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx b/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx
index 18917666f..2db74b4ed 100644
--- a/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx
+++ b/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx
@@ -25,8 +25,6 @@ export default function RightPanel({
transform: "translateY(-50%)",
bgcolor: "primary.main",
color: "primary.contrastText",
- border: "1px solid",
- borderColor: "primary.dark",
boxShadow: 2,
width: 18,
height: 32,
diff --git a/DashAI/front/src/components/tour/CustomTooltip.jsx b/DashAI/front/src/components/tour/CustomTooltip.jsx
index 408d33693..863060361 100644
--- a/DashAI/front/src/components/tour/CustomTooltip.jsx
+++ b/DashAI/front/src/components/tour/CustomTooltip.jsx
@@ -36,7 +36,7 @@ export const CustomTooltip = ({
lineHeight: "1.6",
color: "#333",
"& h3": {
- fontSize: "18px",
+ fontSize: "22px",
fontWeight: "bold",
marginBottom: "8px",
marginTop: 0,
@@ -136,8 +136,8 @@ export const CustomTooltip = ({
{continuous && (
-
+
);
diff --git a/DashAI/front/src/constants/tours/datasetViewTour.js b/DashAI/front/src/constants/tours/datasetViewTour.js
index d65e919a5..863d3ae6e 100644
--- a/DashAI/front/src/constants/tours/datasetViewTour.js
+++ b/DashAI/front/src/constants/tours/datasetViewTour.js
@@ -52,6 +52,21 @@ export const datasetViewTourSteps = [
disableCloseOnEsc: true,
disableBackButton: true,
},
+ {
+ target: '[data-tour="models-dataset-selection"]',
+ content: (
+
+
+
+ ),
+ placement: "bottom",
+ disableBeacon: true,
+ disableOverlayClose: true,
+ disableBackButton: true,
+ },
{
target: '[data-tour="create-notebook-button"]',
content: (
diff --git a/DashAI/front/src/constants/tours/datasetsTour.js b/DashAI/front/src/constants/tours/datasetsTour.js
index 2eff86dba..e35a4337f 100644
--- a/DashAI/front/src/constants/tours/datasetsTour.js
+++ b/DashAI/front/src/constants/tours/datasetsTour.js
@@ -65,7 +65,7 @@ export const datasetsTourSteps = [
onMouseOut={(e) => (e.target.style.backgroundColor = "#ef9f27")}
>
-
+
@@ -148,9 +148,7 @@ export const datasetsTourSteps = [
-
+
),
diff --git a/DashAI/front/src/constants/tours/experimentsTour.js b/DashAI/front/src/constants/tours/experimentsTour.js
index d9da1dc17..d265ec011 100644
--- a/DashAI/front/src/constants/tours/experimentsTour.js
+++ b/DashAI/front/src/constants/tours/experimentsTour.js
@@ -523,7 +523,7 @@ export const experimentsTourSteps = [
padding: "8px",
borderRadius: "4px",
marginTop: "10px",
- fontSize: "0.9em",
+ fontSize: "14px",
}}
>
⏱️ Training may take a few moments depending on your dataset size and
diff --git a/DashAI/front/src/constants/tours/modelsTour.js b/DashAI/front/src/constants/tours/modelsTour.js
index 8217b4d6b..287256f6d 100644
--- a/DashAI/front/src/constants/tours/modelsTour.js
+++ b/DashAI/front/src/constants/tours/modelsTour.js
@@ -110,24 +110,6 @@ export const modelsTourSteps = [
disableBackButton: true,
maxWidth: "320px",
},
- {
- target: '[data-tour="models-next-button"]',
- content: (
-
-
-
- ),
- placement: "top",
- disableBeacon: true,
- spotlightClicks: true,
- isInteractive: true,
- disableBackButton: true,
- },
{
target: '[data-tour="models-validation-alert"]',
content: (
@@ -222,7 +204,7 @@ export const modelsTourSteps = [
),
- placement: "right",
+ placement: "left",
disableBeacon: true,
maxWidth: "320px",
},
@@ -239,9 +221,7 @@ export const modelsTourSteps = [
-
+
),
diff --git a/DashAI/front/src/hooks/useSchema.js b/DashAI/front/src/hooks/useSchema.js
index 2ad3ade53..a0ad03982 100644
--- a/DashAI/front/src/hooks/useSchema.js
+++ b/DashAI/front/src/hooks/useSchema.js
@@ -14,6 +14,8 @@ export default function useSchema({ modelName = null } = {}) {
const { t } = useTranslation();
useEffect(() => {
+ setModel(null);
+
const getModel = async () => {
try {
setLoading(true);
diff --git a/DashAI/front/src/pages/datasets/DatasetsContent.jsx b/DashAI/front/src/pages/datasets/DatasetsContent.jsx
index d55739d84..733429002 100644
--- a/DashAI/front/src/pages/datasets/DatasetsContent.jsx
+++ b/DashAI/front/src/pages/datasets/DatasetsContent.jsx
@@ -28,6 +28,7 @@ export default function DatasetsContent() {
notebooks,
selectedNotebookId,
selectedDatasetId,
+ selectedOption,
rightBarContent,
step,
selectDataset,
@@ -119,7 +120,10 @@ export default function DatasetsContent() {
>
- ) : selectedDatasetId ? (
+ ) : selectedDatasetId ||
+ (step === 1 &&
+ selectedOption === OptionsEnum.NOTEBOOK &&
+ location.state?.preselectedDatasetId) ? (
{
const path = location.pathname;
- if (path.startsWith("/app/generative/sessions/new/") && params.taskName) {
- const task = tasks.find((tk) => tk.name === params.taskName);
- setSelectedTaskName(params.taskName);
- setSelectedDisplayName(task?.display_name ?? null);
+ if (isCreating) {
setSelectedSessionId(null);
+ setSelectedTaskName("");
+ setSelectedDisplayName(null);
setStepIndex(1);
return;
}
@@ -65,7 +68,7 @@ export default function GenerativeContent() {
setSelectedDisplayName(null);
setStepIndex(0);
}
- }, [location.pathname, params.id, params.taskName, tasks, sessions]);
+ }, [location.pathname, params.id, tasks, sessions, isCreating]);
useEffect(() => {
setDisabled?.(
@@ -74,39 +77,34 @@ export default function GenerativeContent() {
);
}, [stepIndex, selectedSessionId, setDisabled, t]);
- return (
+ const renderCenter = () => {
+ if (selectedSessionId) return ;
+ if (isCreating) return ;
+ return ;
+ };
+
+ const renderRight = () => {
+ if (isCreating) return ;
+ return ;
+ };
+
+ const layout = (
-
- {selectedSessionId ? (
-
- ) : stepIndex === 0 ? (
-
- ) : (
-
-
-
-
- )}
-
-
+ {renderCenter()}
-
+ {renderRight()}
);
+
+ return isCreating ? (
+ {layout}
+ ) : (
+ layout
+ );
}
diff --git a/DashAI/front/src/pages/home/Home.jsx b/DashAI/front/src/pages/home/Home.jsx
index dbb294c69..5dcc846f9 100644
--- a/DashAI/front/src/pages/home/Home.jsx
+++ b/DashAI/front/src/pages/home/Home.jsx
@@ -268,18 +268,15 @@ function Home() {
: theme.palette.background.box,
}}
>
-
+
{t("home:label.welcomeDashboardAI")}
diff --git a/DashAI/front/src/pages/plugins/components/PluginsCard.jsx b/DashAI/front/src/pages/plugins/components/PluginsCard.jsx
index 137743296..be5031bdd 100644
--- a/DashAI/front/src/pages/plugins/components/PluginsCard.jsx
+++ b/DashAI/front/src/pages/plugins/components/PluginsCard.jsx
@@ -97,7 +97,7 @@ function PluginsCard({
-
+
{t("models:label.generalMetrics")}
@@ -29,7 +29,7 @@ function ResultsGraphsSwitch({ showCustomMetrics, handleToggleMetrics }) {
/>
-
+
{t("models:label.customMetrics")}
diff --git a/DashAI/front/src/styles/theme.js b/DashAI/front/src/styles/theme.js
index bd6fac82b..1dc4c92d5 100644
--- a/DashAI/front/src/styles/theme.js
+++ b/DashAI/front/src/styles/theme.js
@@ -168,57 +168,49 @@ const getTheme = (mode) => ({
typography: {
fontFamily: '"IBM Plex Sans", sans-serif',
- // --- SANS SERIF ---
- pageTitle: {
- fontSize: "18px",
- fontWeight: 600,
- letterSpacing: "-0.01em",
- }, // Main titles
-
- cardTitle: {
- fontSize: "15px",
- fontWeight: 600,
- letterSpacing: "-0.01em",
- }, // Module/Card titles
-
- navItem: {
- fontSize: "12.5px",
- fontWeight: 400,
- }, // Sidebar links/Navigation
-
- description: {
+ // --- ESCALA DE TITULARES ---
+ h1: { fontSize: "28px", fontWeight: 700, letterSpacing: "-0.01em" },
+ h2: { fontSize: "22px", fontWeight: 600, letterSpacing: "-0.01em" },
+ h3: { fontSize: "20px", fontWeight: 600, letterSpacing: "-0.01em" },
+ h4: { fontSize: "17px", fontWeight: 600, letterSpacing: "-0.01em" },
+ h5: { fontSize: "16px", fontWeight: 600 },
+ h6: { fontSize: "14px", fontWeight: 600 },
+
+ // --- ESCALA DE CUERPO ---
+ subtitle1: { fontSize: "17px", fontWeight: 400 }, // Parámetros principales
+ subtitle2: { fontSize: "16px", fontWeight: 400 }, // Texto destacado
+ body1: { fontSize: "14px", fontWeight: 400, lineHeight: 1.6 }, // Cuerpo / párrafos
+ body2: { fontSize: "12px", fontWeight: 400, lineHeight: 1.5 }, // Texto auxiliar / labels
+ caption: { fontSize: "12px", fontWeight: 400 }, // Información secundaria, captions
+ code: {
+ fontFamily: '"IBM Plex Mono", monospace',
fontSize: "12px",
- fontWeight: 300,
- lineHeight: 1.65,
- color: "rgba(171, 178, 191, 0.45)",
- }, // Explanatory text
+ fontWeight: 400,
+ }, // Code snippets, valores técnicos
- // --- MONOSPACE ---
+ // --- VARIANTES UI FUNCIONALES ---
+ navItem: { fontSize: "12px", fontWeight: 400 }, // Sidebar links/Navigation
tabLabel: {
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "10px",
letterSpacing: "0.12em",
textTransform: "uppercase",
}, // Navigation tabs
-
sectionLabel: {
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "9px",
letterSpacing: "0.2em",
textTransform: "uppercase",
}, // Sidebar section headers
-
statusBadge: {
fontFamily: '"IBM Plex Mono", monospace',
fontSize: "8.5px",
letterSpacing: "0.12em",
textTransform: "uppercase",
},
-
- // Others
button: {
- fontSize: "15px",
- fontWeight: 400,
+ fontSize: "14px",
+ fontWeight: 500,
letterSpacing: "-0.01em",
textTransform: "uppercase",
},
diff --git a/DashAI/front/src/types/component.ts b/DashAI/front/src/types/component.ts
index 6234fe108..9bedf444b 100644
--- a/DashAI/front/src/types/component.ts
+++ b/DashAI/front/src/types/component.ts
@@ -8,4 +8,6 @@ export interface IComponent {
schema: IParameterJsonSchema;
metadata: ITaskMetadataParameters;
description: string;
+ display_name?: string;
+ color?: string;
}
diff --git a/DashAI/front/src/utils/i18n/locales/en/common.json b/DashAI/front/src/utils/i18n/locales/en/common.json
index 90e7e9af0..18b7a6bc5 100644
--- a/DashAI/front/src/utils/i18n/locales/en/common.json
+++ b/DashAI/front/src/utils/i18n/locales/en/common.json
@@ -215,5 +215,7 @@
"expand": "Expand",
"optimize": "Optimize",
"viewMore": "View more",
- "viewLess": "View less"
+ "viewLess": "View less",
+ "switchToDarkMode": "Switch to dark mode",
+ "switchToLightMode": "Switch to light mode"
}
diff --git a/DashAI/front/src/utils/i18n/locales/en/custom.json b/DashAI/front/src/utils/i18n/locales/en/custom.json
index 1a2187d41..7ca90eba3 100644
--- a/DashAI/front/src/utils/i18n/locales/en/custom.json
+++ b/DashAI/front/src/utils/i18n/locales/en/custom.json
@@ -1,5 +1,14 @@
{
"inferenceMethods": "Inference Methods",
"selectAnItemToShowInfo": "Select an item to see the description.",
- "selectInferenceMethods": "Select the inference methods you want to apply"
+ "selectInferenceMethods": "Select the inference methods you want to apply",
+ "search": "Search",
+ "noItemsFound": "No components found",
+ "tryAdjustingSearch": "Try adjusting your search or filters",
+ "componentsAvailable_one": "{{count}} component available",
+ "componentsAvailable_other": "{{count}} components available",
+ "componentDetails": "Component Details",
+ "description": "Description",
+ "tags": "Tags",
+ "noDescriptionAvailable": "No description available."
}
diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json
index fe7184973..489c06130 100644
--- a/DashAI/front/src/utils/i18n/locales/en/datasets.json
+++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json
@@ -197,7 +197,7 @@
"ifYourDatasetHaveSplits": "If your dataset have splits, upload it as a zip file",
"indices": "Indices (comma separated, or type 'all')",
"inferenceRows": "Inference Rows",
- "inferenceRowsDescription": "Number of rows used for preview/type inference (quick override).",
+ "inferenceRowsDescription": "Number of rows used for preview/type inference (minimum 2).",
"inferredConfiguration": "Type Inference Configuration",
"insightConstantColumn": "This column has only one unique value. It provides no information for analysis.",
"insightHighCardinality": "This categorical column has more than 100 unique values. Consider grouping or encoding.",
diff --git a/DashAI/front/src/utils/i18n/locales/en/datasetsTour.json b/DashAI/front/src/utils/i18n/locales/en/datasetsTour.json
index 7a674db45..bce30e637 100644
--- a/DashAI/front/src/utils/i18n/locales/en/datasetsTour.json
+++ b/DashAI/front/src/utils/i18n/locales/en/datasetsTour.json
@@ -5,6 +5,7 @@
"dataLoaderConfig": "<0><0>DataLoader Configuration0><1>Here you can configure how the dataset should be loaded:1><2><0><0>Name:0> Give your dataset a meaningful name0><1><0>Separator:0> The character that separates values (comma for CSV)1><2><0>Other options:0> Advanced settings for specific needs2>2><3>💡 <0>Pro tip:0> The default settings work well for most CSV files, so you can usually leave them as they are!3>0>",
"datasetModule": "<0><0>Dataset Module0><1>This is where you manage your data and create interactive notebooks for analysis. Let's see how to get started!1>0>",
"downloadSample": "<0><0>Download Sample Dataset0><1>To get started quickly, let's download a sample dataset.1><2><0>Download Personality_Dataset.csv0>2><3>💡 <0>Tip:0> The file will be saved to your Downloads folder by default.3><4>Once downloaded, click \"Next\" to learn how to upload it!4>0>",
+ "notebookDatasetSelection": "<0><0>Select a Dataset0><1>Choose the dataset you want to explore in this notebook. The dataset linked to this notebook will be used for all visualizations and transformations.1>0>",
"finishProcess": "<0><0>Finish the Process0><1>Click \"Create Notebook\" to start working with your data in an interactive environment.1><2>You'll be able to visualize, transform, and prepare your data for modeling.2>0>",
"importantNote": "<0><0>Important Note0><1>Pay attention to this information1><2>This ensures your original data remains intact while you experiment.2>0>",
"nextSteps": "<0><0>Next Steps: Create a Notebook0><1>Now click \"New Notebook\" to open the dataset in an interactive environment.1><2>In a notebook, you can analyze, visualize, and transform your data.2>0>",
diff --git a/DashAI/front/src/utils/i18n/locales/en/experiments.json b/DashAI/front/src/utils/i18n/locales/en/experiments.json
index 359dfc2d4..211b84a1b 100644
--- a/DashAI/front/src/utils/i18n/locales/en/experiments.json
+++ b/DashAI/front/src/utils/i18n/locales/en/experiments.json
@@ -60,10 +60,19 @@
"parameterModification": "Parameter Modification",
"prepareDataset": "Prepare dataset",
"recommendPreprocessMissingValues": "It's recommended to preprocess the dataset to handle these missing values before training a model.",
+ "manual": "Manual",
+ "predefined": "Predefined",
+ "random": "Random",
+ "rowIndexes": "Row Indexes",
+ "rowIndexesDescription": "Specify row ranges for each split using comma-separated values or ranges (e.g. 0-100, 200).",
"seed": "Seed",
+ "splits": "Splits",
+ "splitsDescription": "Proportion of data assigned to each subset. Values must be between 0 and 1 and sum to 1.",
+ "stratifyRequiresShuffle": "Requires shuffle to be enabled",
"selectDataset": "Select dataset",
"selectDatasetColumns": "Indicate which columns of the dataset will be used as input and output.",
"selectDatasetTitle": "Select a dataset for the selected task",
+ "splitType": "Split type",
"selectHowToDivideDataset": "Select how to divide the dataset into training, validation and test subsets.",
"selectInputOutputColumnsDescription": "Select column names from the lists.",
"selectMetrics": "Select metrics",
diff --git a/DashAI/front/src/utils/i18n/locales/en/generative.json b/DashAI/front/src/utils/i18n/locales/en/generative.json
index dcca069fa..f0b46ed1e 100644
--- a/DashAI/front/src/utils/i18n/locales/en/generative.json
+++ b/DashAI/front/src/utils/i18n/locales/en/generative.json
@@ -11,6 +11,7 @@
"failedToFetchSessionInfo": "Failed to fetch session information",
"failedToFetchSessions": "Failed to fetch sessions",
"failedToFetchTasks": "Failed to fetch generative tasks",
+ "failedToLoadModels": "Failed to load models",
"failedToUpdateSession": "Failed to update session",
"nameRequired": "Name is required",
"processError": "The process has failed. Deleting it... {{error}}",
@@ -24,7 +25,16 @@
"showLess": "Show less",
"readMore": "Read more",
"generativeModule": "Generative Module",
+ "createNewSession": "Create a New Session",
+ "createNewSessionDescription": "Pick a model, configure its parameters, and start chatting.",
+ "configureSession": "Configure session",
+ "modelHasNoParameters": "This model has no configurable parameters.",
+ "nameAndDescribeYourSession": "Name your session. Tweak parameters on the right. You can change them at any time during the session.",
"nameYourSession": "Name your session",
+ "newSession": "New Session",
+ "pickAModelGroupedByTask": "Pick a model. Models are grouped by task.",
+ "searchModels": "Search models...",
+ "startBySelectingATask": "Start by selecting a generative task.",
"noSessionsFound": "No sessions found",
"parameterChangeEvent": "Parameters updated: <1>1>",
"parameterChangeHistory": "Parameter change history for the current session",
@@ -45,9 +55,9 @@
"attachVideo": "Attach video",
"attachMedia": "Attach media",
"attachMediaToContinue": "Attach media to continue",
- "noInputAvailable": "No input available for this task"
+ "noInputAvailable": "No input available for this task",
+ "selectModelCrumb": "Select Model"
},
-
"message": {
"sessionCreatedSuccess": "Session successfully created.",
"sessionDeleted": "Session deleted successfully",
diff --git a/DashAI/front/src/utils/i18n/locales/en/models.json b/DashAI/front/src/utils/i18n/locales/en/models.json
index f53441bee..cb0e25a2e 100644
--- a/DashAI/front/src/utils/i18n/locales/en/models.json
+++ b/DashAI/front/src/utils/i18n/locales/en/models.json
@@ -66,6 +66,7 @@
"chooseTaskForSessionWithDataset": "Choose the machine learning task for your session with dataset \"{{datasetName}}\".",
"configureModel": "Configure Model",
"configureOptimizer": "Configure Optimizer",
+ "configureSession": "Configure Session",
"configureTasksTrainCompareModels": "Configure tasks, train and compare models in organized sessions. Select a task to begin your modeling workflow.",
"customMetrics": "Custom Metrics",
"datasetPredictions": "Dataset Predictions",
@@ -95,6 +96,7 @@
"modelComparison": "Model Comparison",
"modelsModule": "Models Module",
"nameYourSession": "Name Your Session",
+ "selectDatasetAndPrepare": "Name your session, select a dataset and configure its columns and splits.",
"noCompatibleModelsFound": "No compatible models found",
"noDatasetPredictionsYet": "No dataset predictions yet",
"noGlobalExplainersYet": "No global explainers yet",
@@ -113,7 +115,9 @@
"predictions": "Predictions",
"predictionsCount_one": "• <1>{{count}}1> prediction",
"predictionsCount_other": "• <1>{{count}}1> predictions",
+ "divideColumnsAndSplits": "Divide columns and configure dataset splits",
"prepareDataset": "Prepare Dataset",
+ "selectDatasetFirst": "Select a dataset to configure its columns and row splits.",
"retrainConfirmDetails": "Are you sure you want to re-train run \"<1>{{runName}}1>\"?",
"retrainModel": "Re-train Model?",
"retrainWillDeleteOperations": "This run has existing operations that will be deleted",
@@ -162,7 +166,11 @@
"profile_regression_fit": "Model Fit",
"profile_regression_error": "Error Balanced",
"profile_translation_quality": "Translation Quality",
- "profile_translation_balanced": "Translation Balanced"
+ "profile_translation_balanced": "Translation Balanced",
+ "profile_text_balanced": "Balanced",
+ "profile_text_detectPositives": "Detect Positives",
+ "profile_text_avoidFalseAlarms": "Avoid False Alarms",
+ "profile_text_probabilityQuality": "Probability Quality"
},
"message": {
"allRunsCompleted": "{{experiment}} has completed all its runs.",
diff --git a/DashAI/front/src/utils/i18n/locales/es/common.json b/DashAI/front/src/utils/i18n/locales/es/common.json
index d9a4f55af..86cf2af56 100644
--- a/DashAI/front/src/utils/i18n/locales/es/common.json
+++ b/DashAI/front/src/utils/i18n/locales/es/common.json
@@ -215,5 +215,7 @@
"expand": "Expandir",
"optimize": "Optimizar",
"viewMore": "Ver más",
- "viewLess": "Ver menos"
+ "viewLess": "Ver menos",
+ "switchToDarkMode": "Cambiar a modo oscuro",
+ "switchToLightMode": "Cambiar a modo claro"
}
diff --git a/DashAI/front/src/utils/i18n/locales/es/custom.json b/DashAI/front/src/utils/i18n/locales/es/custom.json
index a9da76dfa..3b26167f3 100644
--- a/DashAI/front/src/utils/i18n/locales/es/custom.json
+++ b/DashAI/front/src/utils/i18n/locales/es/custom.json
@@ -1,5 +1,14 @@
{
"inferenceMethods": "Métodos de Inferencia",
"selectAnItemToShowInfo": "Seleccione un elemento para ver la descripción.",
- "selectInferenceMethods": "Seleccione los métodos de inferencia que desea aplicar"
+ "selectInferenceMethods": "Seleccione los métodos de inferencia que desea aplicar",
+ "search": "Buscar",
+ "noItemsFound": "No se encontraron componentes",
+ "tryAdjustingSearch": "Intenta ajustar tu búsqueda o filtros",
+ "componentsAvailable_one": "{{count}} componente disponible",
+ "componentsAvailable_other": "{{count}} componentes disponibles",
+ "componentDetails": "Detalles del Componente",
+ "description": "Descripción",
+ "tags": "Etiquetas",
+ "noDescriptionAvailable": "No hay descripción disponible."
}
diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json
index c44881eeb..27b22f00c 100644
--- a/DashAI/front/src/utils/i18n/locales/es/datasets.json
+++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json
@@ -200,7 +200,7 @@
"ifYourDatasetHaveSplits": "Si su dataset tiene divisiones, súbalo como un archivo zip",
"indices": "Índices (separados por comas, o escriba 'all')",
"inferenceRows": "Filas de Inferencia",
- "inferenceRowsDescription": "Número de filas utilizadas para vista previa/inferencia de tipo (anulación rápida).",
+ "inferenceRowsDescription": "Número de filas utilizadas para vista previa/inferencia de tipo (mínimo 2).",
"inferredConfiguration": "Configuración de Inferencia de Tipo",
"insightConstantColumn": "Esta columna tiene un solo valor único. No aporta información para el análisis.",
"insightHighCardinality": "Esta columna categórica tiene más de 100 valores únicos. Considera agrupar o codificar.",
diff --git a/DashAI/front/src/utils/i18n/locales/es/datasetsTour.json b/DashAI/front/src/utils/i18n/locales/es/datasetsTour.json
index 7daefc386..87a090a71 100644
--- a/DashAI/front/src/utils/i18n/locales/es/datasetsTour.json
+++ b/DashAI/front/src/utils/i18n/locales/es/datasetsTour.json
@@ -5,6 +5,7 @@
"dataLoaderConfig": "<0><0>Configuración del DataLoader0><1>Aquí puedes configurar cómo se debe cargar el dataset:1><2><0><0>Nombre:0> Dale a tu dataset un nombre significativo0><1><0>Separador:0> El carácter que separa los valores (coma para CSV)1><2><0>Otras opciones:0> Configuraciones avanzadas para necesidades específicas2>2><3>💡 <0>Consejo:0> La configuración predeterminada funciona bien para la mayoría de los archivos CSV, ¡así que generalmente puedes dejarla como está!3>0>",
"datasetModule": "<0><0>Módulo de Datasets0><1>El módulo de Datasets permite gestionar datos y crear Cuadernos interactivos para análisis. ¡Veamos cómo empezar!1>0>",
"downloadSample": "<0><0>Descargar Dataset de Ejemplo0><1>Para comenzar rápidamente, descarguemos un dataset de ejemplo.1><2><0>Descargar Personality_Dataset.csv0>2><3>💡 <0>Consejo:0> El archivo se guardará en tu carpeta de Descargas de forma predeterminada.3><4>¡Una vez descargado, haz clic en \"Siguiente\" para aprender a cargarlo!4>0>",
+ "notebookDatasetSelection": "<0><0>Seleccionar un Dataset0><1>Elige el dataset que deseas explorar en este Cuaderno. El dataset vinculado se usará para todas las visualizaciones y transformaciones.1>0>",
"finishProcess": "<0><0>Finalizar el Proceso0><1>Haz clic en \"Crear Cuaderno\" para comenzar a trabajar con tus datos en un entorno interactivo.1><2>Podrás visualizar, transformar y preparar tus datos para el modelado.2>0>",
"importantNote": "<0><0>Nota Importante0><1>Presta atención a esta información1><2>Esto asegura que tus datos originales permanezcan intactos mientras experimentas.2>0>",
"nextSteps": "<0><0>Próximos Pasos: Crear un Cuaderno0><1>Ahora haz clic en \"Nuevo Cuaderno\" para abrir el dataset en un entorno interactivo.1><2>En un Cuaderno, puedes analizar, visualizar y transformar tus datos.2>0>",
diff --git a/DashAI/front/src/utils/i18n/locales/es/experiments.json b/DashAI/front/src/utils/i18n/locales/es/experiments.json
index d1ea6cace..eb6fb956f 100644
--- a/DashAI/front/src/utils/i18n/locales/es/experiments.json
+++ b/DashAI/front/src/utils/i18n/locales/es/experiments.json
@@ -60,10 +60,19 @@
"parameterModification": "Modificación de Parámetros",
"prepareDataset": "Preparar dataset",
"recommendPreprocessMissingValues": "Se recomienda preprocesar el dataset para manejar estos valores faltantes antes de entrenar un modelo.",
+ "manual": "Manual",
+ "predefined": "Predefinido",
+ "random": "Aleatorio",
+ "rowIndexes": "Índices de filas",
+ "rowIndexesDescription": "Especifica rangos de filas para cada partición usando valores separados por comas o rangos (ej. 0-100, 200).",
"seed": "Semilla",
+ "splits": "Particiones",
+ "splitsDescription": "Proporción de datos asignada a cada subconjunto. Los valores deben estar entre 0 y 1 y sumar 1.",
+ "stratifyRequiresShuffle": "Requiere que barajar esté activado",
"selectDataset": "Seleccionar dataset",
"selectDatasetColumns": "Indique qué columnas del dataset se usarán como entrada y salida.",
"selectDatasetTitle": "Seleccione un dataset para la tarea seleccionada",
+ "splitType": "Tipo de partición",
"selectHowToDivideDataset": "Seleccione cómo dividir el dataset en subconjuntos de entrenamiento, validación y prueba.",
"selectInputOutputColumnsDescription": "Seleccione nombres de columnas de las listas.",
"selectMetrics": "Seleccionar métricas",
diff --git a/DashAI/front/src/utils/i18n/locales/es/generative.json b/DashAI/front/src/utils/i18n/locales/es/generative.json
index cd94325cb..f4a8ff980 100644
--- a/DashAI/front/src/utils/i18n/locales/es/generative.json
+++ b/DashAI/front/src/utils/i18n/locales/es/generative.json
@@ -11,6 +11,7 @@
"failedToFetchSessionInfo": "Error al obtener información de la sesión",
"failedToFetchSessions": "Error al obtener sesiones",
"failedToFetchTasks": "Error al obtener tareas generativas",
+ "failedToLoadModels": "Error al cargar los modelos",
"failedToUpdateSession": "Error al actualizar la sesión",
"nameRequired": "Se requiere un nombre",
"processError": "El proceso ha fallado. Eliminándolo... {{error}}",
@@ -24,7 +25,16 @@
"showLess": "Mostrar menos",
"readMore": "Leer más",
"generativeModule": "Módulo Generativo",
+ "createNewSession": "Crear una Nueva Sesión",
+ "createNewSessionDescription": "Elige un modelo, configura sus parámetros y empieza a conversar.",
+ "configureSession": "Configurar sesión",
+ "modelHasNoParameters": "Este modelo no tiene parámetros configurables.",
+ "nameAndDescribeYourSession": "Nombra tu sesión. Ajusta los parámetros a la derecha. Puedes cambiarlos en cualquier momento durante la sesión.",
"nameYourSession": "Nombre su sesión",
+ "newSession": "Nueva Sesión",
+ "pickAModelGroupedByTask": "Elige un modelo. Los modelos están agrupados por tarea.",
+ "searchModels": "Buscar modelos...",
+ "startBySelectingATask": "Comienza seleccionando una tarea generativa.",
"noSessionsFound": "No se encontraron sesiones",
"parameterChangeEvent": "Parámetros actualizados: <1>1>",
"parameterChangeHistory": "Historial de cambios de parámetros para la sesión actual",
@@ -45,9 +55,9 @@
"attachVideo": "Adjuntar video",
"attachMedia": "Adjuntar medio",
"attachMediaToContinue": "Adjunta medios para continuar",
- "noInputAvailable": "No hay entrada disponible para esta tarea"
+ "noInputAvailable": "No hay entrada disponible para esta tarea",
+ "selectModelCrumb": "Select Model"
},
-
"message": {
"sessionCreatedSuccess": "Sesión creada exitosamente.",
"sessionDeleted": "Sesión eliminada exitosamente",
diff --git a/DashAI/front/src/utils/i18n/locales/es/models.json b/DashAI/front/src/utils/i18n/locales/es/models.json
index d17b19c95..e0655c7b8 100644
--- a/DashAI/front/src/utils/i18n/locales/es/models.json
+++ b/DashAI/front/src/utils/i18n/locales/es/models.json
@@ -66,6 +66,7 @@
"chooseTaskForSessionWithDataset": "Elija la tarea de aprendizaje automático para su sesión con el dataset \"{{datasetName}}\".",
"configureModel": "Configurar Modelo",
"configureOptimizer": "Configurar Optimizador",
+ "configureSession": "Configurar Sesión",
"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",
@@ -95,6 +96,7 @@
"modelComparison": "Comparación de Modelos",
"modelsModule": "Módulo de Modelos",
"nameYourSession": "Nombre su Sesión",
+ "selectDatasetAndPrepare": "Nombra tu sesión, selecciona un dataset y configura sus columnas y particiones.",
"noCompatibleModelsFound": "No se encontraron modelos compatibles",
"noDatasetPredictionsYet": "Aún no hay predicciones de dataset",
"noGlobalExplainersYet": "Aún no hay explicadores globales",
@@ -113,7 +115,9 @@
"predictions": "Predicciones",
"predictionsCount_one": "• <1>{{count}}1> predicción",
"predictionsCount_other": "• <1>{{count}}1> predicciones",
+ "divideColumnsAndSplits": "Divide columnas y configura las particiones del dataset",
"prepareDataset": "Preparar Dataset",
+ "selectDatasetFirst": "Selecciona un dataset para configurar sus columnas y particiones de filas.",
"retrainConfirmDetails": "¿Está seguro de que desea re-entrenar la ejecución \"<1>{{runName}}1>\"?",
"retrainModel": "¿Re-entrenar Modelo?",
"retrainWillDeleteOperations": "Esta ejecución tiene operaciones existentes que serán eliminadas",
@@ -162,7 +166,11 @@
"profile_regression_fit": "Ajuste del Modelo",
"profile_regression_error": "Error Balanceado",
"profile_translation_quality": "Calidad de Traducción",
- "profile_translation_balanced": "Traducción Balanceada"
+ "profile_translation_balanced": "Traducción Balanceada",
+ "profile_text_balanced": "Equilibrio",
+ "profile_text_detectPositives": "Detectar Positivos",
+ "profile_text_avoidFalseAlarms": "Evitar Falsas Alarmas",
+ "profile_text_probabilityQuality": "Calidad de Probabilidades"
},
"message": {
"allRunsCompleted": "{{experiment}} ha completado todas sus ejecuciones.",
diff --git a/docs/docs/build/contributing.md b/docs/docs/build/contributing.md
index 977672512..5d361a701 100644
--- a/docs/docs/build/contributing.md
+++ b/docs/docs/build/contributing.md
@@ -15,7 +15,7 @@ The easiest way to extend DashAI is by building and publishing a plugin — a se
package that adds new models, tasks, metrics, explorers, or other components to the
platform without touching the core codebase.
-See the [Plugin Development](./plugin-development/overview) guide to get started.
+See the [Plugin Development](/build/plugin-development/overview) guide to get started.
## Fork and Submit a Pull Request
@@ -23,7 +23,7 @@ To contribute code directly to the core project:
1. Fork the [DashAI repository on GitHub](https://github.com/DashAISoftware/DashAI).
2. Create a branch for your change.
-3. Follow the [Dev Setup](./dev-setup) and [Testing](./testing) guides to run the project
+3. Follow the [Dev Setup](/build/dev-setup) and [Testing](/build/testing) guides to run the project
locally and verify your changes.
4. Open a pull request against the `develop` branch with a clear description of what
you changed and why.
diff --git a/docs/docs/build/plugin-development/develop.md b/docs/docs/build/plugin-development/develop.md
index 0a23efa9d..59a7b2397 100644
--- a/docs/docs/build/plugin-development/develop.md
+++ b/docs/docs/build/plugin-development/develop.md
@@ -9,8 +9,8 @@ sidebar_label: Developing a Plugin
Before developing a plugin, ensure you understand:
-1. **[What is a Plugin?](./overview)** — High-level overview of plugin concepts and capabilities
-2. **[Plugin Structure](./structure)** — How plugins are organized and what DashAI requires
+1. **[What is a Plugin?](/build/plugin-development/overview)** — High-level overview of plugin concepts and capabilities
+2. **[Plugin Structure](/build/plugin-development/structure)** — How plugins are organized and what DashAI requires
---
@@ -82,7 +82,7 @@ class MyCustomModel(TabularClassificationModel):
1. Create the plugin folder inside the `plugins` directory (if using Option 2 above)
2. Write your Python classes extending appropriate DashAI base classes
3. Create any required JSON configuration files
-4. Add the `pyproject.toml` with proper entry points (see [Plugin Structure](./structure))
+4. Add the `pyproject.toml` with proper entry points (see [Plugin Structure](/build/plugin-development/structure))
5. Write a README describing your plugin
---
@@ -93,7 +93,7 @@ class MyCustomModel(TabularClassificationModel):
Study working examples to understand best practices:
-- **[Real-world example](./overview):** `dashai-phi-model-package` adds Microsoft Phi models
+- **[Real-world example](/build/plugin-development/overview):** `dashai-phi-model-package` adds Microsoft Phi models
- **Community plugins:** [pypi.org/search/?q=dashai](https://pypi.org/search/?q=dashai)
### Test Your Plugin During Development
diff --git a/docs/docs/build/plugin-development/structure.md b/docs/docs/build/plugin-development/structure.md
index 96e747f6d..a4aefe540 100644
--- a/docs/docs/build/plugin-development/structure.md
+++ b/docs/docs/build/plugin-development/structure.md
@@ -7,7 +7,7 @@ sidebar_label: Plugin Structure
## Overview
-If you haven't already, start with [What is a Plugin?](./overview) to understand the core concept.
+If you haven't already, start with [What is a Plugin?](/build/plugin-development/overview) to understand the core concept.
This page details how plugins are structured, what files and configurations are required, and how DashAI discovers and loads them.
@@ -84,6 +84,6 @@ keywords = [
## Next Steps
-- See [Developing a Plugin](./develop) for step-by-step implementation guidance
-- Check [Plugin Overview](./overview) for a complete working example
-- Learn how to [upload your plugin to PyPI](./upload)
+- See [Developing a Plugin](/build/plugin-development/develop) for step-by-step implementation guidance
+- Check [Plugin Overview](/build/plugin-development/overview) for a complete working example
+- Learn how to [upload your plugin to PyPI](/build/plugin-development/upload)
diff --git a/docs/docs/build/plugin-development/upload.md b/docs/docs/build/plugin-development/upload.md
index 1a5cb2fa9..5f9a90eb3 100644
--- a/docs/docs/build/plugin-development/upload.md
+++ b/docs/docs/build/plugin-development/upload.md
@@ -11,8 +11,8 @@ Once your plugin is developed and tested, you can share it with the DashAI commu
Before uploading, ensure you have completed:
-1. **[Plugin Structure](./structure)** — Your plugin has the correct folder and configuration format
-2. **[Developing a Plugin](./develop)** — Your plugin is fully implemented and tested locally
+1. **[Plugin Structure](/build/plugin-development/structure)** — Your plugin has the correct folder and configuration format
+2. **[Developing a Plugin](/build/plugin-development/develop)** — Your plugin is fully implemented and tested locally
---
diff --git a/docs/docs/deep-dive/architecture.md b/docs/docs/deep-dive/architecture.md
index 4c796be59..f3df7b444 100644
--- a/docs/docs/deep-dive/architecture.md
+++ b/docs/docs/deep-dive/architecture.md
@@ -46,13 +46,13 @@ so that API endpoints can receive them automatically.
## Further Reading
-| Topic | Page |
-| ---------------------------------------------------- | ---------------------------------------- |
-| REST API structure, router map, dependency injection | [API](./api) |
-| Component types, registry, and configurable objects | [Components](./components) |
-| SQLite schema, ORM tables, and data storage | [Database](./database) |
-| Huey job queue and job types | [Job System](./job-system) |
-| Notebook sessions, explorers, and converters | [Notebook](./notebook) |
-| End-to-end training and exploration walkthroughs | [Workflow Examples](./workflow-examples) |
-| Column semantic types and inference | [Semantic Types](./semantic-types) |
-| Core dataset primitive, splits, and data lifecycle | [DashAIDataset](./dashai-dataset) |
+| Topic | Page |
+| ---------------------------------------------------- | ------------------------------------------------- |
+| REST API structure, router map, dependency injection | [API](/deep-dive/api) |
+| Component types, registry, and configurable objects | [Components](/deep-dive/components) |
+| SQLite schema, ORM tables, and data storage | [Database](/deep-dive/database) |
+| Huey job queue and job types | [Job System](/deep-dive/job-system) |
+| Notebook sessions, explorers, and converters | [Notebook](/deep-dive/notebook) |
+| End-to-end training and exploration walkthroughs | [Workflow Examples](/deep-dive/workflow-examples) |
+| Column semantic types and inference | [Semantic Types](/deep-dive/semantic-types) |
+| Core dataset primitive, splits, and data lifecycle | [DashAIDataset](/deep-dive/dashai-dataset) |
diff --git a/docs/docs/deep-dive/dashai-dataset.md b/docs/docs/deep-dive/dashai-dataset.md
index 47ea64e15..400c832f1 100644
--- a/docs/docs/deep-dive/dashai-dataset.md
+++ b/docs/docs/deep-dive/dashai-dataset.md
@@ -10,7 +10,7 @@ sidebar_position: 9
`DashAIDataset` is DashAI's core dataset primitive. It extends the HuggingFace `Dataset` class with two additional responsibilities:
-- **Semantic type metadata** — a `_types` dictionary (`Dict[str, DashAIDataType]`) that maps every column name to its DashAI semantic type (see [Semantic Types](./semantic-types)). This metadata is persisted inside the Apache Arrow schema so it survives save/load round-trips.
+- **Semantic type metadata** — a `_types` dictionary (`Dict[str, DashAIDataType]`) that maps every column name to its DashAI semantic type (see [Semantic Types](/deep-dive/semantic-types)). This metadata is persisted inside the Apache Arrow schema so it survives save/load round-trips.
- **Split metadata** — a `splits` dictionary that records which row indices belong to which split (`train`, `test`, `validation`), plus aggregate statistics computed during upload.
Every piece of data that flows through DashAI — upload, notebook transformations, model training, predictions — is represented as a `DashAIDataset`.
diff --git a/docs/docs/deep-dive/replicability.md b/docs/docs/deep-dive/replicability.md
index 3c33ce911..ba1f68e2e 100644
--- a/docs/docs/deep-dive/replicability.md
+++ b/docs/docs/deep-dive/replicability.md
@@ -46,7 +46,7 @@ Full pipeline export (a portable "recipe" capturing the full preprocessing + mod
:::info What is a Notebook?
A Notebook in DashAI is a working session with a mutable copy of a dataset. It groups
Explorers (visualizations) and Converters (transformations) applied to that copy. The
-original dataset is never modified. See [System Design → Notebook](./notebook)
+original dataset is never modified. See [System Design → Notebook](/deep-dive/notebook)
for details.
:::
diff --git a/docs/docs/learn/tutorials/Models/overview.md b/docs/docs/learn/tutorials/Models/overview.md
index 6b76da9a0..da77c3ed0 100644
--- a/docs/docs/learn/tutorials/Models/overview.md
+++ b/docs/docs/learn/tutorials/Models/overview.md
@@ -77,11 +77,11 @@ Other tasks have their own corresponding model catalogs.
This section is divided into the following pages:
-- **[Train a Model](./train)** — How to create a session, configure input/output columns,
+- **[Train a Model](/learn/tutorials/Models/train)** — How to create a session, configure input/output columns,
define data splits, add models, set hyperparameters, and run training.
-- **[Predictions](./predictions)** — How to generate predictions using trained models,
+- **[Predictions](/learn/tutorials/Models/predictions)** — How to generate predictions using trained models,
both from a full dataset and from manually entered data.
-- **[Explainability](./explainability)** — How to use global and local explainers to
+- **[Explainability](/learn/tutorials/Models/explainability)** — How to use global and local explainers to
understand model behavior.
-- **[Model Comparison](./comparison)** — How to compare metrics across models using
+- **[Model Comparison](/learn/tutorials/Models/comparison)** — How to compare metrics across models using
tables and charts.
diff --git a/docs/docs/learn/tutorials/Models/train.md b/docs/docs/learn/tutorials/Models/train.md
index 38b88c86f..b86656274 100644
--- a/docs/docs/learn/tutorials/Models/train.md
+++ b/docs/docs/learn/tutorials/Models/train.md
@@ -190,12 +190,12 @@ Metrics are only available after training is complete. If a model shows
### EXPLAINABILITY
Shows global and local explainers attached to this model. See the
-[Explainability](./explainability) page for details.
+[Explainability](/learn/tutorials/Models/explainability) page for details.
### PREDICTIONS
Shows dataset predictions and manual predictions generated from this model. See the
-[Predictions](./predictions) page for details.
+[Predictions](/learn/tutorials/Models/predictions) page for details.
### HYPERPARAMETERS
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/contributing.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/contributing.md
new file mode 100644
index 000000000..b6a22a30d
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/contributing.md
@@ -0,0 +1,44 @@
+---
+title: Contribuir
+sidebar_label: Contribuir
+sidebar_position: 5
+---
+
+# Contribuir a DashAI
+
+Hay varias formas de contribuir a DashAI, independientemente de tu experiencia o
+nivel de conocimiento.
+
+## Subir un Plugin
+
+La forma más sencilla de extender DashAI es construir y publicar un plugin — un
+paquete autocontenido que agrega nuevos modelos, tareas, métricas, exploradores u
+otros componentes a la plataforma sin modificar el código central.
+
+Consulta la guía de [Desarrollo de Plugins](/build/plugin-development/overview) para comenzar.
+
+## Hacer un Fork y Enviar un Pull Request
+
+Para contribuir código directamente al proyecto central:
+
+1. Haz un fork del [repositorio DashAI en GitHub](https://github.com/DashAISoftware/DashAI).
+2. Crea una rama para tu cambio.
+3. Sigue las guías de [Configuración de Desarrollo](/build/dev-setup) y [Testing](/build/testing) para
+ ejecutar el proyecto localmente y verificar tus cambios.
+4. Abre un pull request contra la rama `develop` con una descripción clara de qué
+ cambiaste y por qué.
+
+Por favor, verifica si existe un issue relacionado o abre uno antes de comenzar
+trabajo significativo, para evitar esfuerzo duplicado.
+
+## Reportar un Problema
+
+¿Encontraste un bug o tienes una solicitud de funcionalidad? Abre un issue en el
+[rastreador de issues de GitHub](https://github.com/DashAISoftware/DashAI/issues). Incluye la mayor
+cantidad de detalle posible — pasos para reproducir, comportamiento esperado vs. real, y tu
+entorno.
+
+## Contáctanos
+
+Para preguntas, propuestas o cualquier otra consulta, puedes contactar al equipo a través
+del formulario de contacto en [dash-ai.com](https://www.dash-ai.com/).
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/dev-setup.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/dev-setup.md
new file mode 100644
index 000000000..3eedf7e1b
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/dev-setup.md
@@ -0,0 +1,126 @@
+---
+title: Configuración de Desarrollo
+sidebar_label: Configuración de Desarrollo
+sidebar_position: 2
+---
+
+# Configuración de Desarrollo
+
+## Requisitos Previos
+
+- Python 3.10 a 3.13
+- Node.js (LTS) y Yarn 3.5.0
+- Git
+
+## 1. Clonar el Repositorio
+
+```bash
+git clone https://github.com/DashAISoftware/DashAI.git
+cd DashAI
+git checkout develop
+```
+
+## 2. Configuración del Backend
+
+Crea y activa un entorno de Python (conda o venv):
+
+```bash
+conda create -n dashai python=3.10
+conda activate dashai
+```
+
+Instala el paquete en modo editable con las dependencias de desarrollo:
+
+```bash
+pip install -r requirements.txt
+pip install -e .
+pip install -r requirements-dev.txt
+pre-commit install
+```
+
+## 3. Configuración del Frontend
+
+```bash
+cd DashAI/front
+yarn install
+```
+
+## Ejecutar en Desarrollo
+
+**Backend** (desde la raíz del repositorio):
+
+```bash
+python -m DashAI
+# o
+dashai --no-browser --logging-level INFO
+```
+
+**Frontend** (servidor de desarrollo con recarga en caliente):
+
+```bash
+cd DashAI/front
+yarn start
+```
+
+El backend corre en `http://localhost:8000` y el servidor de desarrollo del frontend en `http://localhost:3000`.
+
+## Linting y Formateo
+
+**Python** (usando Ruff):
+
+```bash
+ruff check . --fix
+ruff format .
+```
+
+**Frontend** (ESLint + Prettier):
+
+```bash
+cd DashAI/front
+yarn lint
+```
+
+## Hooks de Pre-commit
+
+DashAI usa hooks de pre-commit para mantener la calidad del código:
+
+```bash
+# Ejecutar todos los hooks manualmente
+pre-commit run --all-files
+
+# Ejecutar sobre archivos en staging (ocurre automáticamente en git commit)
+pre-commit run
+```
+
+## Estructura del Proyecto
+
+```
+DashAI/
+├── DashAI/
+│ ├── __main__.py # Punto de entrada CLI (Typer)
+│ ├── back/ # Backend FastAPI
+│ │ ├── app.py # Application factory
+│ │ ├── container.py # Kink DI container
+│ │ ├── initial_components.py # Registro de componentes al iniciar
+│ │ ├── api/ # Routers y schemas de request/response
+│ │ ├── converters/ # Componentes Converter
+│ │ ├── dataloaders/ # Componentes DataLoader
+│ │ ├── dependencies/ # Registry, motor de base de datos, cola de jobs
+│ │ ├── explainability/ # Componentes Explainer
+│ │ ├── exploration/ # Componentes Explorer
+│ │ ├── job/ # Implementaciones de Jobs
+│ │ ├── metrics/ # Componentes Metric
+│ │ ├── models/ # Componentes de modelos ML
+│ │ ├── optimizers/ # Componentes optimizadores de hiperparámetros
+│ │ ├── plugins/ # Sistema de carga de plugins
+│ │ ├── tasks/ # Componentes Task
+│ │ └── types/ # Definiciones de tipos compartidos
+│ └── front/ # Frontend React
+│ └── src/
+│ ├── api/ # Cliente HTTP
+│ ├── components/ # Componentes de UI
+│ └── pages/ # Componentes de páginas
+├── docs/ # Sitio de documentación (Docusaurus)
+├── tests/ # Tests del backend
+└── alembic/ # Migraciones de base de datos
+```
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/migrations.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/migrations.md
new file mode 100644
index 000000000..d5b5ddc0a
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/migrations.md
@@ -0,0 +1,67 @@
+---
+title: Migraciones de Base de Datos
+sidebar_label: Migraciones de Base de Datos
+sidebar_position: 3
+---
+
+# Migraciones de Base de Datos
+
+DashAI usa **Alembic** para las migraciones del esquema de base de datos. Las migraciones se ejecutan automáticamente al iniciar la aplicación.
+
+## Aplicar Migraciones
+
+Para ejecutar todas las migraciones pendientes manualmente (desde el directorio `DashAI/`):
+
+```bash
+alembic upgrade head
+```
+
+## Crear una Nueva Migración
+
+Después de modificar los modelos SQLAlchemy en `back/dependencies/database/`:
+
+```bash
+alembic revision --autogenerate -m "description of changes"
+```
+
+El archivo de migración generado se guarda en `alembic/versions/` y debe ser incluido en el repositorio.
+
+:::tip
+Siempre revisa el archivo de migración generado automáticamente antes de aplicarlo. Alembic no siempre detecta correctamente cambios complejos (como renombrado de columnas o cambios en restricciones).
+:::
+
+## Revertir
+
+Revertir la migración más reciente:
+
+```bash
+alembic downgrade -1
+```
+
+Revertir a una revisión específica:
+
+```bash
+alembic downgrade
+```
+
+## Verificar Estado
+
+```bash
+# Migración aplicada actualmente
+alembic current
+
+# Historial completo de migraciones
+alembic history
+```
+
+## Verificaciones en CI
+
+El pipeline de CI ejecuta verificaciones de migraciones en cada PR:
+
+- `alembic upgrade head` — verificar que la migración se aplica correctamente
+- `alembic downgrade -1` / `alembic upgrade head` — verificar reversibilidad
+- Verificación de consistencia del esquema — verificar que el esquema final coincide con los modelos SQLAlchemy
+
+## Ubicación de la Base de Datos
+
+La base de datos principal se almacena en `~/.DashAI/db.sqlite`. La cola de jobs usa una base de datos separada en `~/.DashAI/job_queue.db`.
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/develop.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/develop.md
new file mode 100644
index 000000000..773c3dc0b
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/develop.md
@@ -0,0 +1,132 @@
+---
+title: Desarrollar un Plugin
+sidebar_label: Desarrollar un Plugin
+---
+
+# Desarrollar un Plugin
+
+## Requisitos Previos
+
+Antes de desarrollar un plugin, asegúrate de entender:
+
+1. **[¿Qué es un Plugin?](/build/plugin-development/overview)** — Visión general de los conceptos y capacidades de los plugins
+2. **[Estructura de un Plugin](/build/plugin-development/structure)** — Cómo se organizan los plugins y qué requiere DashAI
+
+---
+
+## Configura tu Entorno de Desarrollo
+
+Para crear un plugin, necesitas acceso a las clases Python de DashAI. Hay dos enfoques:
+
+### Opción 1: DashAI como Biblioteca Instalada
+
+Instala DashAI como paquete en tu entorno virtual de Python:
+
+```bash
+pip install dashai
+```
+
+**Nota:** Este enfoque es más sencillo pero limita tu capacidad de probar el plugin interactivamente dentro de DashAI.
+
+### Opción 2: Clonar el Repositorio (Recomendado)
+
+Clona el repositorio de DashAI para obtener capacidades completas de desarrollo y prueba:
+
+```bash
+git clone https://github.com/DashAISoftware/DashAI.git
+cd DashAI
+```
+
+Crea una carpeta `plugins` en la raíz del repositorio para el desarrollo de tu plugin:
+
+```text
+DashAI/
+├── DashAI/
+├── plugins/ ← Crea esta carpeta
+├── tests/
+├── ...
+```
+
+Este enfoque te permite probar tu plugin en tiempo real mientras lo desarrollas.
+
+---
+
+## Paso 1: Identifica Qué Quieres Construir
+
+Los plugins pueden extender DashAI con:
+
+- **Modelos de Machine Learning** (`TabularClassificationModel`, `RegressionModel`, `TextGenerationModel`, etc.)
+- **Data Loaders** (soporte para formatos de dataset adicionales)
+- **Data Converters** (preprocesamiento, ingeniería de características, transformaciones)
+- **Explorers** (herramientas de visualización y análisis de datos)
+- **Explainers** (herramientas de interpretabilidad de modelos)
+- **Tareas Personalizadas** (nuevos tipos de problemas de ML)
+- **Métricas Personalizadas** (métricas de evaluación)
+
+Para garantizar la compatibilidad, las clases de tu plugin **deben extender la clase base apropiada de DashAI**:
+
+```python
+# Ejemplo: Crear un modelo de clasificación tabular
+from DashAI.back.models.tabular_classification_model import TabularClassificationModel
+
+class MyCustomModel(TabularClassificationModel):
+ def train(self, X, y):
+ # Tu lógica de entrenamiento
+ pass
+```
+
+---
+
+## Paso 2: Implementa tu Plugin
+
+1. Crea la carpeta del plugin dentro del directorio `plugins` (si usas la Opción 2)
+2. Escribe tus clases Python extendiendo las clases base apropiadas de DashAI
+3. Crea los archivos de configuración JSON necesarios
+4. Agrega el `pyproject.toml` con los entry points correctos (ver [Estructura de un Plugin](/build/plugin-development/structure))
+5. Escribe un README describiendo tu plugin
+
+---
+
+## Recomendaciones
+
+### Revisa Plugins Existentes
+
+Estudia ejemplos funcionales para entender las mejores prácticas:
+
+- **[Ejemplo real](/build/plugin-development/overview):** `dashai-phi-model-package` agrega los modelos Microsoft Phi
+- **Plugins de la comunidad:** [pypi.org/search/?q=dashai](https://pypi.org/search/?q=dashai)
+
+### Prueba tu Plugin Durante el Desarrollo
+
+1. Crea los archivos Python y JSON localmente (Opción 2: clonar el repositorio)
+2. Colócalos en la carpeta `/plugins`
+3. Inicia DashAI y verifica que tus componentes aparecen y funcionan correctamente
+4. Itera hasta estar seguro de la implementación
+
+### Verifica Conflictos de Dependencias
+
+Después de instalar nuevas dependencias, verifica que no existan conflictos:
+
+```bash
+pip check
+```
+
+**Sin conflictos:**
+
+```text
+No broken requirements found.
+```
+
+**Con conflictos:**
+
+```text
+fastapi 0.106.0 has requirement starlette<0.28.0,>=0.27.0, but you have starlette 0.20.0.
+```
+
+Resuelve cualquier conflicto antes de publicar tu plugin.
+
+**Es importante que al instalar una nueva librería para un plugin o paquete, esta sea incluida entre las dependencias del plugin o paquete.**
+
+5. **Usa prints en el flujo de los componentes agregados**
+
+ Incluir prints en el flujo del componente que creaste puede ser muy útil para verificar el correcto funcionamiento del componente en el software.
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/overview.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/overview.md
new file mode 100644
index 000000000..421825a57
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/overview.md
@@ -0,0 +1,215 @@
+---
+title: Visión General de Plugins
+sidebar_label: Visión General de Plugins
+---
+
+# ¿Qué es un Plugin?
+
+Un **plugin** es un paquete de extensión que agrega nueva funcionalidad a DashAI sin modificar la aplicación central. Los plugins te permiten:
+
+- Agregar nuevos **modelos de Machine Learning** (clasificación, regresión, generación, etc.)
+- Crear **data loaders** personalizados para soportar formatos de dataset adicionales
+- Implementar nuevos **data converters** para preprocesamiento y transformación
+- Agregar **explorers** especializados para análisis de datos
+- Desarrollar **explainers** personalizados para la interpretabilidad de modelos
+- Extender **tareas** para soportar nuevos tipos de problemas de ML
+- Definir **métricas** personalizadas para evaluación
+
+Los plugins se distribuyen como paquetes Python en [PyPI](https://pypi.org) y son descubiertos e instalados automáticamente por DashAI cuando usas el módulo **Plugins**. Esto facilita que la comunidad extienda DashAI con funcionalidades específicas de dominio y experimentales sin esperar lanzamientos oficiales.
+
+:::tip Convención de Nombres para Plugins
+Todos los plugins de DashAI **deben** usar el prefijo `dashai-` en el nombre de su paquete (ej. `dashai-my-model-package`) para que la aplicación pueda descubrirlos y cargarlos automáticamente. Ver más plugins de la comunidad: [pypi.org/search/?q=dashai](https://pypi.org/search/?q=dashai).
+:::
+
+---
+
+## Ejemplo Real: Modelos Microsoft Phi
+
+Aquí hay un ejemplo concreto de un plugin de DashAI en acción.
+
+**dashai-phi-model-package** agrega los modelos de lenguaje Microsoft Phi para generación de texto a DashAI.
+
+**Disponible en PyPI:** [dashai-phi-model-package](https://pypi.org/project/dashai-phi-model-package/)
+
+### Estructura del Paquete
+
+```bash
+dashai_phi_model_package/
+├── dashai_phi_model_package/
+│ ├── __init__.py
+│ └── phi_model.py
+├── pyproject.toml
+└── README.md
+```
+
+
+pyproject.toml
+
+```toml
+[project]
+name = "dashai_phi_model_package"
+version = "0.0.2"
+
+dependencies = ['llama-cpp-python>=0.2.90', 'huggingface-hub>=0.29.1']
+
+authors = [{ name = "DashAI team" }, { email = "dashaisoftware@gmail.com" }]
+
+keywords = ["DashAI", "Model"]
+
+description = "Phi Model for DashAI"
+readme = "README.md"
+requires-python = ">=3.8"
+
+[project.entry-points.'dashai.plugins']
+PhiModel = 'dashai_phi_model_package.phi_model:PhiModel'
+
+[project.urls]
+Homepage = "https://github.com/DashAISoftware/DashAI"
+Issues = "https://github.com/DashAISoftware/DashAI/issues"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+```
+
+
+
+La sección `[project.entry-points.'dashai.plugins']` es clave — le indica a DashAI qué clases registrar cuando el plugin está instalado. Cada entrada mapea un nombre arbitrario a una ruta de importación `module:ClassName`.
+
+
+phi_model.py
+
+```python
+from typing import List
+
+from llama_cpp import Llama
+
+from DashAI.back.core.schema_fields import (
+ BaseSchema,
+ enum_field,
+ float_field,
+ int_field,
+ schema_field,
+)
+from DashAI.back.models.hugging_face.llama_utils import is_gpu_available_for_llama_cpp
+from DashAI.back.models.text_to_text_generation_model import (
+ TextToTextGenerationTaskModel,
+)
+
+if is_gpu_available_for_llama_cpp():
+ DEVICE_ENUM = ["gpu", "cpu"]
+ DEVICE_PLACEHOLDER = "gpu"
+else:
+ DEVICE_ENUM = ["cpu"]
+ DEVICE_PLACEHOLDER = "cpu"
+
+
+class PhiSchema(BaseSchema):
+ """Schema for Phi model."""
+
+ model_name: schema_field(
+ enum_field(
+ enum=[
+ "microsoft/Phi-3-mini-4k-instruct-gguf",
+ "microsoft/phi-4-gguf",
+ ]
+ ),
+ placeholder="microsoft/Phi-3-mini-4k-instruct-gguf",
+ description="The specific Phi model version to use.",
+ ) # type: ignore
+
+ max_tokens: schema_field(
+ int_field(ge=1),
+ placeholder=100,
+ description="Maximum number of tokens to generate.",
+ ) # type: ignore
+
+ temperature: schema_field(
+ float_field(ge=0.0, le=1.0),
+ placeholder=0.7,
+ description=(
+ "Sampling temperature. Higher values make the output more random, while "
+ "lower values make it more focused and deterministic."
+ ),
+ ) # type: ignore
+
+ frequency_penalty: schema_field(
+ float_field(ge=0.0, le=2.0),
+ placeholder=0.1,
+ description=(
+ "Penalty for repeated tokens in the output. Higher values reduce the "
+ "likelihood of repetition, encouraging more diverse text generation."
+ ),
+ ) # type: ignore
+
+ context_window: schema_field(
+ int_field(ge=1),
+ placeholder=512,
+ description=(
+ "Maximum number of tokens the model can process in a single forward pass "
+ "(context window size)."
+ ),
+ ) # type: ignore
+
+ device: schema_field(
+ enum_field(enum=DEVICE_ENUM),
+ placeholder=DEVICE_PLACEHOLDER,
+ description="The device to use for model inference.",
+ ) # type: ignore
+
+
+class PhiModel(TextToTextGenerationTaskModel):
+ """Phi model for text generation using llama.cpp library."""
+
+ SCHEMA = PhiSchema
+
+ def __init__(self, **kwargs):
+ kwargs = self.validate_and_transform(kwargs)
+ self.model_name = kwargs.get(
+ "model_name", "microsoft/Phi-3-mini-4k-instruct-gguf"
+ )
+ self.max_tokens = kwargs.pop("max_tokens", 100)
+ self.temperature = kwargs.pop("temperature", 0.7)
+ self.frequency_penalty = kwargs.pop("frequency_penalty", 0.1)
+ self.n_ctx = kwargs.pop("context_window", 512)
+
+ model_filenames = {
+ "microsoft/Phi-3-mini-4k-instruct-gguf": "*4.gguf",
+ "microsoft/phi-4-gguf": "phi-4-IQ3_M.gguf",
+ }
+
+ self.filename = model_filenames.get(
+ self.model_name, "Phi-3-mini-4k-instruct-q4.gguf"
+ )
+
+ self.model = Llama.from_pretrained(
+ repo_id=self.model_name,
+ filename=self.filename,
+ verbose=True,
+ n_ctx=self.n_ctx,
+ n_gpu_layers=-1 if kwargs.get("device", "gpu") == "gpu" else 0,
+ )
+
+ def generate(self, prompt: list[dict[str, str]]) -> List[str]:
+ output = self.model.create_chat_completion(
+ messages=prompt,
+ max_tokens=self.max_tokens,
+ temperature=self.temperature,
+ frequency_penalty=self.frequency_penalty,
+ )
+
+ generated_text = output["choices"][0]["message"]["content"]
+ return [generated_text]
+```
+
+
+
+## Conceptos Clave Ilustrados
+
+| Concepto | Dónde buscar |
+| --------------------------------- | ------------------------------------------------------------ |
+| Registro de entry points | `pyproject.toml` → `[project.entry-points.'dashai.plugins']` |
+| Extender una clase base de DashAI | `PhiModel(TextToTextGenerationTaskModel)` |
+| Definir parámetros | `PhiSchema` con `schema_field()` |
+| Carga del modelo | `__init__` con `validate_and_transform` |
+| Generar salida | `generate()` retornando `List[str]` |
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/structure.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/structure.md
new file mode 100644
index 000000000..e1b22e172
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/structure.md
@@ -0,0 +1,89 @@
+---
+title: Estructura de un Plugin
+sidebar_label: Estructura de un Plugin
+---
+
+# Estructura de un Plugin
+
+## Visión General
+
+Si aún no lo hiciste, comienza con [¿Qué es un Plugin?](/build/plugin-development/overview) para entender el concepto central.
+
+Esta página detalla cómo se estructuran los plugins, qué archivos y configuraciones son necesarios, y cómo DashAI los descubre y carga.
+
+---
+
+## ¿De Qué se Compone un Plugin?
+
+Un plugin es un paquete Python con una estructura estandarizada. Consta de 4 partes principales:
+
+1. **src**: Esta carpeta contiene la carpeta con el **nombre del plugin o paquete**. Esto es importante para una correcta organización y descubrimiento.
+
+ La carpeta con el nombre del plugin o paquete debe contener todos los archivos **Python** y **JSON** necesarios para extender el software.
+
+2. **pyproject.toml**: Archivo de configuración. Determina cómo se crea el paquete, qué metadatos contiene y qué clases registrar como entry points del plugin.
+
+3. **readme.md**: Contiene la descripción extendida del paquete o plugin.
+
+4. **LICENSE**: Determina el tipo de licencia que tendrá el paquete.
+
+---
+
+## Estructura de Carpetas Recomendada
+
+```text
+dashai-my-plugin
+│ LICENSE
+│ pyproject.toml
+│ readme.md
+└───src
+ └───dashai_my_plugin
+ │ example_model.py
+ │ ExampleModel.json
+```
+
+---
+
+## Configuración Requerida
+
+Para que el software integre el plugin al instalarlo, debe cumplir los siguientes requisitos:
+
+### 1. Entry Points en pyproject.toml
+
+Tu **pyproject.toml** DEBE contener un **entrypoint** por cada clase Python que quieras agregar a DashAI:
+
+```toml
+[project.entry-points.'dashai.plugins']
+ExampleModel = 'dashai_my_plugin.example_model:ExampleModel'
+```
+
+Esto le indica a DashAI qué clases registrar y hacer disponibles en la UI.
+
+### 2. Sección de Keywords
+
+Tu **pyproject.toml** DEBE incluir una sección **keywords**. Estas etiquetas se muestran al presentar el plugin en el módulo **Plugins** de DashAI.
+
+Los únicos tags válidos son:
+
+```toml
+"DashAI", "Model", "Task", "Dataloaders", "Converter", "Explainer"
+```
+
+### Ejemplo de Sección Keywords
+
+```toml
+[project]
+keywords = [
+ "DashAI",
+ "Model",
+ "Dataloaders"
+]
+```
+
+---
+
+## Próximos Pasos
+
+- Ver [Desarrollar un Plugin](/build/plugin-development/develop) para una guía de implementación paso a paso
+- Revisar [Visión General de Plugins](/build/plugin-development/overview) para un ejemplo completo y funcional
+- Aprender cómo [subir tu plugin a PyPI](/build/plugin-development/upload)
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/upload.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/upload.md
new file mode 100644
index 000000000..8786ef0a1
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/plugin-development/upload.md
@@ -0,0 +1,129 @@
+---
+title: Subir un Plugin
+sidebar_label: Subir un Plugin
+---
+
+# Subir un Plugin a PyPI
+
+Una vez que tu plugin está desarrollado y probado, puedes compartirlo con la comunidad de DashAI en [PyPI](https://pypi.org/).
+
+## Requisitos Previos
+
+Antes de subir, asegúrate de haber completado:
+
+1. **[Estructura de un Plugin](/build/plugin-development/structure)** — Tu plugin tiene el formato correcto de carpetas y configuración
+2. **[Desarrollar un Plugin](/build/plugin-development/develop)** — Tu plugin está completamente implementado y probado localmente
+
+---
+
+## Publicar tu Plugin en PyPI
+
+Esta guía usa **twine** para subir tu paquete, aunque hay otros métodos disponibles.
+
+### Paso 1: Construir tu Paquete
+
+Instala las herramientas de build:
+
+```bash
+python -m pip install --upgrade build
+```
+
+Construye tu paquete plugin:
+
+```bash
+python -m build
+```
+
+Esto crea dos archivos de distribución en la carpeta `dist/`:
+
+```text
+dist/
+├── dashai_my_plugin-0.0.1-py3-none-any.whl
+└── dashai_my_plugin-0.0.1.tar.gz
+```
+
+### Paso 2: Obtener un Token de API de PyPI
+
+1. Crea una [cuenta en PyPI](https://pypi.org/account/register/) (si aún no tienes una)
+2. Ve a tu [página de tokens de API](https://pypi.org/manage/account/#api-tokens)
+3. Haz clic en "Add API token"
+4. Guarda el token en un lugar seguro (lo necesitarás en el siguiente paso)
+
+### Paso 3: Subir a Test PyPI (Recomendado Primero)
+
+Antes de subir al PyPI de producción, prueba tu paquete en [Test PyPI](https://test.pypi.org/):
+
+Instala twine:
+
+```bash
+python -m pip install --upgrade twine
+```
+
+Sube a Test PyPI:
+
+```bash
+python -m twine upload --repository testpypi dist/*
+```
+
+Cuando se te solicite, usa:
+
+- **Username:** `__token__`
+- **Password:** ``
+
+Visita `https://test.pypi.org/project/dashai-my-plugin/` para verificar que tu paquete aparece correctamente.
+
+### Paso 4: Subir a PyPI de Producción
+
+Una vez completadas las pruebas, sube al PyPI oficial:
+
+```bash
+python -m twine upload --repository pypi dist/*
+```
+
+Cuando se te solicite, usa:
+
+- **Username:** `__token__`
+- **Password:** ``
+
+¡Tu plugin ya está disponible en PyPI! Los usuarios pueden instalarlo con:
+
+```bash
+pip install dashai-my-plugin
+```
+
+---
+
+## Notas Importantes
+
+### Convención de Nombres
+
+Asegúrate de que tu paquete use el prefijo `dashai-` (ej., `dashai-my-plugin`) para que DashAI lo descubra automáticamente al instalarse.
+
+### Metadatos del Paquete
+
+Tu **pyproject.toml** debe incluir:
+
+- Descripción y keywords claras
+- Entry points para las clases del plugin
+- Links a la página principal y al repositorio
+- Información de licencia
+
+### Versionado
+
+Sigue el [Versionado Semántico](https://semver.org/):
+
+- `0.0.1` para lanzamientos iniciales
+- `0.1.0` para adiciones de funcionalidades menores
+- `1.0.0` para lanzamientos estables con estabilidad de API
+
+---
+
+## Compartir tu Plugin
+
+Después de publicar, comparte tu plugin con la comunidad:
+
+1. Agrega el topic `dashai-plugin` a tu repositorio de GitHub
+2. Anúncialo en [GitHub Discussions](https://github.com/DashAISoftware/DashAI/discussions)
+3. Considera agregar documentación o un tutorial
+
+¡Feliz publicación! 🚀
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/build/testing.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/testing.md
new file mode 100644
index 000000000..1c77d1636
--- /dev/null
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/build/testing.md
@@ -0,0 +1,89 @@
+---
+title: Testing
+sidebar_label: Testing
+sidebar_position: 4
+---
+
+# Testing
+
+## Tests del Backend
+
+DashAI usa **pytest** para los tests del backend.
+
+### Ejecutar Todos los Tests
+
+```bash
+pytest -v
+```
+
+### Ejecutar un Archivo de Test Específico
+
+```bash
+pytest tests/back/api/test_components_api.py -v
+```
+
+### Ejecutar un Test por Nombre
+
+```bash
+pytest tests/back/api/test_components_api.py::test_name -v
+```
+
+### Ejecutar con Cobertura
+
+```bash
+pytest --cov=DashAI --cov-report=html
+```
+
+## Tests del Frontend
+
+```bash
+cd DashAI/front
+yarn test
+```
+
+## Estructura de Tests
+
+```
+tests/
+├── back/
+│ ├── api/ # Tests de endpoints de la API
+│ ├── job/ # Tests de ejecución de jobs
+│ ├── models/ # Tests de componentes de modelos
+│ ├── converters/ # Tests de converters
+│ ├── explorers/ # Tests de explorers
+│ └── ...
+└── docs/ # Tests del generador de documentación
+```
+
+## CI
+
+GitHub Actions ejecuta la suite completa de tests en cada PR y push sobre:
+
+- **Versiones de Python**: 3.10, 3.11, 3.12, 3.13
+- **Sistemas operativos**: Ubuntu, Windows, macOS
+
+Verificaciones adicionales en CI:
+
+- **pre-commit**: Linting con Ruff, formateo y otros hooks
+- **db-migrations**: Verificaciones de upgrade/downgrade/reversibilidad con Alembic
+- **docs**: Build de Docusaurus
+
+## Escribir Tests
+
+Los tests del backend siguen las convenciones de pytest:
+
+```python
+def test_create_dataset(client, tmp_path):
+ # Arrange
+ csv_file = tmp_path / "test.csv"
+ csv_file.write_text("a,b\n1,2\n3,4")
+
+ # Act
+ response = client.post("/api/v1/dataset/", files={"file": open(csv_file)})
+
+ # Assert
+ assert response.status_code == 201
+ assert response.json()["name"] == "test"
+```
+
+Se provee un fixture `client` que crea una aplicación FastAPI de prueba con una base de datos SQLite en memoria.
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/architecture.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/architecture.md
index 15d328817..77ff02c3e 100644
--- a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/architecture.md
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/architecture.md
@@ -31,13 +31,13 @@ La inyección de dependencias es gestionada por **Kink**. El contenedor DI (`bac
## Lecturas Adicionales
-| Tema | Página |
-| ------------------------------------------------------------------- | --------------------------------------------------- |
-| Estructura de la API REST, mapa de rutas, inyección de dependencias | [API](./api) |
-| Tipos de componentes, registro y objetos configurables | [Componentes](./components) |
-| Esquema SQLite, tablas ORM y almacenamiento de datos | [Base de Datos](./database) |
-| Cola de trabajos Huey y tipos de trabajos | [Sistema de Trabajos](./job-system) |
-| Sesiones de Notebook, exploradores y converters | [Notebook](./notebook) |
-| Recorridos completos de entrenamiento y exploración | [Ejemplos de Flujo de Trabajo](./workflow-examples) |
-| Tipos semánticos de columnas e inferencia | [Tipos Semánticos](./semantic-types) |
-| Primitivo central de datos, splits y ciclo de vida del dato | [DashAIDataset](./dashai-dataset) |
+| Tema | Página |
+| ------------------------------------------------------------------- | ------------------------------------------------------------ |
+| Estructura de la API REST, mapa de rutas, inyección de dependencias | [API](/deep-dive/api) |
+| Tipos de componentes, registro y objetos configurables | [Componentes](/deep-dive/components) |
+| Esquema SQLite, tablas ORM y almacenamiento de datos | [Base de Datos](/deep-dive/database) |
+| Cola de trabajos Huey y tipos de trabajos | [Sistema de Trabajos](/deep-dive/job-system) |
+| Sesiones de Notebook, exploradores y converters | [Notebook](/deep-dive/notebook) |
+| Recorridos completos de entrenamiento y exploración | [Ejemplos de Flujo de Trabajo](/deep-dive/workflow-examples) |
+| Tipos semánticos de columnas e inferencia | [Tipos Semánticos](/deep-dive/semantic-types) |
+| Primitivo central de datos, splits y ciclo de vida del dato | [DashAIDataset](/deep-dive/dashai-dataset) |
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/dashai-dataset.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/dashai-dataset.md
index ed1f08c2e..0baccfdae 100644
--- a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/dashai-dataset.md
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/dashai-dataset.md
@@ -10,7 +10,7 @@ sidebar_position: 9
`DashAIDataset` es el primitivo central de datos de DashAI. Extiende la clase `Dataset` de HuggingFace con dos responsabilidades adicionales:
-- **Metadatos de tipo semántico** — un diccionario `_types` (`Dict[str, DashAIDataType]`) que mapea cada nombre de columna a su tipo semántico DashAI (ver [Tipos Semánticos](./semantic-types)). Estos metadatos se persisten dentro del esquema Apache Arrow para sobrevivir ciclos de guardado/carga.
+- **Metadatos de tipo semántico** — un diccionario `_types` (`Dict[str, DashAIDataType]`) que mapea cada nombre de columna a su tipo semántico DashAI (ver [Tipos Semánticos](/deep-dive/semantic-types)). Estos metadatos se persisten dentro del esquema Apache Arrow para sobrevivir ciclos de guardado/carga.
- **Metadatos de splits** — un diccionario `splits` que registra qué índices de fila pertenecen a qué split (`train`, `test`, `validation`), junto con estadísticas agregadas calculadas durante la carga del dataset.
Cada pieza de datos que fluye por DashAI — carga, transformaciones en el notebook, entrenamiento de modelos, predicciones — se representa como un `DashAIDataset`.
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/replicability.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/replicability.md
index 77b45d2bf..ccaf4af18 100644
--- a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/replicability.md
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/replicability.md
@@ -44,15 +44,15 @@ La exportación completa de la pipeline (una "receta" portátil que captura toda
:::
:::info ¿Qué es un Notebook?
-Un Notebook en DashAI es una sesión de trabajo con una copia mutable de un dataset. Agrupa Exploradores (visualizaciones) y Converters (transformaciones) aplicados a esa copia. El dataset original nunca se modifica. Consulta [Diseño del Sistema → Notebook](./notebook) para más detalles.
+Un Notebook en DashAI es una sesión de trabajo con una copia mutable de un dataset. Agrupa Exploradores (visualizaciones) y Converters (transformaciones) aplicados a esa copia. El dataset original nunca se modifica. Consulta [Diseño del Sistema → Notebook](/deep-dive/notebook) para más detalles.
:::
## Detalles del Almacenamiento de Datos
-| Artefacto | Ubicación de almacenamiento |
-| --------------------- | ---------------------------------------------------------------------------------------------------- |
-| Datasets | Archivos Apache Arrow IPC en `~/.DashAI/` |
-| Modelos entrenados | Archivos pickle/joblib en `~/.DashAI/runs/{run_id}/` |
-| Gráficos de optimización | Objetos Plotly serializados junto a la ejecución |
-| Métricas | Tabla `Metric` en `db.sqlite` |
+| Artefacto | Ubicación de almacenamiento |
+| ------------------------- | ---------------------------------------------------------------------------------------------------- |
+| Datasets | Archivos Apache Arrow IPC en `~/.DashAI/` |
+| Modelos entrenados | Archivos pickle/joblib en `~/.DashAI/runs/{run_id}/` |
+| Gráficos de optimización | Objetos Plotly serializados junto a la ejecución |
+| Métricas | Tabla `Metric` en `db.sqlite` |
| Resultados de exploración | Imágenes PNG generadas por Exploradores dentro de un Notebook, referenciadas por la tabla `Explorer` |
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/overview.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/overview.md
index 16ed1268c..fda6bb8c3 100644
--- a/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/overview.md
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/overview.md
@@ -22,12 +22,12 @@ El módulo de Modelos es el entorno de DashAI para entrenar, evaluar, comparar y
Al abrir el módulo de Modelos, el área principal muestra los tipos de tareas disponibles con una descripción de cada una. Usa la barra de búsqueda para filtrar tareas por nombre.
-| Tarea | Descripción |
-| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Clasificación Tabular** | Predice una etiqueta categórica a partir de datos tabulares estructurados (filas y columnas). |
-| **Clasificación de Texto** | Asigna categorías o etiquetas predefinidas a documentos de texto según su contenido. Útil para análisis de sentimientos, filtrado de spam, categorización de temas. |
-| **Regresión** | Predice un valor numérico continuo a partir de datos tabulares estructurados. |
-| **Traducción** | Convierte texto de un idioma a otro preservando el significado y el contexto (tarea de NLP). |
+| Tarea | Descripción |
+| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Clasificación Tabular** | Predice una etiqueta categórica a partir de datos tabulares estructurados (filas y columnas). |
+| **Clasificación de Texto** | Asigna categorías o etiquetas predefinidas a documentos de texto según su contenido. Útil para análisis de sentimientos, filtrado de spam, categorización de temas. |
+| **Regresión** | Predice un valor numérico continuo a partir de datos tabulares estructurados. |
+| **Traducción** | Convierte texto de un idioma a otro preservando el significado y el contexto (tarea de NLP). |
Cada tarea impone requisitos específicos sobre los tipos de columnas para entrada y salida — estos se validan automáticamente al configurar la sesión.
@@ -58,7 +58,7 @@ Los modelos disponibles varían según la tarea. Para Clasificación Tabular, po
Esta sección se divide en las siguientes páginas:
-- **[Entrenar un Modelo](./train)** — Cómo crear una sesión, configurar columnas de entrada/salida, definir divisiones de datos, añadir modelos, establecer hiperparámetros y ejecutar el entrenamiento.
-- **[Predicciones](./predictions)** — Cómo generar predicciones usando modelos entrenados, tanto desde un dataset completo como desde datos ingresados manualmente.
-- **[Explicabilidad](./explainability)** — Cómo usar explicadores globales y locales para entender el comportamiento del modelo.
-- **[Comparación de Modelos](./comparison)** — Cómo comparar métricas entre modelos usando tablas y gráficos.
+- **[Entrenar un Modelo](/learn/tutorials/Models/train)** — Cómo crear una sesión, configurar columnas de entrada/salida, definir divisiones de datos, añadir modelos, establecer hiperparámetros y ejecutar el entrenamiento.
+- **[Predicciones](/learn/tutorials/Models/predictions)** — Cómo generar predicciones usando modelos entrenados, tanto desde un dataset completo como desde datos ingresados manualmente.
+- **[Explicabilidad](/learn/tutorials/Models/explainability)** — Cómo usar explicadores globales y locales para entender el comportamiento del modelo.
+- **[Comparación de Modelos](/learn/tutorials/Models/comparison)** — Cómo comparar métricas entre modelos usando tablas y gráficos.
diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/train.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/train.md
index 2de62a69c..6ef6152ef 100644
--- a/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/train.md
+++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/learn/tutorials/Models/train.md
@@ -52,18 +52,18 @@ Cada tarea impone requisitos de tipo específicos. Para Clasificación Tabular,
Define cómo DashAI divide el dataset en subconjuntos de entrenamiento, validación y prueba. Hay tres opciones disponibles:
-| Opción | Descripción |
-| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Use predefined splits** | Usa divisiones train/validación/test ya definidas en el archivo del dataset. Solo disponible si el dataset fue cargado con estructura pre-dividida. |
-| **Random split by proportion** | Asigna filas aleatoriamente a cada subconjunto según las proporciones que especifiques. El valor por defecto es Train: 0.6, Validation: 0.2, Test: 0.2. |
-| **Manual split by row indices** | Especifica manualmente los índices de fila de inicio y fin para cada subconjunto. |
+| Opción | Descripción |
+| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Use predefined splits** | Usa divisiones train/validación/test ya definidas en el archivo del dataset. Solo disponible si el dataset fue cargado con estructura pre-dividida. |
+| **Random split by proportion** | Asigna filas aleatoriamente a cada subconjunto según las proporciones que especifiques. El valor por defecto es Train: 0.6, Validation: 0.2, Test: 0.2. |
+| **Manual split by row indices** | Especifica manualmente los índices de fila de inicio y fin para cada subconjunto. |
Al usar **Random split**, hay tres opciones adicionales disponibles:
-| Opción | Descripción |
-| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
-| **Shuffle** | Mezcla aleatoriamente las filas antes de dividir. Recomendado para evitar sesgo de orden en los datos. Habilitado por defecto. |
-| **Stratify** | Asegura que cada división preserve las mismas proporciones de clases que el dataset completo. Útil para datasets desbalanceados. |
+| Opción | Descripción |
+| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Shuffle** | Mezcla aleatoriamente las filas antes de dividir. Recomendado para evitar sesgo de orden en los datos. Habilitado por defecto. |
+| **Stratify** | Asegura que cada división preserve las mismas proporciones de clases que el dataset completo. Útil para datasets desbalanceados. |
| **Seed** | Una semilla aleatoria fija para reproducibilidad. El valor por defecto es `42`. Establece un valor específico para asegurar que siempre se produzca la misma división. |
Haz clic en **CREATE SESSION** para finalizar la configuración. La sesión se abre inmediatamente.
@@ -120,12 +120,12 @@ Una tabla resumen que muestra todos los modelos en la sesión con columnas:
Cada modelo tiene una tarjeta expandible que muestra su nombre, algoritmo, insignia de estado actual y botones de acción:
-| Botón | Descripción |
-| ---------------- | --------------------------------------------------------------------------------------------- |
-| **EDIT** | Reabre el modal de configuración para cambiar el nombre del modelo o los hiperparámetros. |
-| **TRAIN** | Inicia el entrenamiento de este modelo. Cambia a **RE-TRAIN** después de la primera ejecución. |
-| **Insignia de estado** | Muestra el estado actual: **Not Started**, **Finalizado** o **Error**. |
-| 🗑 | Elimina el modelo de la sesión. |
+| Botón | Descripción |
+| ---------------------- | ---------------------------------------------------------------------------------------------- |
+| **EDIT** | Reabre el modal de configuración para cambiar el nombre del modelo o los hiperparámetros. |
+| **TRAIN** | Inicia el entrenamiento de este modelo. Cambia a **RE-TRAIN** después de la primera ejecución. |
+| **Insignia de estado** | Muestra el estado actual: **Not Started**, **Finalizado** o **Error**. |
+| 🗑 | Elimina el modelo de la sesión. |
**Para entrenar un modelo individual:** haz clic en **TRAIN** en su tarjeta.
@@ -151,11 +151,11 @@ Las métricas solo están disponibles después de que el entrenamiento está com
### EXPLAINABILITY
-Muestra los explicadores globales y locales adjuntos a este modelo. Consulta la página [Explicabilidad](./explainability) para más detalles.
+Muestra los explicadores globales y locales adjuntos a este modelo. Consulta la página [Explicabilidad](/learn/tutorials/Models/explainability) para más detalles.
### PREDICTIONS
-Muestra las predicciones de dataset y las predicciones manuales generadas desde este modelo. Consulta la página [Predicciones](./predictions) para más detalles.
+Muestra las predicciones de dataset y las predicciones manuales generadas desde este modelo. Consulta la página [Predicciones](/learn/tutorials/Models/predictions) para más detalles.
### HYPERPARAMETERS
@@ -172,10 +172,10 @@ Muestra los valores exactos de hiperparámetros usados en la última ejecución
## Solución de Problemas
-| Síntoma | Causa probable | Solución |
-| ------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
-| El banner de compatibilidad muestra una advertencia | Los tipos de columnas no coinciden con los requisitos de la tarea | Revisa los tipos de columnas en el Explorador de Datasets y vuelve a cargar si es necesario |
-| El botón NEXT no está activo en la configuración de sesión | Los campos requeridos están vacíos | Asegúrate de que se seleccionó un dataset y se ingresó un nombre de sesión |
-| El modelo muestra insignia **Error** después del entrenamiento | Valores de hiperparámetros inválidos o problema con los datos | Haz clic en **EDIT** para revisar los parámetros, o revisa la Job Queue para detalles del error |
-| No hay métricas disponibles después del entrenamiento | Modelo entrenado con datos incompatibles | Revisa la selección de columnas de entrada/salida y vuelve a entrenar |
-| RUN ALL no es visible | No se han añadido modelos todavía | Añade al menos un modelo antes de usar RUN ALL |
+| Síntoma | Causa probable | Solución |
+| -------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| El banner de compatibilidad muestra una advertencia | Los tipos de columnas no coinciden con los requisitos de la tarea | Revisa los tipos de columnas en el Explorador de Datasets y vuelve a cargar si es necesario |
+| El botón NEXT no está activo en la configuración de sesión | Los campos requeridos están vacíos | Asegúrate de que se seleccionó un dataset y se ingresó un nombre de sesión |
+| El modelo muestra insignia **Error** después del entrenamiento | Valores de hiperparámetros inválidos o problema con los datos | Haz clic en **EDIT** para revisar los parámetros, o revisa la Job Queue para detalles del error |
+| No hay métricas disponibles después del entrenamiento | Modelo entrenado con datos incompatibles | Revisa la selección de columnas de entrada/salida y vuelve a entrenar |
+| RUN ALL no es visible | No se han añadido modelos todavía | Añade al menos un modelo antes de usar RUN ALL |
diff --git a/docs/scripts/generate_components.py b/docs/scripts/generate_components.py
index 9759e0fcc..328d9df95 100644
--- a/docs/scripts/generate_components.py
+++ b/docs/scripts/generate_components.py
@@ -528,7 +528,7 @@ def _render_component_mdx(info) -> str:
comp_type = class_lookup.get(comp)
if comp_type:
comp_dir = _type_to_dir(comp_type)
- lines.append(f"- [`{comp}`](../{comp_dir}/{comp})")
+ lines.append(f"- [`{comp}`](/components/{comp_dir}/{comp})")
else:
lines.append(f"- `{comp}`")
lines.append("")
diff --git a/installer/installer.iss b/installer/installer.iss
index d53848f29..38f743732 100644
--- a/installer/installer.iss
+++ b/installer/installer.iss
@@ -4,7 +4,7 @@
; pyinstaller -D -n DashAI-launcher-cpu --clean --add-data "DashAI/front/build;DashAI/front/build" --add-data "%CONDA_PREFIX%\Lib\site-packages\transformers;transformers" --add-binary "%CONDA_PREFIX%\Lib\site-packages\llama_cpp\lib\*;llama_cpp/lib" --additional-hooks-dir=hooks DashAI/__main__.py
[Setup]
AppName=DashAI
-AppVersion=0.9.2
+AppVersion=0.9.3
AppPublisher=DashAI Software
AppPublisherURL=https://dash-ai.com
DefaultDirName={pf}\DashAI
diff --git a/setup.py b/setup.py
index 6bde33622..821d55b8f 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ def load_requirements(filename):
setup(
name="DashAI",
- version="0.9.2",
+ version="0.9.3",
license="MIT",
description=(
"DashAI: a graphical toolbox for training, evaluating and deploying "
diff --git a/tests/back/api/test_components_api.py b/tests/back/api/test_components_api.py
index f41a29e50..9d501dbf0 100644
--- a/tests/back/api/test_components_api.py
+++ b/tests/back/api/test_components_api.py
@@ -191,7 +191,7 @@ def test_get_component_by_id(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -318,7 +318,7 @@ def test_get_components_select_only_dataloaders(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -328,7 +328,7 @@ def test_get_components_select_only_dataloaders(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -338,7 +338,7 @@ def test_get_components_select_only_dataloaders(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -443,7 +443,7 @@ def test_get_components_ignore_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -453,7 +453,7 @@ def test_get_components_ignore_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -463,7 +463,7 @@ def test_get_components_ignore_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -481,7 +481,7 @@ def test_get_components_ignore_tasks_and_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -491,7 +491,7 @@ def test_get_components_ignore_tasks_and_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -501,7 +501,7 @@ def test_get_components_ignore_tasks_and_models(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -605,7 +605,7 @@ def test_get_components_dataloader_component_parent(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -615,7 +615,7 @@ def test_get_components_dataloader_component_parent(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -650,7 +650,7 @@ def test_get_components_by_type_and_task(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -660,7 +660,7 @@ def test_get_components_by_type_and_task(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -694,7 +694,7 @@ def test_get_components_select_and_ignore_by_type(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -704,7 +704,7 @@ def test_get_components_select_and_ignore_by_type(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -714,7 +714,7 @@ def test_get_components_select_and_ignore_by_type(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -735,7 +735,7 @@ def test_get_components_select_type_and_parent(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
@@ -745,7 +745,7 @@ def test_get_components_select_type_and_parent(client: TestClient):
"type": "DataLoader",
"configurable_object": True,
"schema": {},
- "metadata": None,
+ "metadata": {"category": "File Uploading"},
"description": None,
"display_name": None,
"color": None,
diff --git a/tests/back/models/test_tabular_class_models.py b/tests/back/models/test_tabular_class_models.py
index 96413440b..3d99798d0 100644
--- a/tests/back/models/test_tabular_class_models.py
+++ b/tests/back/models/test_tabular_class_models.py
@@ -16,10 +16,20 @@
split_indexes,
to_dashai_dataset,
)
+from DashAI.back.models.scikit_learn.adaboost_classifier import AdaBoostClassifier
+from DashAI.back.models.scikit_learn.bagging_classifier import BaggingClassifier
+from DashAI.back.models.scikit_learn.extra_trees_classifier import ExtraTreesClassifier
+from DashAI.back.models.scikit_learn.gaussian_nb import GaussianNB
+from DashAI.back.models.scikit_learn.gradient_boosting_classifier import (
+ GradientBoostingClassifier,
+)
from DashAI.back.models.scikit_learn.k_neighbors_classifier import KNeighborsClassifier
+from DashAI.back.models.scikit_learn.linear_svc_classifier import LinearSVCClassifier
+from DashAI.back.models.scikit_learn.mlp_classifier import MLPClassifier
from DashAI.back.models.scikit_learn.random_forest_classifier import (
RandomForestClassifier,
)
+from DashAI.back.models.scikit_learn.sgd_classifier import SGDClassifier
from DashAI.back.models.scikit_learn.svc import SVC
from DashAI.back.types.categorical import Categorical
from DashAI.back.types.utils import save_types_in_arrow_metadata
@@ -119,6 +129,65 @@ def fixture_model_params() -> dict:
"tol": 0.001,
"verbose": False,
},
+ "gradient_boosting": {
+ "loss": "log_loss",
+ "learning_rate": 0.1,
+ "n_estimators": 5,
+ "max_depth": 2,
+ "min_samples_split": 2,
+ "min_samples_leaf": 1,
+ "subsample": 1.0,
+ "random_state": 42,
+ },
+ "extra_trees": {
+ "n_estimators": 5,
+ "max_depth": None,
+ "min_samples_split": 2,
+ "min_samples_leaf": 1,
+ "bootstrap": False,
+ "random_state": 42,
+ },
+ "adaboost": {
+ "n_estimators": 5,
+ "learning_rate": 1.0,
+ "random_state": 42,
+ },
+ "bagging": {
+ "n_estimators": 5,
+ "max_samples": 1.0,
+ "max_features": 1.0,
+ "bootstrap": True,
+ "bootstrap_features": False,
+ "random_state": 42,
+ },
+ "gaussian_nb": {
+ "var_smoothing": 1e-9,
+ },
+ "mlp": {
+ "hidden_layer_size": 10,
+ "activation": "relu",
+ "solver": "adam",
+ "alpha": 0.0001,
+ "learning_rate_init": 0.001,
+ "max_iter": 50,
+ "random_state": 42,
+ },
+ "linear_svc": {
+ "C": 1.0,
+ "loss": "squared_hinge",
+ "max_iter": 100,
+ "tol": 1e-4,
+ "fit_intercept": True,
+ "random_state": 42,
+ },
+ "sgd": {
+ "loss": "hinge",
+ "alpha": 0.0001,
+ "max_iter": 100,
+ "tol": 1e-3,
+ "learning_rate": "optimal",
+ "random_state": 42,
+ },
}
@@ -205,3 +274,134 @@ def test_get_schema_from_model_class():
assert model_schema["type"] == "object"
assert "properties" in model_schema
assert isinstance(model_schema["properties"], dict)
+
+
+def test_check_is_fitted_new_classifiers(
+ divided_dataset: Tuple[DatasetDict, DatasetDict], model_params: dict
+):
+ gb_model = GradientBoostingClassifier(**model_params["gradient_boosting"])
+ gb_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ et_model = ExtraTreesClassifier(**model_params["extra_trees"])
+ et_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ ab_model = AdaBoostClassifier(**model_params["adaboost"])
+ ab_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ bag_model = BaggingClassifier(**model_params["bagging"])
+ bag_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ gnb_model = GaussianNB(**model_params["gaussian_nb"])
+ gnb_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ mlp_model = MLPClassifier(**model_params["mlp"])
+ mlp_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ lsvc_model = LinearSVCClassifier(**model_params["linear_svc"])
+ lsvc_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ sgd_model = SGDClassifier(**model_params["sgd"])
+ sgd_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+
+ try:
+ check_is_fitted(gb_model)
+ check_is_fitted(et_model)
+ check_is_fitted(ab_model)
+ check_is_fitted(bag_model)
+ check_is_fitted(gnb_model)
+ check_is_fitted(mlp_model)
+ check_is_fitted(lsvc_model)
+ check_is_fitted(sgd_model)
+ except Exception as e:
+ pytest.fail(
+ f"Unexpected error in test_check_is_fitted_new_classifiers: {repr(e)}"
+ )
+
+
+def test_predict_new_classifiers(
+ divided_dataset: Tuple[DatasetDict, DatasetDict], model_params: dict
+):
+ gb_model = GradientBoostingClassifier(**model_params["gradient_boosting"])
+ gb_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_gb = gb_model.predict(divided_dataset[0]["test"])
+
+ et_model = ExtraTreesClassifier(**model_params["extra_trees"])
+ et_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_et = et_model.predict(divided_dataset[0]["test"])
+
+ ab_model = AdaBoostClassifier(**model_params["adaboost"])
+ ab_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_ab = ab_model.predict(divided_dataset[0]["test"])
+
+ bag_model = BaggingClassifier(**model_params["bagging"])
+ bag_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_bag = bag_model.predict(divided_dataset[0]["test"])
+
+ gnb_model = GaussianNB(**model_params["gaussian_nb"])
+ gnb_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_gnb = gnb_model.predict(divided_dataset[0]["test"])
+
+ mlp_model = MLPClassifier(**model_params["mlp"])
+ mlp_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_mlp = mlp_model.predict(divided_dataset[0]["test"])
+
+ lsvc_model = LinearSVCClassifier(**model_params["linear_svc"])
+ lsvc_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_lsvc = lsvc_model.predict(divided_dataset[0]["test"])
+
+ sgd_model = SGDClassifier(**model_params["sgd"])
+ sgd_model.train(divided_dataset[0]["train"], divided_dataset[1]["train"])
+ y_pred_sgd = sgd_model.predict(divided_dataset[0]["test"])
+
+ n_test = divided_dataset[0]["test"].num_rows
+ for y_pred in (
+ y_pred_gb,
+ y_pred_et,
+ y_pred_ab,
+ y_pred_bag,
+ y_pred_gnb,
+ y_pred_mlp,
+ y_pred_lsvc,
+ y_pred_sgd,
+ ):
+ assert isinstance(y_pred, np.ndarray)
+ assert len(y_pred) == n_test
+
+
+def test_not_fitted_new_classifiers(
+ divided_dataset: Tuple[DatasetDict, DatasetDict], model_params: dict
+):
+ with pytest.raises(NotFittedError):
+ GradientBoostingClassifier(**model_params["gradient_boosting"]).predict(
+ divided_dataset[0]["test"]
+ )
+
+ with pytest.raises(NotFittedError):
+ LinearSVCClassifier(**model_params["linear_svc"]).predict(
+ divided_dataset[0]["test"]
+ )
+
+ with pytest.raises(NotFittedError):
+ SGDClassifier(**model_params["sgd"]).predict(divided_dataset[0]["test"])
+
+
+def test_get_schema_from_new_classifier_classes():
+ new_models = (
+ GradientBoostingClassifier,
+ ExtraTreesClassifier,
+ AdaBoostClassifier,
+ BaggingClassifier,
+ GaussianNB,
+ MLPClassifier,
+ LinearSVCClassifier,
+ SGDClassifier,
+ )
+ for model_cls in new_models:
+ schema = model_cls.get_schema()
+ assert isinstance(schema, dict), f"{model_cls.__name__} schema is not a dict"
+ assert schema.get("type") == "object", (
+ f"{model_cls.__name__} schema type != object"
+ )
+ assert isinstance(schema.get("properties"), dict), (
+ f"{model_cls.__name__} schema has no properties dict"
+ )