=> {
+ return new Promise((resolve) => {
+ const startTime = Date.now();
+
+ const checkBlock = () => {
+ const block = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCK_CARD_PREFIX,
+ );
+ if (block) {
+ resolve(block);
+ } else if (Date.now() - startTime > timeout) {
+ resolve(null);
+ } else {
+ setTimeout(checkBlock, TUTORIAL_CONFIG.ELEMENT_CHECK_INTERVAL);
+ }
+ };
+ checkBlock();
+ });
+};
+
+/**
+ * Sets focus on an input element
+ */
+export const focusElement = (selector: string): void => {
+ const element = document.querySelector(selector) as HTMLElement;
+ if (element) {
+ element.focus();
+ }
+};
+
+/**
+ * Scrolls an element into view smoothly
+ */
+export const scrollIntoView = (selector: string): void => {
+ const element = document.querySelector(selector);
+ if (element) {
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ }
+};
+
+/**
+ * Types text into an input element with event dispatch
+ */
+export const typeIntoInput = (selector: string, text: string) => {
+ const input = document.querySelector(selector) as HTMLInputElement;
+ if (input) {
+ input.focus();
+ input.value = text;
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+};
+
+/**
+ * Creates a mutation observer to watch for element appearance
+ */
+export const observeElement = (
+ selector: string,
+ callback: (element: Element) => void,
+): MutationObserver => {
+ const observer = new MutationObserver((mutations, obs) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ callback(element);
+ obs.disconnect();
+ }
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+
+ // Also check immediately
+ const element = document.querySelector(selector);
+ if (element) {
+ callback(element);
+ observer.disconnect();
+ }
+
+ return observer;
+};
+
+/**
+ * Watches for search input changes and calls callback when target is typed
+ */
+export const watchSearchInput = (
+ targetValue: string,
+ onMatch: () => void,
+): (() => void) => {
+ const input = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT,
+ ) as HTMLInputElement;
+ if (!input) return () => {};
+
+ let hasMatched = false;
+
+ const handler = () => {
+ if (hasMatched) return;
+
+ const currentValue = input.value.toLowerCase().trim();
+ const target = targetValue.toLowerCase().trim();
+
+ // Match when user types at least 4 characters that match
+ if (currentValue.length >= 4 && target.startsWith(currentValue)) {
+ hasMatched = true;
+ onMatch();
+ }
+ };
+
+ input.addEventListener("input", handler);
+
+ return () => {
+ input.removeEventListener("input", handler);
+ };
+};
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/highlights.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/highlights.ts
new file mode 100644
index 000000000000..1cc7a22352a2
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/highlights.ts
@@ -0,0 +1,80 @@
+/**
+ * Highlight and animation helpers for the tutorial
+ */
+
+import { CSS_CLASSES, TUTORIAL_SELECTORS } from "../constants";
+
+/**
+ * Disables all blocks except the target block
+ */
+export const disableOtherBlocks = (targetBlockSelector: string) => {
+ document
+ .querySelectorAll(TUTORIAL_SELECTORS.BLOCK_CARD_PREFIX)
+ .forEach((block) => {
+ const isTarget = block.matches(targetBlockSelector);
+ block.classList.toggle(CSS_CLASSES.DISABLE, !isTarget);
+ block.classList.toggle(CSS_CLASSES.HIGHLIGHT, isTarget);
+ });
+};
+
+/**
+ * Enables all blocks (removes disable and highlight classes)
+ */
+export const enableAllBlocks = () => {
+ document
+ .querySelectorAll(TUTORIAL_SELECTORS.BLOCK_CARD_PREFIX)
+ .forEach((block) => {
+ block.classList.remove(
+ CSS_CLASSES.DISABLE,
+ CSS_CLASSES.HIGHLIGHT,
+ CSS_CLASSES.PULSE,
+ );
+ });
+};
+
+/**
+ * Adds highlight class to an element
+ */
+export const highlightElement = (selector: string) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ element.classList.add(CSS_CLASSES.HIGHLIGHT);
+ }
+};
+
+/**
+ * Removes highlight from all elements
+ */
+export const removeAllHighlights = () => {
+ document.querySelectorAll(`.${CSS_CLASSES.HIGHLIGHT}`).forEach((el) => {
+ el.classList.remove(CSS_CLASSES.HIGHLIGHT);
+ });
+ document.querySelectorAll(`.${CSS_CLASSES.PULSE}`).forEach((el) => {
+ el.classList.remove(CSS_CLASSES.PULSE);
+ });
+};
+
+/**
+ * Adds pulse animation to an element
+ */
+export const pulseElement = (selector: string) => {
+ const element = document.querySelector(selector);
+ if (element) {
+ element.classList.add(CSS_CLASSES.PULSE);
+ }
+};
+
+/**
+ * Highlights the first matching block in search results
+ */
+export const highlightFirstBlockInSearch = () => {
+ const firstBlock = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCK_CARD_PREFIX,
+ );
+ if (firstBlock) {
+ firstBlock.classList.add(CSS_CLASSES.PULSE);
+ // Scroll it into view
+ firstBlock.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+};
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/index.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/index.ts
new file mode 100644
index 000000000000..5d5d5806b5d1
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/index.ts
@@ -0,0 +1,77 @@
+/**
+ * Tutorial helpers - re-exports all helper modules
+ */
+
+// DOM helpers
+export {
+ waitForElement,
+ waitForInputValue,
+ waitForSearchResult,
+ waitForAnyBlockCard,
+ focusElement,
+ scrollIntoView,
+ typeIntoInput,
+ observeElement,
+ watchSearchInput,
+} from "./dom";
+
+// Highlight helpers
+export {
+ disableOtherBlocks,
+ enableAllBlocks,
+ highlightElement,
+ removeAllHighlights,
+ pulseElement,
+ highlightFirstBlockInSearch,
+} from "./highlights";
+
+// Block helpers
+export {
+ prefetchTutorialBlocks,
+ getPrefetchedBlock,
+ clearPrefetchedBlocks,
+ addPrefetchedBlock,
+ getNodeByBlockId,
+ addAgentIOBlocks,
+ getFormContainerSelector,
+ getFormContainerElement,
+} from "./blocks";
+
+// Canvas helpers
+export {
+ waitForNodeOnCanvas,
+ waitForNodesCount,
+ getNodesCount,
+ getFirstNode,
+ getNodeById,
+ nodeHasValues,
+ fitViewToScreen,
+} from "./canvas";
+
+// Connection helpers
+export { isConnectionMade } from "./connections";
+
+// Menu helpers
+export {
+ forceBlockMenuOpen,
+ openBlockMenu,
+ closeBlockMenu,
+ clearBlockMenuSearch,
+} from "./menu";
+
+// Save helpers
+export {
+ openSaveControl,
+ closeSaveControl,
+ forceSaveOpen,
+ clickSaveButton,
+ isAgentSaved,
+} from "./save";
+
+// State helpers
+export {
+ handleTutorialCancel,
+ handleTutorialSkip,
+ handleTutorialComplete,
+} from "./state";
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/menu.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/menu.ts
new file mode 100644
index 000000000000..fba9bce0f9ca
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/menu.ts
@@ -0,0 +1,42 @@
+/**
+ * Block menu helpers for the tutorial
+ */
+
+import { TUTORIAL_SELECTORS } from "../constants";
+import { useControlPanelStore } from "../../../../stores/controlPanelStore";
+
+/**
+ * Forces the block menu to stay open during tutorial
+ */
+export const forceBlockMenuOpen = (force: boolean) => {
+ useControlPanelStore.getState().setForceOpenBlockMenu(force);
+};
+
+/**
+ * Opens the block menu
+ */
+export const openBlockMenu = () => {
+ useControlPanelStore.getState().setBlockMenuOpen(true);
+};
+
+/**
+ * Closes the block menu
+ */
+export const closeBlockMenu = () => {
+ useControlPanelStore.getState().setBlockMenuOpen(false);
+ useControlPanelStore.getState().setForceOpenBlockMenu(false);
+};
+
+/**
+ * Clears the search input in block menu
+ */
+export const clearBlockMenuSearch = () => {
+ const input = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT,
+ ) as HTMLInputElement;
+ if (input) {
+ input.value = "";
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ }
+};
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/save.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/save.ts
new file mode 100644
index 000000000000..797374b5583b
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/save.ts
@@ -0,0 +1,51 @@
+/**
+ * Save control helpers for the tutorial
+ */
+
+import { TUTORIAL_SELECTORS } from "../constants";
+import { useControlPanelStore } from "../../../../stores/controlPanelStore";
+
+/**
+ * Opens the save control popover
+ */
+export const openSaveControl = () => {
+ useControlPanelStore.getState().setSaveControlOpen(true);
+};
+
+/**
+ * Closes the save control popover
+ */
+export const closeSaveControl = () => {
+ useControlPanelStore.getState().setSaveControlOpen(false);
+ useControlPanelStore.getState().setForceOpenSave(false);
+};
+
+/**
+ * Forces the save control to stay open during tutorial
+ */
+export const forceSaveOpen = (force: boolean) => {
+ useControlPanelStore.getState().setForceOpenSave(force);
+};
+
+/**
+ * Simulates a click on the save button
+ */
+export const clickSaveButton = () => {
+ const saveButton = document.querySelector(
+ TUTORIAL_SELECTORS.SAVE_AGENT_BUTTON,
+ ) as HTMLButtonElement;
+ if (saveButton && !saveButton.disabled) {
+ saveButton.click();
+ }
+};
+
+/**
+ * Check if the agent has been saved (by checking if version exists)
+ */
+export const isAgentSaved = (): boolean => {
+ const versionInput = document.querySelector(
+ '[data-testid="save-control-version-output"]',
+ ) as HTMLInputElement;
+ return !!(versionInput && versionInput.value && versionInput.value !== "-");
+};
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/state.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/state.ts
new file mode 100644
index 000000000000..4c2505c69bfe
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/helpers/state.ts
@@ -0,0 +1,47 @@
+/**
+ * Tutorial state management helpers
+ */
+
+import { Key, storage } from "@/services/storage/local-storage";
+import { closeBlockMenu } from "./menu";
+import { closeSaveControl, forceSaveOpen } from "./save";
+import { removeAllHighlights, enableAllBlocks } from "./highlights";
+
+/**
+ * Handles tutorial cancellation
+ */
+export const handleTutorialCancel = (tour: any) => {
+ closeBlockMenu();
+ closeSaveControl();
+ forceSaveOpen(false);
+ removeAllHighlights();
+ enableAllBlocks();
+ tour.cancel();
+ storage.set(Key.SHEPHERD_TOUR, "canceled");
+};
+
+/**
+ * Handles tutorial skip
+ */
+export const handleTutorialSkip = (tour: any) => {
+ closeBlockMenu();
+ closeSaveControl();
+ forceSaveOpen(false);
+ removeAllHighlights();
+ enableAllBlocks();
+ tour.cancel();
+ storage.set(Key.SHEPHERD_TOUR, "skipped");
+};
+
+/**
+ * Handles tutorial completion
+ */
+export const handleTutorialComplete = () => {
+ closeBlockMenu();
+ closeSaveControl();
+ forceSaveOpen(false);
+ removeAllHighlights();
+ enableAllBlocks();
+ storage.set(Key.SHEPHERD_TOUR, "completed");
+};
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/icons.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/icons.ts
new file mode 100644
index 000000000000..19e133ef7d29
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/icons.ts
@@ -0,0 +1,7 @@
+// These are SVG Phosphor icons
+
+export const ICONS = {
+ ClickIcon: ` `,
+ Keyboard: ` `,
+ Drag: ` `,
+};
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/index.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/index.ts
new file mode 100644
index 000000000000..147c45c20851
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/index.ts
@@ -0,0 +1,64 @@
+import Shepherd from "shepherd.js";
+import { analytics } from "@/services/analytics";
+import { TUTORIAL_CONFIG } from "./constants";
+import { createTutorialSteps } from "./steps";
+import { injectTutorialStyles, removeTutorialStyles } from "./styles";
+import {
+ handleTutorialComplete,
+ handleTutorialCancel,
+ prefetchTutorialBlocks,
+ clearPrefetchedBlocks,
+} from "./helpers";
+
+/**
+ * Starts the interactive tutorial
+ */
+export const startTutorial = async () => {
+ // Prefetch Agent Input and Agent Output blocks at the start
+ await prefetchTutorialBlocks();
+
+ const tour = new Shepherd.Tour({
+ useModalOverlay: TUTORIAL_CONFIG.USE_MODAL_OVERLAY,
+ defaultStepOptions: {
+ cancelIcon: { enabled: true },
+ scrollTo: {
+ behavior: TUTORIAL_CONFIG.SCROLL_BEHAVIOR,
+ block: TUTORIAL_CONFIG.SCROLL_BLOCK,
+ },
+ classes: "new-builder-tour",
+ modalOverlayOpeningRadius: 4,
+ },
+ });
+
+ // Inject tutorial styles
+ injectTutorialStyles();
+
+ // Add all steps to the tour
+ const steps = createTutorialSteps(tour);
+ steps.forEach((step) => tour.addStep(step));
+
+ // Event handlers
+ tour.on("complete", () => {
+ handleTutorialComplete();
+ removeTutorialStyles();
+ clearPrefetchedBlocks(); // Clean up prefetched blocks
+ });
+
+ tour.on("cancel", () => {
+ handleTutorialCancel(tour);
+ removeTutorialStyles();
+ clearPrefetchedBlocks(); // Clean up prefetched blocks
+ });
+
+ // Track tutorial steps with google analytics
+ for (const step of tour.steps) {
+ step.on("show", () => {
+ console.debug("sendTutorialStep", step.id);
+ analytics.sendGAEvent("event", "tutorial_step_shown", {
+ value: step.id,
+ });
+ });
+ }
+
+ tour.start();
+};
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/agent-io.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/agent-io.ts
new file mode 100644
index 000000000000..e7ece97a4774
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/agent-io.ts
@@ -0,0 +1,276 @@
+/**
+ * Agent I/O steps - Steps 10-13
+ * Add agent input/output blocks and configure them
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS, BLOCK_IDS } from "../constants";
+import {
+ waitForElement,
+ waitForNodesCount,
+ fitViewToScreen,
+ highlightElement,
+ removeAllHighlights,
+ addAgentIOBlocks,
+ getNodeByBlockId,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+
+/**
+ * Creates the agent I/O steps
+ */
+export const createAgentIOSteps = (tour: any): StepOptions[] => [
+ {
+ id: "ask-add-agent-io-blocks",
+ title: "Add Agent Input & Output",
+ text: `
+
+
Great job configuring the Calculator!
+
Now we need to add Agent Input and Agent Output blocks to complete your agent.
+
+
+
These blocks are essential:
+
+ • Agent Input — Receives data when the agent runs
+ • Agent Output — Returns the result to the user
+
+
+
+
Can I add these blocks for you?
+
+ `,
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Yes, Add Blocks",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ {
+ id: "blocks-added",
+ title: "Blocks Added! ✅",
+ text: `
+
+
I've added Agent Input and Agent Output blocks to your canvas.
+
Now let's configure them and connect everything together.
+
+
You now have 3 blocks:
+
+ • Agent Input (for receiving data)
+ • Calculator (processes data)
+ • Agent Output (for sending results)
+
+
+
+ `,
+ beforeShowPromise: async () => {
+ addAgentIOBlocks();
+ await waitForNodesCount(3, 5000);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ fitViewToScreen();
+ },
+ buttons: [
+ {
+ text: "Let's configure them",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 12: Configure Agent Input Name
+ {
+ id: "configure-input-name",
+ title: "Configure Agent Input",
+ text: `
+
+
First, let's set up the Agent Input block.
+
+
+
⚠️ Required:
+
+
+ ○ Enter a Name for the input (e.g., "number_a")
+
+
+
+ ${banner(ICONS.ClickIcon, "Fill in the Name field in this block")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.AGENT_INPUT_NODE_FORM_CONTAINER,
+ on: "right",
+ },
+ when: {
+ show: () => {
+ // Get the form container and manually position the popover
+ const formContainer = document.querySelector(
+ TUTORIAL_SELECTORS.AGENT_INPUT_NODE_FORM_CONTAINER,
+ );
+
+ // Get the Shepherd popover element and position it
+ const popover = document.querySelector(".shepherd-element");
+ if (formContainer && popover) {
+ const rect = formContainer.getBoundingClientRect();
+ (popover as HTMLElement).style.position = "fixed";
+ (popover as HTMLElement).style.left = `${rect.left - 320}px`; // Position to the left
+ (popover as HTMLElement).style.top = `${rect.top}px`;
+ }
+
+ const checkInterval = setInterval(() => {
+ const node = getNodeByBlockId(BLOCK_IDS.AGENT_INPUT);
+ if (!node) return;
+
+ const hardcodedValues = node.data?.hardcodedValues || {};
+ const hasName =
+ hardcodedValues.name && hardcodedValues.name.trim() !== "";
+
+ const reqName = document.querySelector("#req-input-name .req-icon");
+ if (reqName) reqName.textContent = hasName ? "✓" : "○";
+
+ // Fix: Explicitly set the correct color class instead of just toggling
+ const reqNameEl = document.querySelector("#req-input-name");
+ if (reqNameEl) {
+ if (hasName) {
+ reqNameEl.classList.remove("text-amber-700");
+ reqNameEl.classList.add("text-green-700");
+ } else {
+ reqNameEl.classList.remove("text-green-700");
+ reqNameEl.classList.add("text-amber-700");
+ }
+ }
+
+ const nextBtn = document.querySelector(
+ ".shepherd-button-primary",
+ ) as HTMLButtonElement;
+ if (nextBtn) {
+ nextBtn.style.opacity = hasName ? "1" : "0.5";
+ nextBtn.style.pointerEvents = hasName ? "auto" : "none";
+ }
+ }, 300);
+
+ (window as any).__tutorialInputNameInterval = checkInterval;
+ },
+ hide: () => {
+ removeAllHighlights();
+ if ((window as any).__tutorialInputNameInterval) {
+ clearInterval((window as any).__tutorialInputNameInterval);
+ delete (window as any).__tutorialInputNameInterval;
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Continue",
+ action: () => {
+ const node = getNodeByBlockId(BLOCK_IDS.AGENT_INPUT);
+ if (!node) return;
+ const hasName = node.data?.hardcodedValues?.name?.trim();
+ if (hasName) tour.next();
+ },
+ classes: "shepherd-button-primary",
+ },
+ ],
+ },
+
+ // STEP 13: Configure Agent Output Name
+ {
+ id: "configure-output-name",
+ title: "Configure Agent Output",
+ text: `
+
+
Now, let's set up the Agent Output block.
+
+
+
⚠️ Required:
+
+
+ ○ Enter a Name for the output (e.g., "result")
+
+
+
+ ${banner(ICONS.ClickIcon, "Fill in the Name field in this block")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.NAME_FIELD_OUTPUT_NODE,
+ on: "bottom",
+ },
+ modalOverlayOpeningPadding: 10,
+ when: {
+ show: () => {
+ // Poll for name being set
+ const checkInterval = setInterval(() => {
+ const node = getNodeByBlockId(BLOCK_IDS.AGENT_OUTPUT);
+ if (!node) return;
+
+ const hardcodedValues = node.data?.hardcodedValues || {};
+ const hasName =
+ hardcodedValues.name && hardcodedValues.name.trim() !== "";
+
+ // Update requirement icon
+ const reqName = document.querySelector("#req-output-name .req-icon");
+ if (reqName) reqName.textContent = hasName ? "✓" : "○";
+
+ // Fix: Explicitly set the correct color class instead of just toggling
+ const reqNameEl = document.querySelector("#req-output-name");
+ if (reqNameEl) {
+ if (hasName) {
+ reqNameEl.classList.remove("text-amber-700");
+ reqNameEl.classList.add("text-green-700");
+ } else {
+ reqNameEl.classList.remove("text-green-700");
+ reqNameEl.classList.add("text-amber-700");
+ }
+ }
+
+ const nextBtn = document.querySelector(
+ ".shepherd-button-primary",
+ ) as HTMLButtonElement;
+ if (nextBtn) {
+ nextBtn.style.opacity = hasName ? "1" : "0.5";
+ nextBtn.style.pointerEvents = hasName ? "auto" : "none";
+ }
+ nextBtn.disabled = !hasName;
+ }, 300);
+
+ (window as any).__tutorialOutputNameInterval = checkInterval;
+ },
+ hide: () => {
+ removeAllHighlights();
+ if ((window as any).__tutorialOutputNameInterval) {
+ clearInterval((window as any).__tutorialOutputNameInterval);
+ delete (window as any).__tutorialOutputNameInterval;
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Continue",
+ action: () => {
+ const node = getNodeByBlockId(BLOCK_IDS.AGENT_OUTPUT);
+ if (!node) return;
+ const hasName = node.data?.hardcodedValues?.name?.trim();
+ if (hasName) tour.next();
+ },
+ classes: "shepherd-button-primary",
+ },
+ ],
+ },
+];
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-basics.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-basics.ts
new file mode 100644
index 000000000000..43087c23e923
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-basics.ts
@@ -0,0 +1,126 @@
+/**
+ * Block basics steps - Steps 6-8
+ * Understanding blocks, input handles, output handles
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS } from "../constants";
+import {
+ waitForElement,
+ waitForNodeOnCanvas,
+ closeBlockMenu,
+ fitViewToScreen,
+ highlightElement,
+ removeAllHighlights,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+
+/**
+ * Creates the block basics steps
+ */
+export const createBlockBasicsSteps = (tour: any): StepOptions[] => [
+ // STEP 6: Focus on New Block
+ {
+ id: "focus-new-block",
+ title: "Your First Block!",
+ text: `
+
+
Excellent! This is your Calculator Block .
+
Let's explore how blocks work.
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.REACT_FLOW_NODE,
+ on: "right",
+ },
+ beforeShowPromise: async () => {
+ closeBlockMenu();
+ await waitForNodeOnCanvas(5000);
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ fitViewToScreen();
+ },
+ when: {
+ show: () => {
+ const node = document.querySelector(TUTORIAL_SELECTORS.REACT_FLOW_NODE);
+ if (node) {
+ highlightElement(TUTORIAL_SELECTORS.REACT_FLOW_NODE);
+ }
+ },
+ hide: () => {
+ removeAllHighlights();
+ },
+ },
+ buttons: [
+ {
+ text: "Show me",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 7: Input Handles
+ {
+ id: "input-handles",
+ title: "Input Handles",
+ text: `
+
+
On the left side of the block are input handles .
+
These are where data flows into the block from other blocks.
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.NODE_INPUT_HANDLE,
+ on: "bottom",
+ },
+ classes: "new-builder-tour input-handles-step",
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.NODE_INPUT_HANDLE, 3000).catch(
+ () => {},
+ ),
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 8: Output Handles
+ {
+ id: "output-handles",
+ title: "Output Handles",
+ text: `
+
+
On the right side is the output handle .
+
This is where the result flows out to connect to other blocks.
+ ${banner(ICONS.Drag, "You can drag from output to input handler to connect blocks")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.NODE_OUTPUT_HANDLE,
+ on: "right",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.NODE_OUTPUT_HANDLE, 3000).catch(
+ () => {},
+ ),
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Next →",
+ action: () => tour.next(),
+ },
+ ],
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-menu.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-menu.ts
new file mode 100644
index 000000000000..262db58ee3e3
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/block-menu.ts
@@ -0,0 +1,219 @@
+/**
+ * Block menu steps - Steps 2-5
+ * Opening menu, overview, search, and select calculator
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_CONFIG, TUTORIAL_SELECTORS, BLOCK_IDS } from "../constants";
+import {
+ waitForElement,
+ forceBlockMenuOpen,
+ focusElement,
+ highlightElement,
+ removeAllHighlights,
+ disableOtherBlocks,
+ enableAllBlocks,
+ pulseElement,
+ highlightFirstBlockInSearch,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+import { useNodeStore } from "../../../../stores/nodeStore";
+
+/**
+ * Creates the block menu steps
+ */
+export const createBlockMenuSteps = (tour: any): StepOptions[] => [
+ // STEP 2: Open Block Menu
+ {
+ id: "open-block-menu",
+ title: "Open the Block Menu",
+ text: `
+
+
Let's start by opening the Block Menu.
+ ${banner(ICONS.ClickIcon, "Click this button to open the menu")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.BLOCKS_TRIGGER,
+ on: "right",
+ },
+ advanceOn: {
+ selector: TUTORIAL_SELECTORS.BLOCKS_TRIGGER,
+ event: "click",
+ },
+ buttons: [],
+ when: {
+ show: () => {
+ highlightElement(TUTORIAL_SELECTORS.BLOCKS_TRIGGER);
+ },
+ hide: () => {
+ removeAllHighlights();
+ },
+ },
+ },
+
+ // STEP 3: Block Menu Overview
+ {
+ id: "block-menu-overview",
+ title: "The Block Menu",
+ text: `
+
+
This is the Block Menu — your toolbox for building agents.
+
Here you'll find:
+
+ Input Blocks — Entry points for data
+ Action Blocks — Processing and AI operations
+ Output Blocks — Results and responses
+ Integrations — Third-party service blocks
+ Library Agents — Your personal agents
+ Marketplace Agents — Community agents
+
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.BLOCKS_CONTENT,
+ on: "left",
+ },
+ beforeShowPromise: () => waitForElement(TUTORIAL_SELECTORS.BLOCKS_CONTENT),
+ when: {
+ show: () => forceBlockMenuOpen(true),
+ },
+ buttons: [
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 4: Search for Calculator Block
+ {
+ id: "search-calculator",
+ title: "Search for a Block",
+ text: `
+
+
Let's add a Calculator block to start.
+ ${banner(ICONS.Keyboard, "Type Calculator in the search bar")}
+
The search will filter blocks as you type.
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT_BOX,
+ on: "bottom",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT_BOX),
+ when: {
+ show: () => {
+ forceBlockMenuOpen(true);
+ setTimeout(() => {
+ focusElement(TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT_BOX);
+ }, 100);
+
+ const checkForCalculator = setInterval(() => {
+ const calcBlock = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR_IN_SEARCH,
+ );
+ if (calcBlock) {
+ clearInterval(checkForCalculator);
+
+ // Blur the search input to prevent further typing
+ const searchInput = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCKS_SEARCH_INPUT,
+ ) as HTMLInputElement;
+ if (searchInput) {
+ searchInput.blur();
+ }
+
+ disableOtherBlocks(
+ TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR_IN_SEARCH,
+ );
+ pulseElement(TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR_IN_SEARCH);
+ calcBlock.scrollIntoView({ behavior: "smooth", block: "center" });
+ setTimeout(() => {
+ tour.next();
+ }, 300);
+ }
+ }, TUTORIAL_CONFIG.ELEMENT_CHECK_INTERVAL);
+
+ (window as any).__tutorialCalcInterval = checkForCalculator;
+ },
+ hide: () => {
+ if ((window as any).__tutorialCalcInterval) {
+ clearInterval((window as any).__tutorialCalcInterval);
+ delete (window as any).__tutorialCalcInterval;
+ }
+ enableAllBlocks();
+ },
+ },
+ buttons: [],
+ },
+
+ // STEP 5: Select Calculator Block
+ {
+ id: "select-calculator",
+ title: "Add the Calculator Block",
+ text: `
+
+
You should see the Calculator block in the results.
+ ${banner(ICONS.ClickIcon, "Click on the Calculator block to add it")}
+ ${banner(ICONS.Drag, "You can also drag blocks onto the canvas", "bg-zinc-100 ring-1 ring-zinc-600 text-zinc-700")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR,
+ on: "left",
+ },
+ beforeShowPromise: async () => {
+ forceBlockMenuOpen(true);
+ await waitForElement(TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR, 5000);
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ },
+ when: {
+ show: () => {
+ // Highlight any visible calculator block or the first block
+ const calcBlock = document.querySelector(
+ TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR,
+ );
+ if (calcBlock) {
+ disableOtherBlocks(TUTORIAL_SELECTORS.BLOCK_CARD_CALCULATOR);
+ } else {
+ // Highlight first available block
+ highlightFirstBlockInSearch();
+ }
+
+ // Calculator block_id from constants
+ const CALCULATOR_BLOCK_ID = BLOCK_IDS.CALCULATOR;
+
+ // Store initial node count to detect additions
+ const initialNodeCount = useNodeStore.getState().nodes.length;
+
+ // Subscribe to node store changes
+ const unsubscribe = useNodeStore.subscribe((state) => {
+ // Check if a new node was added
+ if (state.nodes.length > initialNodeCount) {
+ // Find if a Calculator node was added
+ const calculatorNode = state.nodes.find(
+ (node) => node.data?.block_id === CALCULATOR_BLOCK_ID,
+ );
+
+ if (calculatorNode) {
+ // Unsubscribe to prevent multiple triggers
+ unsubscribe();
+
+ // Clean up and close block menu
+ enableAllBlocks();
+ forceBlockMenuOpen(false);
+ tour.next();
+ }
+ }
+ });
+
+ // Store unsubscribe function on the step for cleanup in hide
+ (tour.getCurrentStep() as any)._nodeUnsubscribe = unsubscribe;
+ },
+ },
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/completion.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/completion.ts
new file mode 100644
index 000000000000..e09145c3d032
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/completion.ts
@@ -0,0 +1,142 @@
+/**
+ * Completion steps - Steps 22-25
+ * Canvas controls, keyboard shortcuts, next steps, congratulations
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS } from "../constants";
+import { waitForElement } from "../helpers";
+import { ICONS } from "../icons";
+
+/**
+ * Creates the completion steps
+ */
+export const createCompletionSteps = (tour: any): StepOptions[] => [
+ // STEP 22: Canvas Controls
+ {
+ id: "canvas-controls",
+ title: "Canvas Controls",
+ text: `
+
+
Use these controls to navigate:
+
+ +/− — Zoom in/out
+ Fit View — Center all blocks
+ Lock — Prevent accidental moves
+ Tutorial — Restart this anytime
+
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.CUSTOM_CONTROLS,
+ on: "right",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.CUSTOM_CONTROLS, 3000).catch(() => {}),
+ buttons: [
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 23: Keyboard Shortcuts
+ {
+ id: "keyboard-shortcuts",
+ title: "Keyboard Shortcuts",
+ text: `
+
+
Speed up your workflow with shortcuts:
+
+ Ctrl/Cmd + Z — Undo
+ Ctrl/Cmd + Y — Redo
+ Ctrl/Cmd + C — Copy block
+ Ctrl/Cmd + V — Paste block
+ Delete — Remove selected
+
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.UNDO_BUTTON,
+ on: "right",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.UNDO_BUTTON, 3000).catch(() => {}),
+ buttons: [
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 24: Next Steps
+ {
+ id: "next-steps",
+ title: "What's Next?",
+ text: `
+
+
You've built and run your first agent!
+
To build more complex agents:
+
+ Add multiple blocks and connect them
+ Try AI blocks for intelligent processing
+ Explore Integrations for external services
+ Use Marketplace Agents as starting points
+
+
+ `,
+ buttons: [
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+
+ // STEP 25: Congratulations
+ {
+ id: "congratulations",
+ title: "Congratulations!",
+ text: `
+
+
You've completed the AutoGPT Builder tutorial!
+
You now know how to:
+
+ ${ICONS.ClickIcon} Add blocks to the canvas
+ ${ICONS.ClickIcon} Configure block inputs and form fields
+ ${ICONS.ClickIcon} Connect blocks together
+ ${ICONS.ClickIcon} Save and run agents
+ ${ICONS.ClickIcon} View execution outputs
+
+
Happy building!
+
+ `,
+ when: {
+ show: () => {
+ const modal = document.querySelector(
+ ".shepherd-modal-overlay-container",
+ );
+ if (modal) {
+ (modal as HTMLElement).style.opacity = "0.3";
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Restart",
+ action: () => {
+ tour.cancel();
+ setTimeout(() => tour.start(), 100);
+ },
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Finish",
+ action: () => tour.complete(),
+ },
+ ],
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/configure-calculator.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/configure-calculator.ts
new file mode 100644
index 000000000000..8b4e48fd5028
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/configure-calculator.ts
@@ -0,0 +1,156 @@
+/**
+ * Configure calculator step - Step 9
+ * Enter values in calculator block
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS } from "../constants";
+import {
+ fitViewToScreen,
+ highlightElement,
+ removeAllHighlights,
+ getFirstNode,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+
+/**
+ * Creates the configure calculator step
+ */
+export const createConfigureCalculatorSteps = (tour: any): StepOptions[] => [
+ // STEP 9: Enter Values (Required)
+ {
+ id: "enter-values",
+ title: "Enter Values",
+ text: `
+
+
Now let's configure the block with actual values.
+
+
+
⚠️ Required to continue:
+
+
+ ○ Enter a number in field A (e.g., 10)
+
+
+ ○ Enter a number in field B (e.g., 5)
+
+
+ ○ Select an Operation (Add, Multiply, etc.)
+
+
+
+ ${banner(ICONS.ClickIcon, "Fill in all the required fields above")}
+
+ `,
+ beforeShowPromise: () => {
+ fitViewToScreen();
+ return Promise.resolve();
+ },
+ attachTo: {
+ element: TUTORIAL_SELECTORS.CALCULATOR_NODE_FORM_CONTAINER,
+ on: "right",
+ },
+ when: {
+ show: () => {
+ const node = getFirstNode();
+ if (node) {
+ highlightElement(`[data-id="custom-node-${node.id}"]`);
+ }
+
+ // Start polling to update requirements UI and button visibility
+ const checkInterval = setInterval(() => {
+ const node = getFirstNode();
+ if (!node) return;
+
+ const hardcodedValues = node.data?.hardcodedValues || {};
+ const hasA =
+ hardcodedValues.a !== undefined &&
+ hardcodedValues.a !== null &&
+ hardcodedValues.a !== "";
+ const hasB =
+ hardcodedValues.b !== undefined &&
+ hardcodedValues.b !== null &&
+ hardcodedValues.b !== "";
+ const hasOp =
+ hardcodedValues.operation !== undefined &&
+ hardcodedValues.operation !== null &&
+ hardcodedValues.operation !== "";
+
+ // Update requirement icons
+ const reqA = document.querySelector("#req-a .req-icon");
+ const reqB = document.querySelector("#req-b .req-icon");
+ const reqOp = document.querySelector("#req-op .req-icon");
+
+ if (reqA) reqA.textContent = hasA ? "✓" : "○";
+ if (reqB) reqB.textContent = hasB ? "✓" : "○";
+ if (reqOp) reqOp.textContent = hasOp ? "✓" : "○";
+
+ // Update styling for completed items
+ document
+ .querySelector("#req-a")
+ ?.classList.toggle("text-green-700", hasA);
+ document
+ .querySelector("#req-b")
+ ?.classList.toggle("text-green-700", hasB);
+ document
+ .querySelector("#req-op")
+ ?.classList.toggle("text-green-700", hasOp);
+
+ // Show/hide the next button based on completion
+ const nextBtn = document.querySelector(
+ ".shepherd-button-primary",
+ ) as HTMLButtonElement;
+ if (nextBtn) {
+ const allComplete = hasA && hasB && hasOp;
+ nextBtn.style.opacity = allComplete ? "1" : "0.5";
+ nextBtn.style.pointerEvents = allComplete ? "auto" : "none";
+ }
+ }, 300);
+
+ // Store interval ID for cleanup
+ (window as any).__tutorialCheckInterval = checkInterval;
+ },
+ hide: () => {
+ removeAllHighlights();
+ if ((window as any).__tutorialCheckInterval) {
+ clearInterval((window as any).__tutorialCheckInterval);
+ delete (window as any).__tutorialCheckInterval;
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Continue",
+ action: () => {
+ const node = getFirstNode();
+ if (!node) return;
+
+ const hardcodedValues = node.data?.hardcodedValues || {};
+ const hasA =
+ hardcodedValues.a !== undefined &&
+ hardcodedValues.a !== null &&
+ hardcodedValues.a !== "";
+ const hasB =
+ hardcodedValues.b !== undefined &&
+ hardcodedValues.b !== null &&
+ hardcodedValues.b !== "";
+ const hasOp =
+ hardcodedValues.operation !== undefined &&
+ hardcodedValues.operation !== null &&
+ hardcodedValues.operation !== "";
+
+ if (hasA && hasB && hasOp) {
+ tour.next();
+ }
+ },
+ classes: "shepherd-button-primary",
+ },
+ ],
+ },
+];
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/connections.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/connections.ts
new file mode 100644
index 000000000000..0649941ca82c
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/connections.ts
@@ -0,0 +1,375 @@
+import { StepOptions } from "shepherd.js";
+import { BLOCK_IDS, TUTORIAL_SELECTORS } from "../constants";
+import {
+ fitViewToScreen,
+ highlightElement,
+ removeAllHighlights,
+ pulseElement,
+ getNodeByBlockId,
+ isConnectionMade,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+import { useEdgeStore } from "../../../../stores/edgeStore";
+
+/**
+ * Creates the connection steps
+ */
+export const createConnectionSteps = (tour: any): StepOptions[] => {
+ let isConnecting = false;
+
+ // Helper to detect when user starts dragging from output handle
+ const handleMouseDown = () => {
+ isConnecting = true;
+ setTimeout(() => {
+ if (isConnecting) {
+ tour.next();
+ }
+ }, 100);
+ };
+
+ // Helper to reset connection state
+ const resetConnectionState = () => {
+ isConnecting = false;
+ };
+
+ return [
+ // STEP 14a: Highlight Agent Input's OUTPUT handle - start dragging
+ {
+ id: "connect-input-output-handle",
+ title: "Connect Agent Input → Calculator (Step 1)",
+ text: `
+
+
Now let's connect the blocks together!
+
+
+
Drag from the output:
+
Click and drag from the result output handle (right side) of Agent Input block.
+
+ ${banner(ICONS.Drag, "Click and drag from the highlighted output handle")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.INPUT_BLOCK_RESULT_OUTPUT_HANDLEER,
+ on: "right",
+ },
+ modalOverlayOpeningPadding: 10,
+ when: {
+ show: () => {
+ resetConnectionState();
+ const inputNode = getNodeByBlockId(BLOCK_IDS.AGENT_INPUT);
+ if (inputNode) {
+ const outputHandle = document.querySelector(
+ TUTORIAL_SELECTORS.INPUT_BLOCK_RESULT_OUTPUT_HANDLEER,
+ );
+ if (outputHandle) {
+ outputHandle.addEventListener("mousedown", handleMouseDown);
+ }
+ }
+ },
+ hide: () => {
+ removeAllHighlights();
+ const inputNode = getNodeByBlockId(BLOCK_IDS.AGENT_INPUT);
+ if (inputNode) {
+ const outputHandle = document.querySelector(
+ TUTORIAL_SELECTORS.INPUT_BLOCK_RESULT_OUTPUT_HANDLEER,
+ );
+ if (outputHandle) {
+ outputHandle.removeEventListener("mousedown", handleMouseDown);
+ }
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ ],
+ },
+
+ // STEP 14b: Highlight Calculator's INPUT handle - complete connection
+ {
+ id: "connect-input-to-calculator-target",
+ title: "Connect Agent Input → Calculator (Step 2)",
+ text: `
+
+
Now connect to the Calculator's input!
+
+
+
Drop on the input:
+
Drag to the A or B input handle (left side) of the Calculator block.
+
+
+
+ Waiting for connection...
+
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.INPUT_BLOCK_RESULT_OUTPUT_HANDLEER,
+ on: "bottom",
+ },
+ modalOverlayOpeningPadding: 10,
+ extraHighlights: [TUTORIAL_SELECTORS.CALCULATOR_NUMBER_A_INPUT_HANDLER],
+ when: {
+ show: () => {
+ // Subscribe to edge store changes to detect connection
+ const unsubscribe = useEdgeStore.subscribe(() => {
+ const connected = isConnectionMade(
+ BLOCK_IDS.AGENT_INPUT,
+ BLOCK_IDS.CALCULATOR,
+ );
+ const statusEl = document.querySelector("#connection-status-1");
+
+ if (connected && statusEl) {
+ statusEl.innerHTML = "✅ Connected!";
+ statusEl.classList.remove(
+ "bg-amber-100",
+ "ring-amber-500",
+ "text-amber-700",
+ );
+ statusEl.classList.add("bg-green-100", "text-green-700");
+
+ // Auto-advance after brief delay
+ setTimeout(() => {
+ unsubscribe();
+ tour.next();
+ }, 500);
+ }
+ });
+
+ (tour.getCurrentStep() as any)._edgeUnsubscribe = unsubscribe;
+
+ // Also handle mouseup to detect failed connection attempts
+ const handleMouseUp = (event: MouseEvent) => {
+ setTimeout(() => {
+ const connected = isConnectionMade(
+ BLOCK_IDS.AGENT_INPUT,
+ BLOCK_IDS.CALCULATOR,
+ );
+ if (!connected) {
+ // Connection failed, go back to output handle step
+ isConnecting = false;
+ tour.show("connect-input-output-handle");
+ }
+ }, 200);
+ };
+ document.addEventListener("mouseup", handleMouseUp, true);
+ (tour.getCurrentStep() as any)._mouseUpHandler = handleMouseUp;
+ },
+ hide: () => {
+ removeAllHighlights();
+ const step = tour.getCurrentStep() as any;
+ if (step?._edgeUnsubscribe) {
+ step._edgeUnsubscribe();
+ }
+ if (step?._mouseUpHandler) {
+ document.removeEventListener("mouseup", step._mouseUpHandler, true);
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.show("connect-input-output-handle"),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Skip (already connected)",
+ action: () => tour.next(),
+ classes: "shepherd-button-secondary",
+ },
+ ],
+ },
+
+ // STEP 15a: Highlight Calculator's OUTPUT handle - start dragging
+ {
+ id: "connect-calculator-output-handle",
+ title: "Connect Calculator → Agent Output (Step 1)",
+ text: `
+
+
Great! Now let's connect Calculator to Agent Output.
+
+
+
Drag from the output:
+
Click and drag from the result output handle (right side) of Calculator block.
+
+ ${banner(ICONS.Drag, "Click and drag from the highlighted output handle")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.CALCULATOR_RESULT_OUTPUT_HANDLEER,
+ on: "right",
+ },
+ beforeShowPromise: async () => {
+ fitViewToScreen();
+ return Promise.resolve();
+ },
+ when: {
+ show: () => {
+ resetConnectionState();
+ const calcNode = getNodeByBlockId(BLOCK_IDS.CALCULATOR);
+ if (calcNode) {
+ const outputHandle = document.querySelector(
+ TUTORIAL_SELECTORS.CALCULATOR_RESULT_OUTPUT_HANDLEER,
+ );
+ if (outputHandle) {
+ highlightElement(
+ TUTORIAL_SELECTORS.CALCULATOR_RESULT_OUTPUT_HANDLEER,
+ );
+ outputHandle.addEventListener("mousedown", handleMouseDown);
+ }
+ }
+ },
+ hide: () => {
+ removeAllHighlights();
+ const calcNode = getNodeByBlockId(BLOCK_IDS.CALCULATOR);
+ if (calcNode) {
+ const outputHandle = document.querySelector(
+ TUTORIAL_SELECTORS.CALCULATOR_RESULT_OUTPUT_HANDLEER,
+ );
+ if (outputHandle) {
+ outputHandle.removeEventListener("mousedown", handleMouseDown);
+ }
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.back(),
+ classes: "shepherd-button-secondary",
+ },
+ ],
+ },
+
+ // STEP 15b: Highlight Agent Output's INPUT handle - complete connection
+ {
+ id: "connect-calculator-to-output-target",
+ title: "Connect Calculator → Agent Output (Step 2)",
+ text: `
+
+
Now connect to the Agent Output's input!
+
+
+
Drop on the input:
+
Drag to the Value input handle (left side) of the Agent Output block.
+
+
+
+ Waiting for connection...
+
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.OUTPUT_VALUE_INPUT_HANDLEER,
+ on: "left",
+ },
+ when: {
+ show: () => {
+ const outputNode = getNodeByBlockId(BLOCK_IDS.AGENT_OUTPUT);
+
+ // Subscribe to edge store changes
+ const unsubscribe = useEdgeStore.subscribe(() => {
+ const connected = isConnectionMade(
+ BLOCK_IDS.CALCULATOR,
+ BLOCK_IDS.AGENT_OUTPUT,
+ );
+ const statusEl = document.querySelector("#connection-status-2");
+
+ if (connected && statusEl) {
+ statusEl.innerHTML = "✅ Connected!";
+ statusEl.classList.remove(
+ "bg-amber-100",
+ "ring-amber-500",
+ "text-amber-700",
+ );
+ statusEl.classList.add("bg-green-100", "text-green-700");
+
+ setTimeout(() => {
+ unsubscribe();
+ tour.next();
+ }, 500);
+ }
+ });
+
+ (tour.getCurrentStep() as any)._edgeUnsubscribe = unsubscribe;
+
+ // Handle failed connection attempts
+ const handleMouseUp = (event: MouseEvent) => {
+ setTimeout(() => {
+ const connected = isConnectionMade(
+ BLOCK_IDS.CALCULATOR,
+ BLOCK_IDS.AGENT_OUTPUT,
+ );
+ if (!connected) {
+ isConnecting = false;
+ tour.show("connect-calculator-output-handle");
+ }
+ }, 200);
+ };
+ document.addEventListener("mouseup", handleMouseUp, true);
+ (tour.getCurrentStep() as any)._mouseUpHandler = handleMouseUp;
+ },
+ hide: () => {
+ removeAllHighlights();
+ const step = tour.getCurrentStep() as any;
+ if (step?._edgeUnsubscribe) {
+ step._edgeUnsubscribe();
+ }
+ if (step?._mouseUpHandler) {
+ document.removeEventListener("mouseup", step._mouseUpHandler, true);
+ }
+ },
+ },
+ buttons: [
+ {
+ text: "Back",
+ action: () => tour.show("connect-calculator-output-handle"),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Skip (already connected)",
+ action: () => tour.next(),
+ classes: "shepherd-button-secondary",
+ },
+ ],
+ },
+
+ // STEP 16: Connections Complete (keep as-is)
+ {
+ id: "connections-complete",
+ title: "Connections Complete! 🎉",
+ text: `
+
+
Excellent! Your agent workflow is now connected:
+
+
+
+ Agent Input
+ →
+ Calculator
+ →
+ Agent Output
+
+
+
+
Data will flow from input, through the calculator, and out as the result.
+
Now let's save your agent!
+
+ `,
+ beforeShowPromise: async () => {
+ fitViewToScreen();
+ return Promise.resolve();
+ },
+ buttons: [
+ {
+ text: "Save My Agent",
+ action: () => tour.next(),
+ },
+ ],
+ },
+ ];
+};
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/index.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/index.ts
new file mode 100644
index 000000000000..ca633fcf1d9a
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/index.ts
@@ -0,0 +1,22 @@
+import { StepOptions } from "shepherd.js";
+import { createWelcomeSteps } from "./welcome";
+import { createBlockMenuSteps } from "./block-menu";
+import { createBlockBasicsSteps } from "./block-basics";
+import { createConfigureCalculatorSteps } from "./configure-calculator";
+import { createAgentIOSteps } from "./agent-io";
+import { createConnectionSteps } from "./connections";
+import { createSaveSteps } from "./save";
+import { createRunSteps } from "./run";
+import { createCompletionSteps } from "./completion";
+
+export const createTutorialSteps = (tour: any): StepOptions[] => [
+ ...createWelcomeSteps(tour), // Step 1
+ ...createBlockMenuSteps(tour), // Steps 2-5
+ ...createBlockBasicsSteps(tour), // Steps 6-8
+ ...createConfigureCalculatorSteps(tour), // Step 9
+ ...createAgentIOSteps(tour), // Steps 10-13
+ ...createConnectionSteps(tour), // Steps 14-16
+ ...createSaveSteps(tour), // Steps 17-18
+ ...createRunSteps(tour), // Steps 19-21
+ ...createCompletionSteps(tour), // Steps 22-25
+];
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/run.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/run.ts
new file mode 100644
index 000000000000..a7a81550bb68
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/run.ts
@@ -0,0 +1,128 @@
+/**
+ * Run steps - Steps 19-21
+ * Run the agent and check output
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS } from "../constants";
+import {
+ waitForElement,
+ fitViewToScreen,
+ removeAllHighlights,
+ pulseElement,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+
+/**
+ * Creates the run steps
+ */
+export const createRunSteps = (tour: any): StepOptions[] => [
+ // STEP 19: Run Button
+ {
+ id: "run-agent",
+ title: "Run Your Agent",
+ text: `
+
+
Your agent is saved! Now let's run it .
+ ${banner(ICONS.ClickIcon, "Click the Run button")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.RUN_BUTTON,
+ on: "top",
+ },
+ advanceOn: {
+ selector: TUTORIAL_SELECTORS.RUN_BUTTON,
+ event: "click",
+ },
+ beforeShowPromise: async () => {
+ await waitForElement(TUTORIAL_SELECTORS.RUN_BUTTON, 3000).catch(() => {});
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ },
+ buttons: [],
+ when: {
+ show: () => {
+ pulseElement(TUTORIAL_SELECTORS.RUN_BUTTON);
+ },
+ hide: () => {
+ removeAllHighlights();
+ },
+ },
+ },
+
+ // STEP 20: Wait for Execution
+ {
+ id: "wait-execution",
+ title: "Processing...",
+ text: `
+
+
Your agent is running! Watch the block for status updates.
+
The badge will show: Queued → Running → Completed
+
+ `,
+ beforeShowPromise: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ fitViewToScreen();
+ },
+ when: {
+ show: () => {
+ // Auto-advance when execution completes
+ const checkComplete = () => {
+ const completed = document.querySelector(
+ TUTORIAL_SELECTORS.BADGE_COMPLETED,
+ );
+ const output = document.querySelector(
+ TUTORIAL_SELECTORS.NODE_LATEST_OUTPUT,
+ );
+ if (completed || output) {
+ setTimeout(() => tour.next(), 500);
+ } else {
+ setTimeout(checkComplete, 500);
+ }
+ };
+ setTimeout(checkComplete, 1000);
+ },
+ },
+ buttons: [
+ {
+ text: "Skip wait",
+ action: () => tour.next(),
+ classes: "shepherd-button-secondary",
+ },
+ ],
+ },
+
+ // STEP 21: Check Output
+ {
+ id: "check-output",
+ title: "View the Output",
+ text: `
+
+
The block has finished! Check the output at the bottom of the block.
+
This shows the result of your calculation.
+ ${banner(ICONS.ClickIcon, "Every block displays its output after execution")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.NODE_LATEST_OUTPUT,
+ on: "top",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.NODE_LATEST_OUTPUT, 5000).catch(
+ () => {},
+ ),
+ when: {
+ show: () => {
+ fitViewToScreen();
+ },
+ },
+ buttons: [
+ {
+ text: "Next",
+ action: () => tour.next(),
+ },
+ ],
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/save.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/save.ts
new file mode 100644
index 000000000000..1aec51d6d0a0
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/save.ts
@@ -0,0 +1,83 @@
+/**
+ * Save steps - Steps 17-18
+ * Save the agent
+ */
+
+import { StepOptions } from "shepherd.js";
+import { TUTORIAL_SELECTORS } from "../constants";
+import {
+ waitForElement,
+ highlightElement,
+ removeAllHighlights,
+ forceSaveOpen,
+} from "../helpers";
+import { ICONS } from "../icons";
+import { banner } from "../styles";
+
+/**
+ * Creates the save steps
+ */
+export const createSaveSteps = (tour: any): StepOptions[] => [
+ // STEP 17: Save - Open Popover
+ {
+ id: "open-save",
+ title: "Save Your Agent",
+ text: `
+
+
Before running, we need to save your agent.
+ ${banner(ICONS.ClickIcon, "Click the Save button")}
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.SAVE_TRIGGER,
+ on: "right",
+ },
+ advanceOn: {
+ selector: TUTORIAL_SELECTORS.SAVE_TRIGGER,
+ event: "click",
+ },
+ beforeShowPromise: () =>
+ waitForElement(TUTORIAL_SELECTORS.SAVE_TRIGGER, 3000).catch(() => {}),
+ buttons: [],
+ when: {
+ show: () => {
+ highlightElement(TUTORIAL_SELECTORS.SAVE_TRIGGER);
+ },
+ hide: () => {
+ removeAllHighlights();
+ },
+ },
+ },
+
+ // STEP 18: Save - Fill Details
+ {
+ id: "save-details",
+ title: "Name Your Agent",
+ text: `
+
+
Give your agent a name and optional description.
+ ${banner(ICONS.ClickIcon, 'Enter a name and click "Save Agent"')}
+
Example: "My Calculator Agent"
+
+ `,
+ attachTo: {
+ element: TUTORIAL_SELECTORS.SAVE_CONTENT,
+ on: "right",
+ },
+ advanceOn: {
+ selector: TUTORIAL_SELECTORS.SAVE_AGENT_BUTTON,
+ event: "click",
+ },
+ beforeShowPromise: () => waitForElement(TUTORIAL_SELECTORS.SAVE_CONTENT),
+ when: {
+ show: () => {
+ forceSaveOpen(true);
+ },
+ hide: () => {
+ forceSaveOpen(false);
+ },
+ },
+ buttons: [],
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/welcome.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/welcome.ts
new file mode 100644
index 000000000000..6158aad05fbf
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/steps/welcome.ts
@@ -0,0 +1,41 @@
+/**
+ * Welcome step - Step 1
+ */
+
+import { StepOptions } from "shepherd.js";
+import { handleTutorialSkip } from "../helpers";
+
+/**
+ * Creates the welcome step
+ */
+export const createWelcomeSteps = (tour: any): StepOptions[] => [
+ {
+ id: "welcome",
+ title: "Welcome to AutoGPT Builder! 👋🏻",
+ text: `
+
+
This interactive tutorial will teach you how to build your first AI agent.
+
You'll learn how to:
+
+ - Add blocks to your workflow
+ - Understand block inputs and outputs
+ - Save and run your agent
+ - and much more...
+
+
Estimated time: 3-4 minutes
+
+ `,
+ buttons: [
+ {
+ text: "Skip Tutorial",
+ action: () => handleTutorialSkip(tour),
+ classes: "shepherd-button-secondary",
+ },
+ {
+ text: "Let's Begin",
+ action: () => tour.next(),
+ },
+ ],
+ },
+];
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/styles.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/styles.ts
new file mode 100644
index 000000000000..2ad6d2ef99f9
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/styles.ts
@@ -0,0 +1,31 @@
+/**
+ * Tutorial Styles for New Builder
+ *
+ * CSS file contains:
+ * - Dynamic classes: .new-builder-tutorial-disable, .new-builder-tutorial-highlight, .new-builder-tutorial-pulse
+ * - Shepherd.js overrides
+ *
+ * Typography (body, small, action, info, tip, warning) uses Tailwind utilities directly in steps.ts
+ */
+import "./tutorial.css";
+
+export const injectTutorialStyles = () => {
+ if (typeof window !== "undefined") {
+ document.documentElement.setAttribute("data-tutorial-styles", "loaded");
+ }
+};
+
+export const removeTutorialStyles = () => {
+ if (typeof window !== "undefined") {
+ document.documentElement.removeAttribute("data-tutorial-styles");
+ }
+};
+
+// Some resulable components
+
+export const banner = (icon: string, content: string, className?: string) => `
+
+ ${icon}
+ ${content}
+
+`;
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/tutorial.css b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/tutorial.css
new file mode 100644
index 000000000000..3e3521239d60
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/tutorial/tutorial.css
@@ -0,0 +1,218 @@
+/* ============================================
+ Tutorial Styles for New Builder
+ Prefix: new-builder- to avoid collision with old builder
+
+ Note: Typography classes (body, small, content, action, etc.)
+ are now using Tailwind utilities directly in steps.ts
+ ============================================ */
+
+/* ============================================
+ Disabled state - pointer-events disabled, dimmed
+ Class name: new-builder-tutorial-disable (matches CSS_CLASSES.DISABLE)
+ ============================================ */
+.new-builder-tutorial-disable {
+ pointer-events: none !important;
+ opacity: 0.4 !important;
+ filter: grayscale(50%);
+}
+
+/* ============================================
+ Highlight state - brings element to front with glow
+ Class name: new-builder-tutorial-highlight (matches CSS_CLASSES.HIGHLIGHT)
+ ============================================ */
+/* .new-builder-tutorial-highlight {
+ position: relative;
+ z-index: 10000 !important;
+ opacity: 1 !important;
+ filter: none !important;
+ box-shadow:
+ 0 0 0 4px white,
+ 0 0 0 6px #7c3aed,
+ 0 0 30px 8px rgba(124, 58, 237, 0.4) !important;
+ border-radius: 1rem;
+} */
+
+.new-builder-tutorial-highlight * {
+ opacity: 1 !important;
+ filter: none !important;
+}
+
+/* ============================================
+ Pulse animation for attention
+ Class name: new-builder-tutorial-pulse (matches CSS_CLASSES.PULSE)
+ ============================================ */
+.new-builder-tutorial-pulse {
+ animation: new-builder-tutorial-pulse 2s ease-in-out infinite;
+}
+
+@keyframes new-builder-tutorial-pulse {
+ 0%,
+ 100% {
+ box-shadow:
+ 0 0 0 4px white,
+ 0 0 0 6px #7c3aed,
+ 0 0 30px 8px rgba(124, 58, 237, 0.4);
+ }
+ 50% {
+ box-shadow:
+ 0 0 0 4px white,
+ 0 0 0 8px #8b5cf6,
+ 0 0 40px 12px rgba(124, 58, 237, 0.55);
+ }
+}
+
+/* ============================================
+ Shepherd.js - Main element container
+ SCOPED: Only applies to new builder tutorial (.new-builder-tour)
+ ============================================ */
+.shepherd-element.new-builder-tour {
+ max-width: 420px !important;
+ border-radius: 1rem !important;
+ box-shadow:
+ 0 0 0 1px rgba(0, 0, 0, 0.08),
+ 0px 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1) !important;
+ background: white !important;
+ font-family: var(--font-geist-sans), system-ui, sans-serif !important;
+}
+
+/* ============================================
+ Shepherd.js - Header section
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-header {
+ padding: 1rem 1.25rem 0.5rem !important;
+ border-radius: 1rem 1rem 0 0 !important;
+ background: transparent !important;
+}
+
+.shepherd-element.new-builder-tour .shepherd-title {
+ font-family: var(--font-poppins), system-ui, sans-serif !important;
+ font-size: 1rem !important;
+ font-weight: 600 !important;
+ line-height: 1.5rem !important;
+ color: #18181b !important; /* zinc-900 */
+}
+
+/* ============================================
+ Shepherd.js - Text content
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-text {
+ padding: 0 1.25rem 1rem !important;
+ color: #52525b !important; /* zinc-600 */
+}
+
+/* ============================================
+ Shepherd.js - Footer section
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-footer {
+ padding: 0.75rem 1.25rem 1rem !important;
+ border-top: 1px solid #e4e4e7 !important; /* zinc-200 */
+ gap: 0.5rem !important;
+ display: flex !important;
+ justify-content: flex-end !important;
+}
+
+/* ============================================
+ Shepherd.js - Button base styles
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-button {
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ white-space: nowrap !important;
+ font-family: var(--font-geist-sans), system-ui, sans-serif !important;
+ font-weight: 500 !important;
+ font-size: 0.875rem !important;
+ line-height: 1.25rem !important;
+ transition: all 150ms ease !important;
+ border-radius: 9999px !important; /* rounded-full */
+ min-width: 5rem !important;
+ padding: 0.5rem 1rem !important;
+ height: 2.25rem !important;
+ gap: 0.375rem !important;
+ cursor: pointer !important;
+}
+
+/* ============================================
+ Shepherd.js - Primary button
+ ============================================ */
+.shepherd-element.new-builder-tour
+ .shepherd-button:not(.shepherd-button-secondary) {
+ background-color: #27272a !important; /* zinc-800 */
+ border: 1px solid #27272a !important;
+ color: white !important;
+}
+
+.shepherd-element.new-builder-tour
+ .shepherd-button:not(.shepherd-button-secondary):hover {
+ background-color: #18181b !important; /* zinc-900 */
+ border-color: #18181b !important;
+}
+
+.shepherd-element.new-builder-tour
+ .shepherd-button:not(.shepherd-button-secondary):active {
+ transform: scale(0.98);
+}
+
+/* ============================================
+ Shepherd.js - Secondary button
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-button-secondary {
+ background-color: #f4f4f5 !important; /* zinc-100 */
+ border: 1px solid #f4f4f5 !important;
+ color: #52525b !important; /* zinc-600 */
+}
+
+.shepherd-element.new-builder-tour .shepherd-button-secondary:hover {
+ background-color: #e4e4e7 !important; /* zinc-200 */
+ border-color: #e4e4e7 !important;
+ color: #27272a !important; /* zinc-800 */
+}
+
+.shepherd-element.new-builder-tour .shepherd-button-secondary:active {
+ transform: scale(0.98);
+}
+
+/* ============================================
+ Shepherd.js - Cancel/close icon
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-cancel-icon {
+ color: #a1a1aa !important; /* zinc-400 */
+ transition: color 150ms ease !important;
+ width: 1.5rem !important;
+ height: 1.5rem !important;
+}
+
+.shepherd-element.new-builder-tour .shepherd-cancel-icon:hover {
+ color: #52525b !important; /* zinc-600 */
+}
+
+/* ============================================
+ Shepherd.js - Arrow styling
+ ============================================ */
+.shepherd-element.new-builder-tour .shepherd-arrow {
+ transform: scale(1.2) !important;
+}
+
+.shepherd-element.new-builder-tour .shepherd-arrow:before {
+ background: white !important;
+}
+
+/* ============================================
+ Shepherd.js - Placement spacing (12px = 0.75rem)
+ ============================================ */
+.shepherd-element.new-builder-tour[data-popper-placement^="top"] {
+ margin-bottom: 20px !important;
+}
+
+.shepherd-element.new-builder-tour[data-popper-placement^="bottom"] {
+ margin-top: 20px !important;
+}
+
+.shepherd-element.new-builder-tour[data-popper-placement^="left"] {
+ margin-right: 30px !important;
+}
+
+.shepherd-element.new-builder-tour[data-popper-placement^="right"] {
+ margin-left: 30px !important;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Block.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Block.tsx
index 435aa62c6161..10f4fc8a4425 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Block.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Block.tsx
@@ -65,9 +65,15 @@ export const Block: BlockComponent = ({
setTimeout(() => document.body.removeChild(dragPreview), 0);
};
+ // Generate a data-id from the block id (e.g., "AgentInputBlock" -> "block-card-AgentInputBlock")
+ const blockDataId = blockData.id
+ ? `block-card-${blockData.id.replace(/[^a-zA-Z0-9]/g, "")}`
+ : undefined;
+
return (
{
- const { blockMenuOpen, setBlockMenuOpen } = useControlPanelStore();
+ const { blockMenuOpen, setBlockMenuOpen, forceOpenBlockMenu } =
+ useControlPanelStore();
return (
- // pinBlocksPopover ? true : open
-
+ {
+ if (!forceOpenBlockMenu || open) {
+ setBlockMenuOpen(open);
+ }
+ }}
+ open={forceOpenBlockMenu ? true : blockMenuOpen}
+ >
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx
index f02fd935aa62..8bb761af63a5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/BlockMenuSearch/BlockMenuSearch.tsx
@@ -40,7 +40,10 @@ export const BlockMenuSearch = () => {
}
return (
-
+
Search results
= ({
return (
{
key={item.type}
name={item.name}
number={item.number}
+ menuItemType={item.type}
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
/>
@@ -111,6 +112,7 @@ export const BlockMenuSidebar = () => {
key={item.type}
name={item.name}
number={item.number}
+ menuItemType={item.type}
className="max-w-[11.5339rem]"
selected={defaultState === item.type}
onClick={() => setDefaultState(item.type as DefaultStateType)}
@@ -122,6 +124,7 @@ export const BlockMenuSidebar = () => {
key={item.type}
name={item.name}
number={item.number}
+ menuItemType={item.type}
selected={defaultState === item.type}
onClick={
item.onClick ||
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/MenuItem.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/MenuItem.tsx
index a1dbbb4c6a4a..b846c02cb7c1 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/MenuItem.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/MenuItem.tsx
@@ -8,6 +8,7 @@ interface Props extends ButtonHTMLAttributes
{
selected?: boolean;
number?: number;
name?: string;
+ menuItemType?: string;
}
export const MenuItem: React.FC = ({
@@ -15,10 +16,12 @@ export const MenuItem: React.FC = ({
number,
name,
className,
+ menuItemType,
...rest
}) => {
return (
{
const { form, isSaving, graphVersion, handleSave } = useNewSaveControl();
- const { saveControlOpen, setSaveControlOpen } = useControlPanelStore();
+ const { saveControlOpen, setSaveControlOpen, forceOpenSave } =
+ useControlPanelStore();
return (
-
+ {
+ if (!forceOpenSave || open) {
+ setSaveControlOpen(open);
+ }
+ }}
+ open={forceOpenSave ? true : saveControlOpen}
+ >
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/UndoRedoButtons.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/UndoRedoButtons.tsx
index 6f134056c8ce..bc100830e4ac 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/UndoRedoButtons.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/UndoRedoButtons.tsx
@@ -42,7 +42,12 @@ export const UndoRedoButtons = () => {
<>
-
+
@@ -51,7 +56,12 @@ export const UndoRedoButtons = () => {
-
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/controlPanelStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/controlPanelStore.ts
index 6847d769bdc4..5dcb11c12101 100644
--- a/autogpt_platform/frontend/src/app/(platform)/build/stores/controlPanelStore.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/controlPanelStore.ts
@@ -3,20 +3,32 @@ import { create } from "zustand";
type ControlPanelStore = {
blockMenuOpen: boolean;
saveControlOpen: boolean;
+ forceOpenBlockMenu: boolean;
+ forceOpenSave: boolean;
+
setBlockMenuOpen: (open: boolean) => void;
setSaveControlOpen: (open: boolean) => void;
+ setForceOpenBlockMenu: (force: boolean) => void;
+ setForceOpenSave: (force: boolean) => void;
+
reset: () => void;
};
export const useControlPanelStore = create((set) => ({
blockMenuOpen: false,
saveControlOpen: false,
+ forceOpenBlockMenu: false,
+ forceOpenSave: false,
+ setForceOpenBlockMenu: (force) => set({ forceOpenBlockMenu: force }),
+ setForceOpenSave: (force) => set({ forceOpenSave: force }),
setBlockMenuOpen: (open) => set({ blockMenuOpen: open }),
setSaveControlOpen: (open) => set({ saveControlOpen: open }),
reset: () =>
set({
blockMenuOpen: false,
saveControlOpen: false,
+ forceOpenBlockMenu: false,
+ forceOpenSave: false,
}),
}));
diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/tutorialStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/tutorialStore.ts
new file mode 100644
index 000000000000..3cf4ff39e184
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/tutorialStore.ts
@@ -0,0 +1,17 @@
+import { create } from "zustand";
+
+type TutorialStore = {
+ isTutorialRunning: boolean;
+ setIsTutorialRunning: (isTutorialRunning: boolean) => void;
+
+ currentStep: number;
+ setCurrentStep: (currentStep: number) => void;
+};
+
+export const useTutorialStore = create((set) => ({
+ isTutorialRunning: false,
+ setIsTutorialRunning: (isTutorialRunning) => set({ isTutorialRunning }),
+
+ currentStep: 0,
+ setCurrentStep: (currentStep) => set({ currentStep }),
+}));
diff --git a/autogpt_platform/frontend/src/components/molecules/TallyPoup/TallyPopup.tsx b/autogpt_platform/frontend/src/components/molecules/TallyPoup/TallyPopup.tsx
index d8b7a6027d17..be00982084c7 100644
--- a/autogpt_platform/frontend/src/components/molecules/TallyPoup/TallyPopup.tsx
+++ b/autogpt_platform/frontend/src/components/molecules/TallyPoup/TallyPopup.tsx
@@ -3,9 +3,14 @@
import React from "react";
import { useTallyPopup } from "./useTallyPopup";
import { Button } from "@/components/atoms/Button/Button";
+import { usePathname, useSearchParams } from "next/navigation";
export function TallyPopupSimple() {
const { state, handlers } = useTallyPopup();
+ const searchParams = useSearchParams();
+ const pathname = usePathname();
+ const isNewBuilder =
+ pathname.includes("build") && searchParams.get("view") === "new";
if (state.isFormVisible) {
return null;
@@ -13,7 +18,7 @@ export function TallyPopupSimple() {
return (
- {state.showTutorial && (
+ {state.showTutorial && !isNewBuilder && (
= ({
shouldShowHandle = false;
}
+ console.log("fieldId", `label-${nodeId}-${fieldId}`);
+
return (
= ({
fromAnyOf && "mb-0",
size === "small" ? "w-[350px]" : "w-full",
)}
+ data-id={`field-${nodeId}-${fieldId}`}
>
{!isAnyOf && !fromAnyOf && label && (
-
+
{shouldShowHandle && (