Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export const BuilderActions = memo(() => {
flowID: parseAsString,
});
return (
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4 rounded-full bg-white p-2 px-2 shadow-lg">
<div
data-id="builder-actions"
className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4 rounded-full bg-white p-2 px-2 shadow-lg"
>
<AgentOutputs flowID={flowID} />
<RunGraph flowID={flowID} />
<ScheduleGraph flowID={flowID} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ export const AgentOutputs = ({ flowID }: { flowID: string | null }) => {
<Tooltip>
<TooltipTrigger asChild>
<SheetTrigger asChild>
<BuilderActionButton disabled={!flowID || !hasOutputs()}>
<BuilderActionButton
data-id="agent-outputs-button"
disabled={!flowID || !hasOutputs()}
>
<BookOpenIcon className="size-6" />
</BuilderActionButton>
</SheetTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => {
<Tooltip>
<TooltipTrigger asChild>
<BuilderActionButton
data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"}
className={cn(
isGraphRunning &&
"border-red-500 bg-gradient-to-br from-red-400 to-red-500 shadow-[inset_0_2px_0_0_rgba(255,255,255,0.5),0_2px_4px_0_rgba(0,0,0,0.2)]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const ScheduleGraph = ({ flowID }: { flowID: string | null }) => {
<Tooltip>
<TooltipTrigger asChild>
<BuilderActionButton
data-id="schedule-graph-button"
onClick={handleScheduleGraph}
disabled={!flowID}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import {
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import {
ChalkboardIcon,
FrameCornersIcon,
MinusIcon,
PlusIcon,
} from "@phosphor-icons/react/dist/ssr";
import { LockIcon, LockOpenIcon } from "lucide-react";
import { memo } from "react";
import { useTutorialStore } from "@/app/(platform)/build/stores/tutorialStore";
import { startTutorial } from "../../tutorial";

export const CustomControls = memo(
({
Expand All @@ -22,27 +25,41 @@ export const CustomControls = memo(
setIsLocked: (isLocked: boolean) => void;
}) => {
const { zoomIn, zoomOut, fitView } = useReactFlow();

const { isTutorialRunning, setIsTutorialRunning } = useTutorialStore();
const controls = [
{
id: "zoom-in-button",
icon: <PlusIcon className="size-4" />,
label: "Zoom In",
onClick: () => zoomIn(),
className: "h-10 w-10 border-none",
},
{
id: "zoom-out-button",
icon: <MinusIcon className="size-4" />,
label: "Zoom Out",
onClick: () => zoomOut(),
className: "h-10 w-10 border-none",
},
{
id: "tutorial-button",
icon: <ChalkboardIcon className="size-4" />,
label: "Start Tutorial",
onClick: () => {
startTutorial();
setIsTutorialRunning(true);
},
className: `h-10 w-10 border-none ${isTutorialRunning ? "bg-zinc-100" : "bg-white"}`,
},
{
id: "fit-view-button",
icon: <FrameCornersIcon className="size-4" />,
label: "Fit View",
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
className: "h-10 w-10 border-none",
},
{
id: "lock-button",
icon: !isLocked ? (
<LockOpenIcon className="size-4" />
) : (
Expand All @@ -55,15 +72,19 @@ export const CustomControls = memo(
];

return (
<div className="absolute bottom-4 left-4 z-10 flex flex-col items-center gap-2 rounded-full bg-white px-1 py-2 shadow-lg">
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={300}>
<div
data-id="custom-controls"
className="absolute bottom-4 left-4 z-10 flex flex-col items-center gap-2 rounded-full bg-white px-1 py-2 shadow-lg"
>
{controls.map((control) => (
<Tooltip key={control.id} delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant="icon"
size={"small"}
onClick={control.onClick}
className={control.className}
data-id={control.id}
>
{control.icon}
<span className="sr-only">{control.label}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const NodeContainer = ({
selected && "shadow-lg ring-2 ring-slate-200",
status && nodeStyleBasedOnStatus[status],
)}
data-id={`custom-node-${nodeId}`}
>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export const FormCreator = React.memo(
const initialValues = getHardCodedValues(nodeId);

return (
<div className={className}>
<div
className={className}
data-id={`form-creator-container-${nodeId}-node`}
>
<FormRenderer
jsonSchema={jsonSchema}
handleChange={handleChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ export const OutputHandler = ({
const isConnected = isOutputConnected(nodeId, key);
const shouldShow = isConnected || isOutputVisible;
const { displayType, colorClass } = getTypeDisplayInfo(property);

return shouldShow ? (
<div key={key} className="relative flex items-center gap-2">
<div
data-id={`output-handler-${nodeId}-${property?.title || key}`}
key={key}
className="relative flex items-center gap-2"
>
{property?.description && (
<TooltipProvider>
<Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Block IDs for tutorial blocks
export const BLOCK_IDS = {
CALCULATOR: "b1ab9b19-67a6-406d-abf5-2dba76d00c79",
AGENT_INPUT: "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
AGENT_OUTPUT: "363ae599-353e-4804-937e-b2ee3cef3da4",
} as const;

export const TUTORIAL_SELECTORS = {
// Custom nodes - These are all before saving
INPUT_NODE: '[data-id="custom-node-2"]',
OUTPUT_NODE: '[data-id="custom-node-3 "]',
CALCULATOR_NODE: '[data-id="custom-node-1"]',

// Paricular field selector
NAME_FIELD_OUTPUT_NODE: '[data-id="field-3-root_name"]',

// Output Handlers
INPUT_BLOCK_RESULT_OUTPUT_HANDLEER: '[data-id="output-handler-2-Result"]',
CALCULATOR_RESULT_OUTPUT_HANDLEER: '[data-id="input-handler-1-Result"]',

// Input Handler
CALCULATOR_NUMBER_A_INPUT_HANDLER: '[data-id="label-1-root_a"]',
OUTPUT_VALUE_INPUT_HANDLEER: '[data-id="label-3-root_value"]',

// Block Menu
BLOCKS_TRIGGER: '[data-id="blocks-control-popover-trigger"]',
BLOCKS_CONTENT: '[data-id="blocks-control-popover-content"]',
BLOCKS_SEARCH_INPUT:
'[data-id="blocks-control-search-bar"] input[type="text"]',
BLOCKS_SEARCH_INPUT_BOX: '[data-id="blocks-control-search-bar"]',

// Add a new selector that checks within search results

// Block Menu Sidebar
MENU_ITEM_INPUT_BLOCKS: '[data-id="menu-item-input_blocks"]',
MENU_ITEM_ALL_BLOCKS: '[data-id="menu-item-all_blocks"]',
MENU_ITEM_ACTION_BLOCKS: '[data-id="menu-item-action_blocks"]',
MENU_ITEM_OUTPUT_BLOCKS: '[data-id="menu-item-output_blocks"]',
MENU_ITEM_INTEGRATIONS: '[data-id="menu-item-integrations"]',
MENU_ITEM_MY_AGENTS: '[data-id="menu-item-my_agents"]',
MENU_ITEM_MARKETPLACE: '[data-id="menu-item-marketplace_agents"]',
MENU_ITEM_SUGGESTION: '[data-id="menu-item-suggestion"]',

// Block Cards
BLOCK_CARD_PREFIX: '[data-id^="block-card-"]',
BLOCK_CARD_AGENT_INPUT: '[data-id="block-card-AgentInputBlock"]',
// Calculator block - legacy ID used in old tutorial
BLOCK_CARD_CALCULATOR:
'[data-id="block-card-b1ab9b1967a6406dabf52dba76d00c79"]',
BLOCK_CARD_CALCULATOR_IN_SEARCH:
'[data-id="blocks-control-search-results"] [data-id="block-card-b1ab9b1967a6406dabf52dba76d00c79"]',

// Save Control
SAVE_TRIGGER: '[data-id="save-control-popover-trigger"]',
SAVE_CONTENT: '[data-id="save-control-popover-content"]',
SAVE_AGENT_BUTTON: '[data-id="save-control-save-agent"]',
SAVE_NAME_INPUT: '[data-id="save-control-name-input"]',
SAVE_DESCRIPTION_INPUT: '[data-id="save-control-description-input"]',

// Builder Actions (Run, Schedule, Outputs)
BUILDER_ACTIONS: '[data-id="builder-actions"]',
RUN_BUTTON: '[data-id="run-graph-button"]',
STOP_BUTTON: '[data-id="stop-graph-button"]',
SCHEDULE_BUTTON: '[data-id="schedule-graph-button"]',
AGENT_OUTPUTS_BUTTON: '[data-id="agent-outputs-button"]',

// Custom Controls (bottom left)
CUSTOM_CONTROLS: '[data-id="custom-controls"]',
ZOOM_IN_BUTTON: '[data-id="zoom-in-button"]',
ZOOM_OUT_BUTTON: '[data-id="zoom-out-button"]',
FIT_VIEW_BUTTON: '[data-id="fit-view-button"]',
LOCK_BUTTON: '[data-id="lock-button"]',
TUTORIAL_BUTTON: '[data-id="tutorial-button"]',

// Canvas
REACT_FLOW_CANVAS: ".react-flow__pane",
REACT_FLOW_NODE: ".react-flow__node",
REACT_FLOW_NODE_FIRST: '[data-testid^="rf__node-"]:first-child',
REACT_FLOW_EDGE: '[data-testid^="rf__edge-"]',

// Node elements
NODE_CONTAINER: '[data-id^="custom-node-"]',
NODE_HEADER: '[data-id^="node-header-"]',
NODE_INPUT_HANDLES: '[data-id="input-handles"]',
NODE_OUTPUT_HANDLE: '[data-handlepos="right"]',
NODE_INPUT_HANDLE: "[data-nodeid]",
NODE_LATEST_OUTPUT: '[data-id="latest-output"]',
// These are the Id's of the nodes before saving
CALCULATOR_NODE_FORM_CONTAINER: '[data-id^="form-creator-container-1-node"]', // <-- Add this line
AGENT_INPUT_NODE_FORM_CONTAINER: '[data-id^="form-creator-container-2-node"]', // <-- Add this line
AGENT_OUTPUT_NODE_FORM_CONTAINER:
'[data-id^="form-creator-container-3-node"]', // <-- Add this line

// Execution badges
BADGE_QUEUED: '[data-id^="badge-"][data-id$="-QUEUED"]',
BADGE_COMPLETED: '[data-id^="badge-"][data-id$="-COMPLETED"]',

// Undo/Redo
UNDO_BUTTON: '[data-id="undo-button"]',
REDO_BUTTON: '[data-id="redo-button"]',
} as const;

export const CSS_CLASSES = {
DISABLE: "new-builder-tutorial-disable",
HIGHLIGHT: "new-builder-tutorial-highlight",
PULSE: "new-builder-tutorial-pulse",
} as const;

export const TUTORIAL_CONFIG = {
ELEMENT_CHECK_INTERVAL: 50, // ms
INPUT_CHECK_INTERVAL: 100, // ms
USE_MODAL_OVERLAY: true,
SCROLL_BEHAVIOR: "smooth" as const,
SCROLL_BLOCK: "center" as const,
SEARCH_TERM_CALCULATOR: "Calculator",
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Block-related helpers for the tutorial
*/

import { BLOCK_IDS } from "../constants";
import { useNodeStore } from "../../../../stores/nodeStore";
import { getV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/default/default";
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";

// Cache for prefetched blocks
let prefetchedBlocks: Map<string, BlockInfo> = new Map();

/**
* Prefetches Agent Input and Agent Output blocks at tutorial start
* Call this when the tutorial is initialized
*/
export const prefetchTutorialBlocks = async (): Promise<void> => {
try {
const blockIds = [BLOCK_IDS.AGENT_INPUT, BLOCK_IDS.AGENT_OUTPUT];
const response = await getV2GetSpecificBlocks({ block_ids: blockIds });

if (response.status === 200 && response.data) {
response.data.forEach((block) => {
prefetchedBlocks.set(block.id, block);
});
console.debug("Tutorial blocks prefetched:", prefetchedBlocks.size);
}
} catch (error) {
console.error("Failed to prefetch tutorial blocks:", error);
}
};

/**
* Gets a prefetched block by ID
*/
export const getPrefetchedBlock = (blockId: string): BlockInfo | undefined => {
return prefetchedBlocks.get(blockId);
};

/**
* Clears the prefetched blocks cache
*/
export const clearPrefetchedBlocks = (): void => {
prefetchedBlocks.clear();
};

/**
* Adds a prefetched block to the canvas at a specific position
*/
export const addPrefetchedBlock = (
blockId: string,
position?: { x: number; y: number },
): void => {
const block = prefetchedBlocks.get(blockId);
if (block) {
useNodeStore.getState().addBlock(block, {}, position);
} else {
console.error(`Block ${blockId} not found in prefetched blocks`);
}
};

/**
* Gets a node by its block_id
*/
export const getNodeByBlockId = (blockId: string) => {
const nodes = useNodeStore.getState().nodes;
return nodes.find((n) => n.data?.block_id === blockId);
};

/**
* Adds Agent Input and Agent Output blocks positioned relative to Calculator
* Agent Input: Left side of Calculator
* Agent Output: Right side of Calculator
*/
export const addAgentIOBlocks = (): void => {
// Find the Calculator node to position relative to it
const calculatorNode = getNodeByBlockId(BLOCK_IDS.CALCULATOR);

if (calculatorNode) {
const calcX = calculatorNode.position.x;
const calcY = calculatorNode.position.y;

// Agent Input: 600px to the left of Calculator
addPrefetchedBlock(BLOCK_IDS.AGENT_INPUT, {
x: calcX - 600,
y: calcY,
});

// Agent Output: 600px to the right of Calculator
addPrefetchedBlock(BLOCK_IDS.AGENT_OUTPUT, {
x: calcX + 600,
y: calcY,
});
} else {
// Fallback: Add without specific positioning if Calculator not found
addPrefetchedBlock(BLOCK_IDS.AGENT_INPUT);
addPrefetchedBlock(BLOCK_IDS.AGENT_OUTPUT);
}
};

/**
* Gets the form container selector for a specific block
*/
export const getFormContainerSelector = (blockId: string): string | null => {
const node = getNodeByBlockId(blockId);
if (node) {
return `[data-id="form-creator-container-${node.id}"]`;
}
return null;
};

/**
* Gets the form container element for a specific block
*/
export const getFormContainerElement = (blockId: string): Element | null => {
const selector = getFormContainerSelector(blockId);
if (selector) {
return document.querySelector(selector);
}
return null;
};

Loading
Loading