From 11990f456e458e93a7471dedc29f6d724eae01f9 Mon Sep 17 00:00:00 2001 From: vedikaagarwal28 Date: Thu, 21 May 2026 13:00:22 +0530 Subject: [PATCH 1/2] feat: add canvas sidebar and node components --- frontend/app/globals.css | 28 +++++ frontend/app/page.tsx | 114 +++++++++--------- frontend/package-lock.json | 69 ++++++++++- frontend/package.json | 3 +- frontend/src/components/Canvas.tsx | 55 +++++++++ frontend/src/components/Sidebar.tsx | 87 +++++++++++++ frontend/src/components/nodes/DatasetNode.tsx | 15 +++ frontend/src/components/nodes/ModelNode.tsx | 15 +++ .../src/components/nodes/PreprocessNode.tsx | 16 +++ .../components/nodes/TrainTestSplitNode.tsx | 21 ++++ frontend/src/store/pipelineStore.ts | 29 +++++ frontend/tsconfig.json | 2 +- 12 files changed, 390 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/nodes/DatasetNode.tsx create mode 100644 frontend/src/components/nodes/ModelNode.tsx create mode 100644 frontend/src/components/nodes/PreprocessNode.tsx create mode 100644 frontend/src/components/nodes/TrainTestSplitNode.tsx diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a2dc41e..52b2aeb 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -19,8 +19,36 @@ } } +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + body { background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + + +.react-flow__node { + padding: 0 !important; + border: none !important; + border-radius: 0 !important; + background: transparent !important; + box-shadow: none !important; + font-size: unset !important; +} + +.react-flow__controls-button { + background: #1a1a1f !important; + border-bottom: 1px solid rgba(255,255,255,0.08) !important; + fill: rgba(255,255,255,0.6) !important; +} + +.react-flow__controls-button:hover { + background: rgba(124,58,237,0.2) !important; + fill: white !important; +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 3f36f7c..a2b8e9c 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,65 +1,63 @@ -import Image from "next/image"; +"use client"; +import Canvas from "@/components/Canvas"; +import Sidebar from "@/components/Sidebar"; export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+ + {/* Topbar */} +
+ + {/* Left: logo + title */} +
+
+ +
+ ML Workflow Builder +
+ + {/* Right: Save + Run Pipeline */} +
+ + +
+
+ + {/* Body */} +
+ + {/* Icon rail */} +
+ {[ + { label: "Nodes", svg: <> }, + { label: "Workflows", svg: <> }, + { label: "Runs", svg: <> }, + { label: "History", svg: <> }, + { label: "Settings", svg: <> }, + ].map((item, i) => ( + + ))}
-
- - Vercel logomark - Deploy Now - - - Documentation - + + {/* Sidebar */} + + + {/* Canvas */} +
+
-
+ +
); -} +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e1496d..bcf590a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,13 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@xyflow/react": "^12.10.2", "axios": "^1.14.0", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4", "reactflow": "^11.11.4", - "zustand": "^5.0.12" + "zustand": "^5.0.13" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -2671,6 +2672,66 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -7329,9 +7390,9 @@ } }, "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/frontend/package.json b/frontend/package.json index 408ecc9..513b17a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,13 @@ "lint": "eslint" }, "dependencies": { + "@xyflow/react": "^12.10.2", "axios": "^1.14.0", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4", "reactflow": "^11.11.4", - "zustand": "^5.0.12" + "zustand": "^5.0.13" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/frontend/src/components/Canvas.tsx b/frontend/src/components/Canvas.tsx index e69de29..242dd17 100644 --- a/frontend/src/components/Canvas.tsx +++ b/frontend/src/components/Canvas.tsx @@ -0,0 +1,55 @@ +"use client"; +import { useCallback } from "react"; +import { ReactFlow, Background, Controls, MiniMap, BackgroundVariant } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { usePipelineStore } from "@/store/pipelineStore"; +import DatasetNode from "./nodes/DatasetNode"; +import PreprocessNode from "./nodes/PreprocessNode"; +import ModelNode from "./nodes/ModelNode"; +import TrainTestSplitNode from "./nodes/TrainTestSplitNode"; + +const nodeTypes = { + dataset: DatasetNode, + preprocess: PreprocessNode, + model: ModelNode, + trainTestSplit: TrainTestSplitNode, +}; + +export default function Canvas() { + const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNode } = usePipelineStore(); + + const onDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }, []); + + const onDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + const type = e.dataTransfer.getData("nodeType"); + if (!type) return; + const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const position = { x: e.clientX - bounds.left - 80, y: e.clientY - bounds.top - 40 }; + addNode({ id: `${type}-${Date.now()}`, type, position, data: { label: type } }); + }, [addNode]); + + return ( +
+ + + + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index e69de29..39ccf64 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,87 @@ +"use client"; +import { useState } from "react"; + +const categories = [ + { + label: "Data", + nodes: [ + { type: "dataset", label: "Dataset", desc: "Load a dataset", color: "#7c3aed", bg: "rgba(124,58,237,0.12)", border: "rgba(124,58,237,0.25)", icon: }, + { type: "trainTestSplit", label: "Train-Test Split", desc: "Split data into train and test sets", color: "#6366f1", bg: "rgba(99,102,241,0.12)", border: "rgba(99,102,241,0.25)", icon: <> }, + ], + }, + { + label: "Preprocessing", + nodes: [ + { type: "preprocess", label: "Preprocess", desc: "Scale and preprocess data", color: "#d97706", bg: "rgba(217,119,6,0.12)", border: "rgba(217,119,6,0.25)", icon: <> }, + ], + }, + { + label: "Model", + nodes: [ + { type: "model", label: "Model", desc: "Train a machine learning model", color: "#059669", bg: "rgba(5,150,105,0.12)", border: "rgba(5,150,105,0.25)", icon: <> }, + ], + }, +]; + +export default function Sidebar() { + const [collapsed, setCollapsed] = useState>({ Data: false, Preprocessing: false, Model: false }); + const toggle = (label: string) => + setCollapsed((prev) => ({ ...prev, [label]: !prev[label] })); + + const onDragStart = (e: React.DragEvent, nodeType: string) => { + e.dataTransfer.setData("nodeType", nodeType); + e.dataTransfer.effectAllowed = "move"; + }; + + return ( +
+ + {/* Header */} +
+

Available Nodes

+

Drag nodes to the canvas

+
+ + {/* Categories */} +
+ {categories.map((cat) => ( +
+ + {/* Category header */} + + + {/* Nodes */} + {!collapsed[cat.label] && ( +
+ {cat.nodes.map((node) => ( +
onDragStart(e, node.type)} + style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", borderRadius: 10, border: `1px solid ${node.border}`, background: node.bg, cursor: "grab" }} + > +
+ {node.icon} +
+
+

{node.label}

+

{node.desc}

+
+
+ ))} +
+ )} +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/nodes/DatasetNode.tsx b/frontend/src/components/nodes/DatasetNode.tsx new file mode 100644 index 0000000..bbcfd9a --- /dev/null +++ b/frontend/src/components/nodes/DatasetNode.tsx @@ -0,0 +1,15 @@ +"use client"; +import { Handle, Position } from "@xyflow/react"; + +export default function DatasetNode({ data }: { data: { label: string; dataset?: string } }) { + return ( +
+
+
+ Dataset +
+

Load a dataset

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/nodes/ModelNode.tsx b/frontend/src/components/nodes/ModelNode.tsx new file mode 100644 index 0000000..68a5902 --- /dev/null +++ b/frontend/src/components/nodes/ModelNode.tsx @@ -0,0 +1,15 @@ +"use client"; +import { Handle, Position } from "@xyflow/react"; + +export default function ModelNode({ data }: { data: { label: string } }) { + return ( +
+
+
+ Model +
+

Train a ML model

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/nodes/PreprocessNode.tsx b/frontend/src/components/nodes/PreprocessNode.tsx new file mode 100644 index 0000000..0d18fea --- /dev/null +++ b/frontend/src/components/nodes/PreprocessNode.tsx @@ -0,0 +1,16 @@ +"use client"; +import { Handle, Position } from "@xyflow/react"; + +export default function PreprocessNode({ data }: { data: { label: string } }) { + return ( +
+
+
+ Preprocess +
+

Scale and transform data

+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/nodes/TrainTestSplitNode.tsx b/frontend/src/components/nodes/TrainTestSplitNode.tsx new file mode 100644 index 0000000..931a6eb --- /dev/null +++ b/frontend/src/components/nodes/TrainTestSplitNode.tsx @@ -0,0 +1,21 @@ +"use client"; +import { Handle, Position } from "@xyflow/react"; + +export default function TrainTestSplitNode({ data }: { data: { label: string; testSize?: string } }) { + return ( +
+
+
+ +
+ Train-Test Split +
+

Split data into train and test sets

+ {data.testSize && ( +

Test Size: {data.testSize}

+ )} + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/store/pipelineStore.ts b/frontend/src/store/pipelineStore.ts index e69de29..726f598 100644 --- a/frontend/src/store/pipelineStore.ts +++ b/frontend/src/store/pipelineStore.ts @@ -0,0 +1,29 @@ +import { create } from "zustand"; +import { addEdge, applyNodeChanges, applyEdgeChanges } from "@xyflow/react"; +import type { Node, Edge, Connection, NodeChange, EdgeChange } from "@xyflow/react"; + +interface PipelineStore { + nodes: Node[]; + edges: Edge[]; + onNodesChange: (changes: NodeChange[]) => void; + onEdgesChange: (changes: EdgeChange[]) => void; + onConnect: (connection: Connection) => void; + addNode: (node: Node) => void; +} + +export const usePipelineStore = create((set) => ({ + nodes: [], + edges: [], + + onNodesChange: (changes) => + set((state) => ({ nodes: applyNodeChanges(changes, state.nodes) })), + + onEdgesChange: (changes) => + set((state) => ({ edges: applyEdgeChanges(changes, state.edges) })), + + onConnect: (connection) => + set((state) => ({ edges: addEdge(connection, state.edges) })), + + addNode: (node) => + set((state) => ({ nodes: [...state.nodes, node] })), +})); \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 3a13f90..cf9c65d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,7 @@ } ], "paths": { - "@/*": ["./*"] + "@/*": ["./src/*"] } }, "include": [ From d645498552dce222246dfd9449b359e2204f9638 Mon Sep 17 00:00:00 2001 From: vedikaagarwal28 Date: Fri, 22 May 2026 00:17:00 +0530 Subject: [PATCH 2/2] feat: add backend integration and pipeline execution --- backend/app/main.py | 9 +++ frontend/app/page.tsx | 80 ++++++++++++++++--- frontend/src/components/Canvas.tsx | 2 +- frontend/src/components/Sidebar.tsx | 2 +- .../components/nodes/TrainTestSplitNode.tsx | 7 +- frontend/src/lib/api.ts | 38 +++++++++ 6 files changed, 123 insertions(+), 15 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index c3e38e7..e2961cb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,8 +1,17 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from app.routes.pipeline import router as pipeline_router app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + @app.get("/") def root(): return {"message": "Backend running"} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index a2b8e9c..62a4dd0 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,15 +1,54 @@ "use client"; +import { useState } from "react"; import Canvas from "@/components/Canvas"; import Sidebar from "@/components/Sidebar"; +import { usePipelineStore } from "@/store/pipelineStore"; +import { runPipeline } from "@/lib/api"; export default function Home() { + const { nodes, edges } = usePipelineStore(); + const [running, setRunning] = useState(false); + const [result, setResult] = useState | null>(null); + const [error, setError] = useState(null); + + const handleRun = async () => { + setRunning(true); + setError(null); + setResult(null); + try { + const payload = { + nodes: nodes.map((n) => ({ + id: n.id, + type: n.type as string, + config: n.type === "model" + ? { algorithm: "logistic_regression" } + : n.type === "dataset" + ? { dataset: "iris" } + : n.type === "train_test_split" + ? { test_size: 0.2, random_state: 42 } + : n.type === "preprocess" + ? { scale_factor: 1 } + : {}, + })), + edges: edges.map((e) => ({ + source: e.source, + target: e.target, + })), + }; + const res = await runPipeline(payload); + setResult(res.results); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : "Something went wrong"); + } finally { + setRunning(false); + } + }; + return (
{/* Topbar */}
- - {/* Left: logo + title */}
@@ -17,15 +56,18 @@ export default function Home() { ML Workflow Builder
- {/* Right: Save + Run Pipeline */}
-
@@ -52,11 +94,31 @@ export default function Home() { {/* Sidebar */} - {/* Canvas */} -
- -
+ {/* Main canvas + output */} +
+ {/* Canvas */} +
+ +
+ + {/* Output panel — only shows after run */} + {(result || error) && ( +
+
+ Output + {result && Success} + {error && Error} +
+ {error &&

{error}

} + {result && ( +
+                  {JSON.stringify(result, null, 2)}
+                
+ )} +
+ )} +
); diff --git a/frontend/src/components/Canvas.tsx b/frontend/src/components/Canvas.tsx index 242dd17..6e743b7 100644 --- a/frontend/src/components/Canvas.tsx +++ b/frontend/src/components/Canvas.tsx @@ -12,7 +12,7 @@ const nodeTypes = { dataset: DatasetNode, preprocess: PreprocessNode, model: ModelNode, - trainTestSplit: TrainTestSplitNode, + train_test_split: TrainTestSplitNode, }; export default function Canvas() { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 39ccf64..cb37838 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -6,7 +6,7 @@ const categories = [ label: "Data", nodes: [ { type: "dataset", label: "Dataset", desc: "Load a dataset", color: "#7c3aed", bg: "rgba(124,58,237,0.12)", border: "rgba(124,58,237,0.25)", icon: }, - { type: "trainTestSplit", label: "Train-Test Split", desc: "Split data into train and test sets", color: "#6366f1", bg: "rgba(99,102,241,0.12)", border: "rgba(99,102,241,0.25)", icon: <> }, + { type: "train_test_split", label: "Train-Test Split", desc: "Split data into train and test sets", color: "#6366f1", bg: "rgba(99,102,241,0.12)", border: "rgba(99,102,241,0.25)", icon: <> }, ], }, { diff --git a/frontend/src/components/nodes/TrainTestSplitNode.tsx b/frontend/src/components/nodes/TrainTestSplitNode.tsx index 931a6eb..08e6bcd 100644 --- a/frontend/src/components/nodes/TrainTestSplitNode.tsx +++ b/frontend/src/components/nodes/TrainTestSplitNode.tsx @@ -2,6 +2,8 @@ import { Handle, Position } from "@xyflow/react"; export default function TrainTestSplitNode({ data }: { data: { label: string; testSize?: string } }) { + const meta = data.testSize ? `Split data (test: ${data.testSize})` : "Split data into train/test sets"; + return (
@@ -10,10 +12,7 @@ export default function TrainTestSplitNode({ data }: { data: { label: string; te
Train-Test Split
-

Split data into train and test sets

- {data.testSize && ( -

Test Size: {data.testSize}

- )} +

{meta}

diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e69de29..ef620e7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -0,0 +1,38 @@ +const BASE_URL = "http://localhost:8000"; + +interface Node { + id: string; + type: string; + config: Record; +} + +interface Edge { + source: string; + target: string; +} + +interface PipelinePayload { + nodes: Node[]; + edges: Edge[]; +} + +export async function runPipeline(payload: PipelinePayload) { + const response = await fetch(`${BASE_URL}/run_pipeline`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Pipeline execution failed"); + } + + return response.json(); +} + +export async function getNodes() { + const response = await fetch(`${BASE_URL}/nodes`); + if (!response.ok) throw new Error("Failed to fetch nodes"); + return response.json(); +} \ No newline at end of file