-
-
-
-
π Model Deployment
-
-
- This module allows users to deploy a machine learning model for inference.
- First, you'll have to choose a model. Then, you can select between two input methods:
-
-
-
- β Manual Sample Entry: Fill in the required feature values manually to test a single sample.
-
-
-
- β Test File Input: Upload a dataset file (CSV format) to run batch predictions on multiple samples.
-
-
-
+
- π Learn more about this tool in our{' '}
- shell.openExternal("https://medomicslab.gitbook.io/medomics-docs/tutorials/deployment/application-module")}
- style={{ color: "#4991dfff", textDecoration: "none", cursor: "pointer" }}
- >
- documentation. π
-
+ Select a model and provide input data. You can either enter a single sample manually
+ or upload a CSV dataset for batch predictions.
-
-
+
+ Single sample: Fill in required feature values to test one prediction.
+
+ Dataset file: Upload a CSV to run batch predictions on multiple samples.
+
+
+ }
+ >
+
-
-
-
-
+
>
)
diff --git a/renderer/components/mainPages/evaluationLandingPage.jsx b/renderer/components/mainPages/evaluationLandingPage.jsx
index 5f19e917..c7c4a85b 100644
--- a/renderer/components/mainPages/evaluationLandingPage.jsx
+++ b/renderer/components/mainPages/evaluationLandingPage.jsx
@@ -1,17 +1,18 @@
import { randomUUID } from "crypto"
import Image from "next/image"
import { InputText } from "primereact/inputtext"
+import { Message } from "primereact/message"
import { useContext, useEffect, useState } from "react"
import { Button, Card, Stack } from "react-bootstrap"
import { FaAlignJustify } from "react-icons/fa"
import { MdDashboard } from "react-icons/md"
import { TbZoom } from "react-icons/tb"
import med3paLogo from "../../../resources/MED3paLogo.png"
-import myimage from "../../../resources/medomics_transparent_bg.png"
import { LayoutModelContext } from "../layout/layoutContext"
import { insertMEDDataObjectIfNotExists } from "../mongoDB/mongoDBUtils"
import { DataContext } from "../workspace/dataContext"
import { MEDDataObject } from "../workspace/NewMedDataObject"
+import ModuleLandingShell, { ModuleGuideText } from "./moduleBasics/ModuleLandingShell"
// Variable used to store some modularity information about the module
@@ -142,30 +143,33 @@ export default function EvaluationLandingPage() {
const isStartDisabled = loading || trimmedName === "" || !isValidName || hasConflict
return (
-
-
- Evaluation Module
-
-
-
-
-
-
- {/* Description of the Evaluation Module */}
-
-
- The Evaluation Module is as a quality-check workspace that tests completed AI models on fresh data,
- uses interactive dashboards to explain how they make decisions, and flags unreliable predictions to
- ensure no patient group is left behind.
-
-
-
-
- {/* Main Title and Subtitle */}
-
-
+
+
+ The Evaluation Module is a quality-check workspace that tests completed AI models on fresh data,
+ uses interactive dashboards to explain how they make decisions, and flags unreliable predictions to
+ ensure no patient group is left behind.
+
+
+ }
+ documentation={{
+ url: "https://medomicslab.gitbook.io/medomics-docs/tutorials/development/evaluation-module",
+ label: "Evaluation Module documentation",
+ }}
+ >
+
{/* Explainer Dashboard Card */}
-
+
Explainer Dashboard
@@ -193,7 +197,10 @@ export default function EvaluationLandingPage() {
{/* MED3pa Card */}
-
+
MED3pa
@@ -201,16 +208,17 @@ export default function EvaluationLandingPage() {
- Evaluate models stability and performance and flag unreliable predictions to ensure no patient group is left behind.
+
+
+ Evaluate models stability and performance and flag unreliable predictions to ensure no patient group is left behind.
+
-
-
-
-
+
)
}
diff --git a/renderer/components/mainPages/exploratory.jsx b/renderer/components/mainPages/exploratory.jsx
index 7b7a0d01..b23dffd7 100644
--- a/renderer/components/mainPages/exploratory.jsx
+++ b/renderer/components/mainPages/exploratory.jsx
@@ -1,12 +1,10 @@
-import { shell } from 'electron'
-import Image from "next/image"
import { useContext } from "react"
-import myimage from "../../../resources/medomics_transparent_bg.png"
import DTale from "../exploratory/dtale"
import SweetViz from "../exploratory/sweetViz"
import YDataProfiling from "../exploratory/yDataProfiling"
import { ErrorRequestContext } from "../generalPurpose/errorRequestContext"
import { WorkspaceContext } from "../workspace/workspaceContext"
+import ModuleLandingShell, { ModuleGuideText } from "./moduleBasics/ModuleLandingShell"
import ModulePage from "./moduleBasics/modulePage"
@@ -19,43 +17,30 @@ const ExploratoryPage = () => {
const { setError } = useContext(ErrorRequestContext)
return (
- <>
-
-
- Exploratory Module
-
-
-
-
-
π Exploratory Data Analysis (EDA)
-
- Before training a machine learning model, it is essential to explore and understand the dataset.
- This page provides three powerful tools, each with a unique role in data exploration:
-
-
-
β‘ SweetViz : Generates automated reports with dataset characteristics,
- feature distributions, correlations, and comparisons. Helps quickly detect outliers, missing values, and potential biases.
-
-
β‘ Y-Data Profiling : Performs a deep statistical audit to analyze feature types,
- missing values, and relationships, helping detect preprocessing issues before model training.
-
-
β‘ D-Tale : Provides a spreadsheet-like interactive interface for filtering,
- sorting, visualizing, and modifying data in real time.
-
-
- π Learn how to use these tools in our
- shell.openExternal("https://medomicslab.gitbook.io/medomics-docs/tutorials/design/exploratory-module")}
- style={{ color: "#4991dfff", textDecoration: "none", cursor: "pointer" }}
- > documentation. π
-
-
-
-
-
-
-
- >
+
+
+ The Exploratory Module helps you understand your data, detect potential issues,
+ and make informed decisions for preprocessing and model selection.
+
+ SweetViz: Automated reports with distributions, correlations, and comparisons.
+ Y-Data Profiling: Deep statistical audit of feature types, missing values, and relationships.
+ D-Tale: Spreadsheet-like interface for filtering, sorting, and visualizing data in real time.
+
+ }
+ >
+
+
+
+
)
}
diff --git a/renderer/components/mainPages/extractionLandingPage.jsx b/renderer/components/mainPages/extractionLandingPage.jsx
index 0619b629..583a7ee8 100644
--- a/renderer/components/mainPages/extractionLandingPage.jsx
+++ b/renderer/components/mainPages/extractionLandingPage.jsx
@@ -6,8 +6,8 @@ import { FaAlignJustify, FaChartLine } from "react-icons/fa"
import { FaImage } from "react-icons/fa6"
import { LuBrain } from "react-icons/lu"
import medimlLogo from "../../../resources/MEDimlLogo.png"
-import myimage from "../../../resources/medomics_transparent_bg.png"
import { LayoutModelContext } from "../layout/layoutContext"
+import ModuleLandingShell, { ModuleGuideText } from "./moduleBasics/ModuleLandingShell"
export default function ExtractionLandingPage() {
@@ -20,114 +20,108 @@ export default function ExtractionLandingPage() {
}
return (
-
-
- Extraction Module
-
-
-
-
+
+
+ This module offers ready-made tools to pull insights from images, notes, and health tracking data,
+ while relying on the specialized MEDiml tool to extract deeper features from medical scans.
+
+
+ }
+ documentation={{
+ url: "https://medomicslab.gitbook.io/medomics-docs/tutorials/design/extraction-modules",
+ label: "Extraction Module documentation",
+ }}
+ >
+
+
- {/* Description of the Extraction Module */}
-
-
- This input module offers ready-made tools to pull insights from images, notes, and health tracking data,
- while relying on the specialized MEDiml tool to extract deeper features from medical scans.
-
-
+
+
+
+
+ MEDiml
+
+
+
+
+ Extract radiomic features from medical images and analyze your radiomics data using machine learning models.
+
+ choosePage(e, "ExtractionMEDimage")}>
+ Start Extraction
+
+
+
-
-
-
- {/* Disabled warning message */}
-
-
- {/* Main Title and Subtitle */}
-
-
- {/* MEDiml Card */}
-
-
-
- MEDiml
-
-
-
-
- Extract radiomic features from medical images and analyze your radiomics data using machine learning models.
-
- choosePage(e, "ExtractionMEDimage")}>
- Start Extraction
-
-
-
-
- {/* Text Card */}
-
-
-
- Text
-
-
-
-
- Extract Embeddings from Text Notes using a variety of models.
-
- choosePage(e, "ExtractionText")}>
- Start Extraction
-
-
-
-
-
+
+
+
+ Text
+
+
+
+
+ Extract Embeddings from Text Notes using a variety of models.
+
+ choosePage(e, "ExtractionText")}>
+ Start Extraction
+
+
+
-
-
-
-
-
- {/* Time Series Card */}
-
-
-
- Time Series
-
-
-
-
- Extract time series characteristics from physiological signals using TSFRESH and other models.
-
- choosePage(e, "ExtractionTS")}>
- Start Extraction
-
-
-
+
+
+
+
+ Time Series
+
+
+
+
+ Extract time series characteristics from physiological signals using TSFRESH and other models.
+
+ choosePage(e, "ExtractionTS")}>
+ Start Extraction
+
+
+
- {/* Image Card */}
-
-
-
- Image
-
-
-
-
- Extract Embeddings from JPG images using a variety of models from TorchXRayVision python library.
-
- choosePage(e, "ExtractionImage")}>
- Start Extraction
-
-
-
-
-
+
+
+
+ Image
+
+
+
+
+ Extract Embeddings from JPG images using a variety of models from TorchXRayVision python library.
+
+ choosePage(e, "ExtractionImage")}>
+ Start Extraction
+
+
+
-
-
+
+
)
}
diff --git a/renderer/components/mainPages/learningLandingPage.jsx b/renderer/components/mainPages/learningLandingPage.jsx
new file mode 100644
index 00000000..54588201
--- /dev/null
+++ b/renderer/components/mainPages/learningLandingPage.jsx
@@ -0,0 +1,501 @@
+import { randomUUID } from "crypto";
+import { Button } from "primereact/button";
+import { InputText } from "primereact/inputtext";
+import { useContext, useEffect, useState } from "react";
+import { Card, Stack } from "react-bootstrap";
+import { AiOutlineExperiment } from "react-icons/ai";
+import { LuBrainCircuit } from "react-icons/lu";
+import { toast } from "react-toastify";
+import { sceneDescription as learningSceneDescription } from "../../public/setupVariables/learningNodesParams";
+import { LayoutModelContext } from "../layout/layoutContext";
+import { getCollectionData } from "../dbComponents/utils";
+import { insertMEDDataObjectIfNotExists } from "../mongoDB/mongoDBUtils";
+import { DataContext } from "../workspace/dataContext";
+import { MEDDataObject } from "../workspace/NewMedDataObject";
+import { FaPlay } from "react-icons/fa";
+import ModuleLandingShell, { ModuleGuideSteps, ModuleGuideText } from "./moduleBasics/ModuleLandingShell";
+
+
+const buildOpenItem = (id, medObject) => ({
+ index: id,
+ canMove: true,
+ isFolder: false,
+ children: medObject.childrenIDs || [],
+ data: medObject.name,
+ canRename: true,
+ type: medObject.type || "medml",
+ inWorkspace: medObject.inWorkspace ?? false,
+ path: medObject.path ?? null,
+ isLocked: medObject.isLocked ?? null,
+ usedIn: medObject.usedIn ?? null,
+});
+
+const BRAND_NAVY = "rgb(88, 131, 196)";
+const BRAND_BLUE = "rgb(171, 223, 147)";
+
+const JOURNEY_STEPS = [
+ { id: 1, label: "Readiness check", detail: "Ensure your data is ready for training using other modules" },
+ { id: 2, label: "Name your experiment", detail: "Pick a clear, unique scene name" },
+ { id: 3, label: "Build visually", detail: "Drag, drop, and connect nodes, no coding required" },
+];
+
+export default function LearningLandingPage() {
+ const [nameExt, setNameExp] = useState("");
+ const [nameML, setNameML] = useState("");
+ const [nameExpError, setNameExpError] = useState("");
+ const [nameMlError, setNameMLError] = useState("");
+ const [isExtDisabled, setIsExtDisabled] = useState(true);
+ const [isMLDisabled, setIsMLDisabled] = useState(true);
+ const [experimentList, setExperimentList] = useState([]);
+ const [loadingExt, setLoadingExp] = useState(false);
+ const [loadingML, setLoadingML] = useState(false);
+ const [pendingOpenId, setPendingOpenId] = useState(null);
+ const [scenesExpanded, setScenesExpanded] = useState(false);
+ const { dispatchLayout, setLayoutRequestQueue } = useContext(LayoutModelContext);
+ const { globalData } = useContext(DataContext);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadExperiments = async () => {
+ const scenes = [];
+
+ for (const id of Object.keys(globalData)) {
+ const medObject = globalData[id];
+ if (medObject.type !== "medml") continue;
+
+ let isExperiment = false;
+ const metadataId = MEDDataObject.getChildIDWithName(globalData, id, "metadata.json");
+ if (metadataId) {
+ try {
+ const jsonContent = await getCollectionData(metadataId);
+ if (jsonContent?.[0]?.isExperiment !== undefined) {
+ isExperiment = !!jsonContent[0].isExperiment;
+ }
+ } catch {
+ // Default to main scene when metadata cannot be read.
+ }
+ }
+
+ scenes.push({
+ id,
+ name: medObject.name.replace(/\.medml$/i, ""),
+ displayName: medObject.name,
+ isExperiment,
+ openItem: buildOpenItem(id, medObject),
+ });
+ }
+
+ scenes.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
+
+ if (!cancelled) {
+ setExperimentList(scenes);
+ }
+ };
+
+ loadExperiments();
+ return () => {
+ cancelled = true;
+ };
+ }, [globalData]);
+
+ useEffect(() => {
+ if (!pendingOpenId || !globalData[pendingOpenId]) return;
+
+ setLoadingExp(false);
+ setLoadingML(false);
+
+ const medObject = globalData[pendingOpenId];
+ const openItem = buildOpenItem(pendingOpenId, medObject);
+ const type = openItem.type === "medml" ? "openInLearningModule" : null;
+ if (type === null) {
+ toast.error("We couldn't open this file type. Please contact support if this persists.");
+ return;
+ }
+ dispatchLayout({ type: type, payload: openItem });
+ if (setLayoutRequestQueue) {
+ setLayoutRequestQueue((prev) => [...prev, { type: "DELETE_TAB", payload: { id: "learningLandingPage" } }]);
+ } else {
+ dispatchLayout({ type: "remove", payload: { name: "Learning Module" } });
+ }
+
+ setPendingOpenId(null);
+ }, [dispatchLayout, globalData, pendingOpenId, setLayoutRequestQueue]);
+
+ const createSceneContent = async (sceneName, isExperiment) => {
+ const setLoading = isExperiment ? setLoadingExp : setLoadingML;
+ setLoading(true);
+ const trimmedSceneName = sceneName.trim();
+
+ const sceneId = randomUUID();
+
+ let sceneFolder = new MEDDataObject({
+ id: randomUUID(),
+ name: trimmedSceneName,
+ type: "directory",
+ parentID: "EXPERIMENTS",
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ let sceneFolderId = await insertMEDDataObjectIfNotExists(sceneFolder)
+
+ if (!isExperiment) {
+ for (const folder of learningSceneDescription.externalFolders) {
+ let medObject = new MEDDataObject({
+ id: randomUUID(),
+ name: folder,
+ type: "directory",
+ parentID: sceneFolderId,
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ await insertMEDDataObjectIfNotExists(medObject)
+ }
+ }
+
+ let sceneObject = new MEDDataObject({
+ id: randomUUID(),
+ name: trimmedSceneName + ".medml",
+ type: "medml",
+ parentID: sceneFolderId,
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ let sceneObjectId = await insertMEDDataObjectIfNotExists(sceneObject)
+
+ const emptyScene = [
+ {
+ nodes: [],
+ edges: [],
+ viewport: {
+ x: 235.01823373389306,
+ y: 186.91830088750686,
+ zoom: 1.0,
+ },
+ MLType: "classification",
+ intersections: [],
+ isExperiment: isExperiment,
+ },
+ ];
+ let metadataObject = new MEDDataObject({
+ id: randomUUID(),
+ name: "metadata.json",
+ type: "json",
+ parentID: sceneObjectId,
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ await insertMEDDataObjectIfNotExists(metadataObject, null, emptyScene)
+
+ let backendMetadataObject = new MEDDataObject({
+ id: randomUUID(),
+ name: "backend_metadata.json",
+ type: "json",
+ parentID: sceneObjectId,
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ await insertMEDDataObjectIfNotExists(backendMetadataObject, null, emptyScene)
+
+ for (const folder of learningSceneDescription.internalFolders) {
+ let medObject = new MEDDataObject({
+ id: randomUUID(),
+ name: folder,
+ type: "directory",
+ parentID: sceneObjectId,
+ childrenIDs: [],
+ inWorkspace: false
+ })
+ await insertMEDDataObjectIfNotExists(medObject)
+ }
+
+ MEDDataObject.updateWorkspaceDataObject();
+ setPendingOpenId(sceneObjectId || sceneId);
+ };
+
+ const validateName = (value, existingList, extension) => {
+ const trimmedName = value.trim();
+ const isValidName = /^[A-Za-z0-9_-]+$/.test(trimmedName);
+ const existingNames = new Set(existingList);
+ const hasConflict =
+ trimmedName !== "" && (existingNames.has(trimmedName) || existingNames.has(`${trimmedName}.${extension}`));
+
+ if (trimmedName === "") return { error: "", disabled: true };
+ if (!isValidName) {
+ return { error: "Use only letters, numbers, hyphens, or underscores.", disabled: true };
+ }
+ if (hasConflict) {
+ return { error: "An experiment with this name already exists. Try a different name.", disabled: true };
+ }
+ return { error: "", disabled: false };
+ };
+
+ const experimentNames = experimentList.map((scene) => scene.displayName);
+
+ const handleOpenScene = (item) => {
+ dispatchLayout({ type: "openInLearningModule", payload: item });
+ if (setLayoutRequestQueue) {
+ setLayoutRequestQueue((prev) => [...prev, { type: "DELETE_TAB", payload: { id: "learningLandingPage" } }]);
+ } else {
+ dispatchLayout({ type: "remove", payload: { name: "Learning Module" } });
+ }
+ };
+
+ const onNameExpChange = (e) => {
+ setNameExp(e);
+ const { error, disabled } = validateName(e, experimentNames, "medml");
+ setNameExpError(error);
+ setIsExtDisabled(loadingExt || disabled);
+ };
+
+ const onNameMLChange = (e) => {
+ setNameML(e);
+ const { error, disabled } = validateName(e, experimentNames, "medml");
+ setNameMLError(error);
+ setIsMLDisabled(loadingML || disabled);
+ };
+
+ const totalExperiments = experimentList.length;
+ const isCreating = loadingExt || loadingML;
+
+ const workspaceFooter = totalExperiments === 0 ? (
+
+
No experiments yet
+
+ Name your first scene above and click Create & open to launch the visual builder.
+
+
+ ) : (
+
+ setScenesExpanded((prev) => !prev)}
+ aria-expanded={scenesExpanded}
+ aria-controls="workspace-scenes-grid"
+ >
+
+ {totalExperiments} scene{totalExperiments !== 1 ? "s" : ""} in workspace
+
+
+
+
+ {scenesExpanded && (
+
+ {experimentList.map((scene) => {
+ const accentColor = scene.isExperiment ? "#8db8ec" : "#54ebeb";
+
+ return (
+
handleOpenScene(scene.openItem)}
+ title={`${scene.name} (${scene.isExperiment ? "Experimental" : "Main"})`}
+ aria-label={`Open ${scene.name} ${scene.isExperiment ? "experimental" : "main"} scene`}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.borderColor = accentColor;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.borderColor = "";
+ }}
+ >
+ {scene.isExperiment ? (
+
+ ) : (
+
+ )}
+ {scene.name}
+
+ );
+ })}
+
+ )}
+
+ );
+
+ return (
+
+
+
+
+
+
+ Start with an Experimental Scene to compare models quickly. When you're ready,
+ open a Main Scene for your full training pipeline.
+
+
+
+ >
+ }
+ footer={workspaceFooter}
+ >
+
+ {/* Experimental Scene */}
+
+
+
+
+
+ Experimental Scene
+
+
+
+
+
+ STEP 1 Β· MODEL EXPLORATION
+
+
+ Compare algorithms side-by-side on your dataset before running a full experiment.
+
+
+
+
+
+
+ onNameExpChange(e.target.value)}
+ aria-invalid={!!nameExpError}
+ aria-describedby={nameExpError ? "experimental-scene-error" : "experimental-scene-hint"}
+ disabled={isCreating}
+ />
+ .medml
+
+ {!nameExpError && (
+
+ Letters, numbers, hyphens, and underscores only.
+
+ )}
+ {nameExpError && (
+
+ {nameExpError}
+
+ )}
+
+
+ createSceneContent(nameExt, "medext")}
+ disabled={isExtDisabled || loadingML}
+ label={loadingExt ? "Creating scene..." : "Create & open scene"}
+ aria-label="Create and open experimental scene"
+ />
+
+
+
+
+ {/* Main Scene */}
+
+
+
+
+
+ Main Scene
+
+
+
+
+
+ STEP 2 Β· TRAIN & FINALIZE
+
+
+ Set up your definitive ML workflow, then train, test, tune hyperparameters, and visualize results in one
+ visual canvas.
+
+
+
+
+
+
+
+
+ onNameMLChange(e.target.value)}
+ aria-invalid={!!nameMlError}
+ aria-describedby={nameMlError ? "main-scene-error" : "main-scene-hint"}
+ disabled={isCreating}
+ />
+ .medml
+
+ {!nameMlError && (
+
+ Letters, numbers, hyphens, and underscores only.
+
+ )}
+ {nameMlError && (
+
+ {nameMlError}
+
+ )}
+
+
+ createSceneContent(nameML, "medml")}
+ disabled={isMLDisabled || loadingExt}
+ label={loadingML ? "Creating scene..." : "Create & open scene"}
+ aria-label="Create and open main learning scene"
+ />
+
+
+
+
+
+ );
+}
diff --git a/renderer/components/mainPages/medfl.jsx b/renderer/components/mainPages/medfl.jsx
index daf2b13a..d4cfebd5 100644
--- a/renderer/components/mainPages/medfl.jsx
+++ b/renderer/components/mainPages/medfl.jsx
@@ -1,13 +1,11 @@
-import React from "react"
+import MedflowWelcomePage from "./medflWelcomePage"
import ModulePage from "./moduleBasics/modulePage"
-import MEDflHelloWorldPanel from "../medfl/medflHelloWorldPanel"
const MEDflPage = ({ pageId }) => {
return (
<>
- MEDfl Module
-
+
>
)
diff --git a/renderer/components/mainPages/medflWelcomePage.jsx b/renderer/components/mainPages/medflWelcomePage.jsx
new file mode 100644
index 00000000..ab828f5b
--- /dev/null
+++ b/renderer/components/mainPages/medflWelcomePage.jsx
@@ -0,0 +1,81 @@
+import Image from "next/image"
+import { Message } from "primereact/message"
+import { useContext } from "react"
+import { Button, Card, Stack } from "react-bootstrap"
+import { FaCogs, FaGlobe } from "react-icons/fa"
+import myimage from "../../../resources/medomics_transparent_bg.png"
+import { LayoutModelContext } from "../layout/layoutContext"
+import ModuleLandingShell, { ModuleGuideText } from "./moduleBasics/ModuleLandingShell"
+
+export default function MedflWelcomePage() {
+ const { dispatchLayout } = useContext(LayoutModelContext)
+
+ function choosePage(event, name) {
+ event.stopPropagation()
+ console.log(`Double clicked ${name}`, event, `open${name}Module`)
+ dispatchLayout({ type: `open${name}Module`, payload: { pageId: name } })
+ }
+
+ return (
+
+
+ MEDfl is a collaborative framework for federated learning in medicine.
+
+
+ A comprehensive package for simulating the federated learning process in the medical field,
+ featuring an intuitive user interface for seamless interaction.
+
+
+ }
+ documentation={{
+ url: "https://medfl.app",
+ label: "MEDfl's Official Website",
+ }}
+ >
+
+
+
+
+
+ Simulation FL
+
+
+
+
+ Run a full federated learning simulation locally. Perfect for testing pipelines, debugging strategies, and visualizing metrics in a controlled environment.
+
+ choosePage(e, "MEDfl")}>
+ Start Simulation
+
+
+
+
+
+
+
+ Real-World FL
+
+
+
+
+ Connect to real devices or remote servers to orchestrate a production-grade federated learning workflow with secure communication and live data.
+
+ choosePage(e, "flRwWorkflow")}>
+ Go Live
+
+
+
+
+
+ )
+}
diff --git a/renderer/components/mainPages/moduleBasics/ModuleLandingShell.jsx b/renderer/components/mainPages/moduleBasics/ModuleLandingShell.jsx
new file mode 100644
index 00000000..d84cbcd4
--- /dev/null
+++ b/renderer/components/mainPages/moduleBasics/ModuleLandingShell.jsx
@@ -0,0 +1,99 @@
+import { shell } from "electron"
+import Image from "next/image"
+import { useState } from "react"
+import myimage from "../../../../resources/medomics_transparent_bg.png"
+import { useTheme } from "../../theme/themeContext"
+
+/**
+ * Shared layout shell for module landing pages.
+ * Tools render immediately; detailed guidance stays in a collapsible panel.
+ */
+export default function ModuleLandingShell({
+ title,
+ description,
+ infoContent,
+ documentation,
+ children,
+ footer,
+ className = "",
+ contentMaxWidth = "960px",
+ alignTop = false,
+}) {
+ const { isDarkMode } = useTheme()
+ const [infoExpanded, setInfoExpanded] = useState(false)
+
+ const openDocumentation = () => {
+ if (documentation?.url) {
+ shell.openExternal(documentation.url)
+ }
+ }
+
+ return (
+
+
+
+
+ {title}
+ {description && {description}
}
+
+ {infoContent && (
+ setInfoExpanded((prev) => !prev)}
+ aria-expanded={infoExpanded}
+ aria-controls="module-landing-info-panel"
+ >
+
+ {infoExpanded ? "Hide guide" : "Module guide"}
+
+
+ )}
+
+
+ {infoContent && infoExpanded && (
+
+ {infoContent}
+ {documentation?.url && (
+
+
+ {documentation.label || "Open documentation"}
+
+
+ )}
+
+ )}
+
+
{children}
+ {footer &&
}
+
+
+ )
+}
+
+export function ModuleGuideSteps({ steps }) {
+ if (!steps?.length) return null
+
+ return (
+
+ {steps.map((step) => (
+ -
+
+ {step.id}
+
+
+
{step.label}
+ {step.detail &&
{step.detail}
}
+
+
+ ))}
+
+ )
+}
+
+export function ModuleGuideText({ children }) {
+ return
{children}
+}
diff --git a/renderer/pages/_app.js b/renderer/pages/_app.js
index 77aac8c0..956d5784 100644
--- a/renderer/pages/_app.js
+++ b/renderer/pages/_app.js
@@ -49,6 +49,7 @@ import "../styles/extraction/extractionTabular.css"
import "../styles/flow/reactFlow.css"
import "../styles/flow/results.css"
import "../styles/globals.css"
+import "../styles/moduleLanding.css"
import "../styles/iconSidebar.css"
import "../styles/imageContainer.css"
import "../styles/input/MEDprofiles.css"
diff --git a/renderer/styles/exploratory/exploratory.css b/renderer/styles/exploratory/exploratory.css
index c87d8640..fb1d5a84 100644
--- a/renderer/styles/exploratory/exploratory.css
+++ b/renderer/styles/exploratory/exploratory.css
@@ -1,23 +1,21 @@
-.exploratory {
+.exploratory.module-landing-shell .module-landing-content {
display: flex;
flex-direction: column;
gap: 1rem;
- width: 100%;
- padding: 1rem;
}
-.exploratory > .p-card {
+.exploratory.module-landing-shell .module-landing-content > .p-card {
box-shadow:
2px 2px 3px -1px rgb(0 0 0 / 20%),
0 1px 1px 0 rgb(0 0 0 / 14%),
0px 0px 3px 2px rgb(0 0 0 / 12%);
}
-.exploratory > .p-card .p-card-content {
+.exploratory.module-landing-shell .module-landing-content > .p-card .p-card-content {
padding: 0;
}
-.exploratory > .p-card .p-card-body {
+.exploratory.module-landing-shell .module-landing-content > .p-card .p-card-body {
padding: 1rem;
padding-top: 0.25rem;
}
diff --git a/renderer/styles/globals.css b/renderer/styles/globals.css
index 4bf9d3e1..9d3250f5 100644
--- a/renderer/styles/globals.css
+++ b/renderer/styles/globals.css
@@ -189,6 +189,10 @@ a {
text-align: right;
}
+.text-italic {
+ font-style: italic;
+}
+
hr.solid {
border-top: 3px solid #bbb;
margin: 0 0 0.25rem 0;
@@ -349,6 +353,31 @@ body {
border-radius: 0.5rem 0.5rem 0rem 0rem;
}
+.flexlayout__tab_button--selected.design-tab {
+ background-color: #1d3666 !important;
+ color: #ffffff !important;
+ border-bottom: 2px solid #355fad;
+}
+
+.flexlayout__tab_button--selected.development-tab {
+ background-color: #076464 !important;
+ color: #ffffff !important;
+ border-bottom: 2px solid #579e9e;
+}
+
+.flexlayout__tab_button--selected.deployment-tab {
+ background-color: #3d81e7 !important;
+ color: #ffffff !important;
+ border-bottom: 2px solid #9ab4da;
+}
+
+.flexlayout__tab_button--selected.settings-tab {
+ background-color: #6e4b8b !important;
+ color: #ffffff !important;
+ border-bottom: 2px solid #d1b1eb;
+}
+
+
.align-center {
display: flex;
align-items: center;
diff --git a/renderer/styles/moduleLanding.css b/renderer/styles/moduleLanding.css
new file mode 100644
index 00000000..3dbac765
--- /dev/null
+++ b/renderer/styles/moduleLanding.css
@@ -0,0 +1,456 @@
+.module-landing-shell {
+ --ml-surface: #ffffff;
+ --ml-surface-muted: #f8fafc;
+ --ml-border: #e2e8f0;
+ --ml-border-strong: #cbd5e1;
+ --ml-text: #334155;
+ --ml-text-muted: #64748b;
+ --ml-accent: rgb(88, 131, 196);
+ --ml-accent-soft: rgba(88, 131, 196, 0.1);
+ --ml-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+ --ml-shadow-hover: 0 4px 14px rgba(15, 23, 42, 0.08);
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100%;
+ padding: 1rem;
+ box-sizing: border-box;
+}
+
+.module-landing-shell--top {
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+.module-landing-shell--top .module-landing-inner {
+ flex: 1;
+ min-height: 0;
+}
+
+.module-landing-shell--top .module-landing-content {
+ justify-content: flex-start;
+ align-items: stretch;
+ flex: 1;
+ min-height: 0;
+}
+
+.module-landing-shell--top .module-landing-input-layout {
+ min-height: 700px;
+ max-height: none;
+ flex: 1;
+}
+
+.module-landing-shell[data-theme-aware="dark"] {
+ --ml-surface: #1e1e1e;
+ --ml-surface-muted: #2a2a2a;
+ --ml-border: #3f3f46;
+ --ml-border-strong: #52525b;
+ --ml-text: #e2e8f0;
+ --ml-text-muted: #94a3b8;
+ --ml-accent-soft: rgba(141, 184, 236, 0.14);
+ --ml-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
+ --ml-shadow-hover: 0 4px 14px rgba(0, 0, 0, 0.35);
+}
+
+.module-landing-inner {
+ width: 100%;
+ margin: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.module-landing-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 0.4rem;
+ margin-bottom: 0.85rem;
+ width: 100%;
+}
+
+.module-landing-logo {
+ flex-shrink: 0;
+}
+
+.module-landing-title {
+ margin: 0;
+ font-size: clamp(1.2rem, 2.5vw, 1.55rem);
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ color: var(--ml-text);
+ line-height: 1.2;
+}
+
+.module-landing-tagline {
+ margin: 0;
+ max-width: 52ch;
+ font-size: 0.84rem;
+ line-height: 1.45;
+ color: var(--ml-text-muted);
+}
+
+.module-landing-info-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ border: 1px solid var(--ml-border);
+ background: var(--ml-surface-muted);
+ color: var(--ml-text-muted);
+ border-radius: 999px;
+ padding: 0.35rem 0.7rem;
+ font-size: 0.78rem;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+ margin-top: 0.15rem;
+}
+
+.module-landing-info-toggle:hover {
+ background: var(--ml-accent-soft);
+ border-color: var(--ml-accent);
+ color: var(--ml-text);
+}
+
+.module-landing-info-panel {
+ width: 100%;
+ border: 1px solid var(--ml-border);
+ border-radius: 10px;
+ background: var(--ml-surface-muted);
+ box-shadow: var(--ml-shadow);
+ margin-bottom: 0.85rem;
+ overflow: hidden;
+ text-align: left;
+}
+
+.module-landing-info-body {
+ padding: 0.85rem 1rem;
+ color: var(--ml-text);
+ font-size: 0.86rem;
+ line-height: 1.55;
+}
+
+.module-landing-guide-text p {
+ margin-bottom: 0.55rem;
+}
+
+.module-landing-guide-text p:last-child {
+ margin-bottom: 0;
+}
+
+.module-landing-guide-text strong {
+ color: var(--ml-text);
+}
+
+.module-landing-steps {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: center;
+}
+
+.module-landing-step {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ flex: 1 1 200px;
+ max-width: 280px;
+ padding: 0.55rem 0.65rem;
+ border: 1px solid var(--ml-border);
+ border-radius: 8px;
+ background: var(--ml-surface);
+}
+
+.module-landing-step-num {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: rgb(171, 223, 147);
+ color: #1e293b;
+ font-size: 0.72rem;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+
+.module-landing-step-label {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--ml-text);
+}
+
+.module-landing-step-detail {
+ font-size: 0.74rem;
+ color: var(--ml-text-muted);
+ margin-top: 0.1rem;
+}
+
+.module-landing-doc-link {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ width: 100%;
+ border: none;
+ border-top: 1px solid var(--ml-border);
+ background: transparent;
+ color: var(--ml-accent);
+ font-size: 0.8rem;
+ font-weight: 600;
+ padding: 0.55rem 1rem;
+ cursor: pointer;
+ text-align: left;
+}
+
+.module-landing-doc-link:hover {
+ background: var(--ml-accent-soft);
+}
+
+.module-landing-content {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+}
+
+.module-landing-content > * {
+ width: 100%;
+}
+
+.module-landing-footer {
+ width: 100%;
+ margin-top: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.module-landing-tool-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 420px));
+ gap: 1rem;
+ align-items: stretch;
+ justify-content: center;
+ width: 100%;
+}
+
+.module-landing-tool-card {
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: var(--ml-shadow);
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
+ width: 100%;
+ max-width: 440px;
+ justify-self: center;
+}
+
+.module-landing-tool-card:hover {
+ box-shadow: var(--ml-shadow-hover);
+}
+
+.module-landing-scenes-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ margin: 0 auto;
+ border: 1px solid var(--ml-border);
+ background: var(--ml-surface-muted);
+ color: var(--ml-text-muted);
+ border-radius: 999px;
+ padding: 0.3rem 0.75rem;
+ font-size: 0.78rem;
+ cursor: pointer;
+}
+
+.module-landing-scenes-toggle:hover {
+ border-color: var(--ml-accent);
+ color: var(--ml-text);
+}
+
+.module-landing-scenes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(118px, 140px));
+ gap: 8px;
+ margin-top: 0.5rem;
+ justify-content: center;
+ width: 100%;
+}
+
+.module-landing-scene-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ border: 1px solid var(--ml-border);
+ border-radius: 8px;
+ padding: 0.5rem;
+ min-height: 72px;
+ background: var(--ml-surface);
+ box-shadow: var(--ml-shadow);
+ cursor: pointer;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.module-landing-scene-card:hover {
+ box-shadow: var(--ml-shadow-hover);
+}
+
+.module-landing-scene-name {
+ font-size: 0.72rem;
+ font-weight: 600;
+ line-height: 1.2;
+ margin-top: 0.25rem;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--ml-text);
+}
+
+.module-landing-empty-state {
+ text-align: center;
+ padding: 1rem;
+ border: 1px dashed var(--ml-border-strong);
+ border-radius: 10px;
+ background: var(--ml-surface-muted);
+ color: var(--ml-text-muted);
+ font-size: 0.86rem;
+ max-width: 520px;
+}
+
+.module-landing-stack-center {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.module-landing-stack-center > .p-message,
+.module-landing-stack-center > .alert {
+ width: 100%;
+ max-width: 720px;
+}
+
+.module-landing-input-layout {
+ display: flex;
+ width: 100%;
+ max-width: 1100px;
+ min-height: min(520px, 68vh);
+ max-height: 72vh;
+ overflow: hidden;
+ border: 1px solid var(--ml-border);
+ border-radius: 12px;
+ background: var(--ml-surface);
+}
+
+.module-landing-input-sidebar {
+ width: 260px;
+ flex-shrink: 0;
+ border-right: 1px solid var(--ml-border);
+ padding: 0.75rem;
+ overflow-y: auto;
+ background: var(--ml-surface-muted);
+}
+
+.module-landing-input-main {
+ flex: 1;
+ min-width: 0;
+ padding: 1rem;
+ overflow-y: auto;
+ text-align: left;
+}
+
+.module-landing-input-sidebar-title {
+ text-align: center;
+ margin-bottom: 0.75rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--ml-border);
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--ml-text);
+}
+
+.module-landing-input-section-title {
+ padding: 0.45rem 0.55rem;
+ border-bottom: 1px solid var(--ml-border);
+ font-weight: 600;
+ font-size: 0.82rem;
+ border-radius: 6px 6px 0 0;
+ color: var(--ml-text);
+}
+
+.module-landing-input-nav-item {
+ cursor: pointer;
+ padding: 0.45rem 0.55rem;
+ margin-bottom: 0.35rem;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border: 1px solid var(--ml-border);
+ font-size: 0.82rem;
+ color: var(--ml-text);
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.module-landing-input-nav-item:hover {
+ border-color: var(--ml-accent);
+}
+
+.module-landing-input-nav-item.is-active {
+ background: var(--ml-accent-soft);
+ border-color: var(--ml-accent);
+}
+
+.module-landing-input-tool-tile {
+ border: 1px solid var(--ml-border);
+ border-radius: 10px;
+ padding: 0.75rem 0.9rem;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--ml-surface);
+}
+
+.module-landing-input-tool-tile:hover {
+ background: var(--ml-accent-soft);
+ border-color: var(--ml-accent);
+ transform: translateY(-1px);
+}
+
+.module-landing-input-tool-tile h4 {
+ margin: 0;
+ font-size: 0.92rem;
+ color: var(--ml-text);
+}
+
+.module-landing-input-tool-tile p {
+ margin: 0.2rem 0 0;
+ font-size: 0.78rem;
+ color: var(--ml-text-muted);
+}
+
+.module-landing-application-panel {
+ width: 100%;
+ max-width: 760px;
+ text-align: left;
+}
+
+.exploratory.module-landing-shell .module-landing-content {
+ gap: 1rem;
+}
+
+.exploratory.module-landing-shell .module-landing-content > * {
+ width: 100%;
+ max-width: 900px;
+}
diff --git a/renderer/styles/sidebarTree.css b/renderer/styles/sidebarTree.css
index 54deb37b..22a4c0d3 100644
--- a/renderer/styles/sidebarTree.css
+++ b/renderer/styles/sidebarTree.css
@@ -1,7 +1,7 @@
:root {
--design-color: #1d3666;
--development-color: #076464;
- --deployment-color: #0b6648;
+ --deployment-color: #3d81e7;
--settings-color: #6e4b8b;
--rct-search-text-offset: 3rem;