diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 419fdfeef..6a91177c6 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -145,6 +145,10 @@ body { transition-duration: 150ms; } +.react-flow__edge.running .react-flow__edge-path { + stroke: var(--status-blue) !important; +} + .ag-react-container { width: 100%; height: 100%; diff --git a/src/frontend/src/CustomEdges/index.tsx b/src/frontend/src/CustomEdges/index.tsx new file mode 100644 index 000000000..8ac7e31bd --- /dev/null +++ b/src/frontend/src/CustomEdges/index.tsx @@ -0,0 +1,33 @@ +import useFlowStore from "@/stores/flowStore"; +import { BaseEdge, EdgeProps, getBezierPath, Position } from "reactflow"; + +export function DefaultEdge({ + sourceHandleId, + source, + sourceX, + sourceY, + target, + targetHandleId, + targetX, + targetY, + ...props +}: EdgeProps) { + const getNode = useFlowStore((state) => state.getNode); + + const sourceNode = getNode(source); + const targetNode = getNode(target); + + const sourceXNew = (sourceNode?.position.x ?? 0) + (sourceNode?.width ?? 0); + const targetXNew = targetNode?.position.x ?? 0; + + const [edgePath] = getBezierPath({ + sourceX: sourceXNew, + sourceY, + sourcePosition: Position.Right, + targetPosition: Position.Left, + targetX: targetXNew, + targetY, + }); + + return ; +} diff --git a/src/frontend/src/CustomNodes/GenericNode/components/HandleTooltipComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/HandleTooltipComponent/index.tsx index 58e28ff3a..0d6210fbb 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/HandleTooltipComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/HandleTooltipComponent/index.tsx @@ -1,30 +1,65 @@ -import { TOOLTIP_EMPTY } from "../../../../constants/constants"; -import useFlowStore from "../../../../stores/flowStore"; -import { useTypesStore } from "../../../../stores/typesStore"; -import { NodeType } from "../../../../types/flow"; -import { groupByFamily } from "../../../../utils/utils"; -import TooltipRenderComponent from "../tooltipRenderComponent"; +import { convertTestName } from "@/components/storeCardComponent/utils/convert-test-name"; -export default function HandleTooltips({ - left, +export default function HandleTooltipComponent({ + isInput, tooltipTitle, + colors, + isConnecting, + isCompatible, + isSameNode, }: { - left: boolean; - nodes: NodeType[]; + isInput: boolean; + colors: string[]; tooltipTitle: string; + isConnecting: boolean; + isCompatible: boolean; + isSameNode: boolean; }) { - const myData = useTypesStore((state) => state.data); - const nodes = useFlowStore((state) => state.nodes); - - let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, nodes!); - - if (groupedObj && groupedObj.length > 0) { - //@ts-ignore - return groupedObj.map((item, index) => { - return ; - }); - } else { - //@ts-ignore - return {TOOLTIP_EMPTY}; - } + const tooltips = tooltipTitle.split("\n"); + const plural = tooltips.length > 1 ? "s" : ""; + return ( +
+ {isSameNode ? ( + "Can't connect to the same node" + ) : ( +
+ {isConnecting ? ( + isCompatible ? ( + + Connect{" "} + to + + ) : ( + Incompatible with + ) + ) : ( + + {isInput ? `Input${plural}` : `Output${plural}`}:{" "} + + )} + {tooltips.map((word, index) => ( +
+ {word} +
+ ))} + {isConnecting && {isInput ? `input` : `output`}} +
+ )} + {!isConnecting && ( +
+
+ Drag to connect compatible {!isInput ? "inputs" : "outputs"} +
+
+ Select to filter compatible {!isInput ? "inputs" : "outputs"}{" "} + and components +
+
+ )} +
+ ); } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index e44ea2147..bd7b104ea 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -81,6 +81,7 @@ export default function NodeInputField({ setFilterEdge={setFilterEdge} showNode={showNode} testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`} + nodeId={data.id} /> ); diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx index 96a7dabb1..e7d52d07c 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -110,6 +110,7 @@ export default function NodeOutputField({ id={id} title={title} edges={edges} + nodeId={data.id} myData={myData} colors={colors} setFilterEdge={setFilterEdge} diff --git a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx index fbf2232de..0e393b357 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx @@ -1,3 +1,6 @@ +import { useDarkStore } from "@/stores/darkStore"; +import useFlowStore from "@/stores/flowStore"; +import { useMemo, useState } from "react"; import { Handle, Position } from "reactflow"; import ShadTooltip from "../../../../components/shadTooltipComponent"; import { @@ -5,7 +8,7 @@ import { scapedJSONStringfy, } from "../../../../utils/reactflowUtils"; import { classNames, cn, groupByFamily } from "../../../../utils/utils"; -import HandleTooltips from "../HandleTooltipComponent"; +import HandleTooltipComponent from "../HandleTooltipComponent"; export default function HandleRenderComponent({ left, @@ -20,6 +23,7 @@ export default function HandleRenderComponent({ setFilterEdge, showNode, testIdComplement, + nodeId, }: { left: boolean; nodes: any; @@ -33,17 +37,168 @@ export default function HandleRenderComponent({ setFilterEdge: any; showNode: any; testIdComplement?: string; + nodeId: string; }) { + const setHandleDragging = useFlowStore((state) => state.setHandleDragging); + const setFilterType = useFlowStore((state) => state.setFilterType); + const handleDragging = useFlowStore((state) => state.handleDragging); + const filterType = useFlowStore((state) => state.filterType); + const dark = useDarkStore((state) => state.dark); + + const onConnect = useFlowStore((state) => state.onConnect); + + const handleMouseUp = () => { + setHandleDragging(undefined); + document.removeEventListener("mouseup", handleMouseUp); + }; + + const myId = useMemo( + () => scapedJSONStringfy(proxy ? { ...id, proxy } : id), + [id, proxy], + ); + + const getConnection = useMemo( + () => + (semiConnection: { + source: string | undefined; + sourceHandle: string | undefined; + target: string | undefined; + targetHandle: string | undefined; + }) => ({ + source: semiConnection.source ?? nodeId, + sourceHandle: semiConnection.sourceHandle ?? myId, + target: semiConnection.target ?? nodeId, + targetHandle: semiConnection.targetHandle ?? myId, + }), + [nodeId, myId], + ); + + const sameDraggingNode = useMemo( + () => (!left ? handleDragging?.target : handleDragging?.source) === nodeId, + [left, handleDragging, nodeId], + ); + + const ownDraggingHandle = useMemo( + () => + handleDragging && + (left ? handleDragging?.target : handleDragging?.source) && + (left ? handleDragging.targetHandle : handleDragging.sourceHandle) === + myId, + [handleDragging, left, myId], + ); + + const sameFilterNode = useMemo( + () => (!left ? filterType?.target : filterType?.source) === nodeId, + [left, filterType, nodeId], + ); + + const ownFilterHandle = useMemo( + () => + filterType && + (left ? filterType?.target : filterType?.source) === nodeId && + (left ? filterType.targetHandle : filterType.sourceHandle) === myId, + [filterType, left, myId], + ); + + const sameNode = useMemo( + () => sameDraggingNode || sameFilterNode, + [sameDraggingNode, sameFilterNode], + ); + const ownHandle = useMemo( + () => ownDraggingHandle || ownFilterHandle, + [ownDraggingHandle, ownFilterHandle], + ); + + const draggingOpenHandle = useMemo( + () => + handleDragging && + (left ? handleDragging.source : handleDragging.target) && + !ownDraggingHandle + ? isValidConnection(getConnection(handleDragging), nodes, edges) + : false, + [handleDragging, left, ownDraggingHandle, getConnection, nodes, edges], + ); + + const filterOpenHandle = useMemo( + () => + filterType && + (left ? filterType.source : filterType.target) && + !ownFilterHandle + ? isValidConnection(getConnection(filterType), nodes, edges) + : false, + [filterType, left, ownFilterHandle, getConnection, nodes, edges], + ); + + const openHandle = useMemo( + () => filterOpenHandle || draggingOpenHandle, + [filterOpenHandle, draggingOpenHandle], + ); + const filterPresent = useMemo( + () => handleDragging || filterType, + [handleDragging, filterType], + ); + + const currentFilter = useMemo( + () => + left + ? { + targetHandle: myId, + target: nodeId, + source: undefined, + sourceHandle: undefined, + type: tooltipTitle, + color: colors[0], + } + : { + sourceHandle: myId, + source: nodeId, + target: undefined, + targetHandle: undefined, + type: tooltipTitle, + color: colors[0], + }, + [left, myId, nodeId, tooltipTitle, colors], + ); + + const handleColor = useMemo( + () => + filterPresent && !(openHandle || ownHandle) + ? dark + ? "conic-gradient(#374151 0deg 360deg)" + : "conic-gradient(#cbd5e1 0deg 360deg)" + : "conic-gradient(" + + colors + .concat(colors[0]) + .map( + (color, index) => + color + + " " + + ((360 / colors.length) * index - 360 / (colors.length * 4)) + + "deg " + + ((360 / colors.length) * index + 360 / (colors.length * 4)) + + "deg", + ) + .join(" ,") + + ")", + [filterPresent, openHandle, ownHandle, dark, colors], + ); + + const [openTooltip, setOpenTooltip] = useState(false); return (
} side={left ? "left" : "right"} @@ -54,47 +209,73 @@ export default function HandleRenderComponent({ }`} type={left ? "target" : "source"} position={left ? Position.Left : Position.Right} - key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)} - id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)} + key={myId} + id={myId} isValidConnection={(connection) => isValidConnection(connection, nodes, edges) } className={classNames( - left ? "-ml-0.5" : "-mr-0.5", - "z-20 h-3 w-3 rounded-full border-none bg-background", + `group/handle z-20 h-6 w-6 rounded-full border-none bg-transparent transition-all`, )} - style={{ - background: - "conic-gradient(" + - colors - .concat(colors[0]) - .map( - (color, index) => - color + - " " + - ((360 / colors.length) * index - - 360 / (colors.length * 4)) + - "deg " + - ((360 / colors.length) * index + - 360 / (colors.length * 4)) + - "deg", - ) - .join(" ,") + - ")", - WebkitMaskImage: "radial-gradient(transparent 40%, black 44%)", - maskImage: "radial-gradient(transparent 40%, black 44%)", - }} onClick={() => { setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!)); + setFilterType(currentFilter); + if (filterOpenHandle && filterType) { + onConnect(getConnection(filterType)); + setFilterType(undefined); + setFilterEdge([]); + } }} - /> + onMouseUp={() => { + setOpenTooltip(false); + }} + onContextMenu={(event) => { + event.preventDefault(); + }} + onMouseDown={(event) => { + if (event.button === 0) { + setHandleDragging(currentFilter); + document.addEventListener("mouseup", handleMouseUp); + } + }} + > +
+
+
+ -
); } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/tooltipRenderComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/tooltipRenderComponent/index.tsx deleted file mode 100644 index ed2760161..000000000 --- a/src/frontend/src/CustomNodes/GenericNode/components/tooltipRenderComponent/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react"; -import { - INPUT_HANDLER_HOVER, - OUTPUT_HANDLER_HOVER, -} from "../../../../constants/constants"; -import { - nodeColors, - nodeIconsLucide, - nodeNames, -} from "../../../../utils/styleUtils"; -import { classNames } from "../../../../utils/utils"; - -const TooltipRenderComponent = ({ item, index, left }) => { - const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"]; - - return ( -
- {index === 0 && ( - {left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER} - )} - 0 ? "mt-2 flex items-center" : "mt-3 flex items-center", - )} - > -
- -
- - {nodeNames[item.family] ?? "Other"}{" "} - {item?.display_name && item?.display_name?.length > 0 ? ( - - {" "} - {item.display_name === "" ? "" : " - "} - {item.display_name.split(", ").length > 2 - ? item.display_name.split(", ").map((el, index) => ( - - - {index === item.display_name.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) - : item.display_name} - - ) : ( - - {" "} - {item.type === "" ? "" : " - "} - {item.type.split(", ").length > 2 - ? item.type.split(", ").map((el, index) => ( - - - {index === item.type.split(", ").length - 1 - ? el - : (el += `, `)} - - - )) - : item.type} - - )} - -
-
- ); -}; - -export default TooltipRenderComponent; diff --git a/src/frontend/src/components/shadTooltipComponent/index.tsx b/src/frontend/src/components/shadTooltipComponent/index.tsx index 1b76ab325..387f24e30 100644 --- a/src/frontend/src/components/shadTooltipComponent/index.tsx +++ b/src/frontend/src/components/shadTooltipComponent/index.tsx @@ -9,12 +9,19 @@ export default function ShadTooltip({ children, styleClasses, delayDuration = 500, + open, + setOpen, }: ShadToolTipType): JSX.Element { return content ? ( - + {children}
- {title} + {title} {beta && (
BETA diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 49a4a7a27..32b1fe7ef 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -17,6 +17,7 @@ import { nodeIconsLucide } from "../../../../utils/styleUtils"; import ParentDisclosureComponent from "../ParentDisclosureComponent"; import { SidebarCategoryComponent } from "./SidebarCategoryComponent"; +import { SidebarFilterComponent } from "./sidebarFilterComponent"; import { sortKeys } from "./utils"; export default function ExtraSidebar(): JSX.Element { @@ -25,6 +26,7 @@ export default function ExtraSidebar(): JSX.Element { const getFilterEdge = useFlowStore((state) => state.getFilterEdge); const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const hasStore = useStoreStore((state) => state.hasStore); + const filterType = useFlowStore((state) => state.filterType); const setErrorData = useAlertStore((state) => state.setErrorData); const [dataFilter, setFilterData] = useState(data); @@ -222,8 +224,18 @@ export default function ExtraSidebar(): JSX.Element {
-
- Components +
+ Components + {filterType && ( + { + setFilterEdge([]); + setFilterData(data); + }} + /> + )}
diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sidebarFilterComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sidebarFilterComponent/index.tsx new file mode 100644 index 000000000..7b1dcbb69 --- /dev/null +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sidebarFilterComponent/index.tsx @@ -0,0 +1,40 @@ +import ForwardedIconComponent from "@/components/genericIconComponent"; +import ShadTooltip from "@/components/shadTooltipComponent"; +import { Button } from "@/components/ui/button"; + +export function SidebarFilterComponent({ + isInput, + type, + resetFilters, +}: { + isInput: boolean; + type: string; + resetFilters: () => void; +}) { + return ( +
+
+ +
+ {isInput ? "Input" : "Output"}: {type} +
+
+ + + +
+ ); +} diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 6fa47d093..cdc910a8e 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -431,6 +431,9 @@ const useFlowStore = create((set, get) => ({ }); }, setFilterEdge: (newState) => { + if (newState.length === 0) { + set({ filterType: undefined }); + } set({ getFilterEdge: newState }); }, getFilterEdge: [], @@ -469,8 +472,6 @@ const useFlowStore = create((set, get) => ({ targetHandle: scapeJSONParse(connection.targetHandle!), sourceHandle: scapeJSONParse(connection.sourceHandle!), }, - // style: { stroke: "#555" }, - // className: "stroke-foreground stroke-connection", }, oldEdges, ); @@ -528,6 +529,7 @@ const useFlowStore = create((set, get) => ({ get().updateBuildStatus(ids, BuildStatus.ERROR); throw new Error("Invalid components"); } + get().updateEdgesRunningByNodes(nodes, true); } function handleBuildUpdate( vertexBuildData: VertexBuildTypeAPI, @@ -631,6 +633,10 @@ const useFlowStore = create((set, get) => ({ }); } } + get().updateEdgesRunningByNodes( + get().nodes.map((n) => n.id), + false, + ); get().setIsBuilding(false); get().setLockChat(false); }, @@ -653,6 +659,10 @@ const useFlowStore = create((set, get) => ({ title: "There are outdated components in the flow. The error could be related to them.", }); + get().updateEdgesRunningByNodes( + get().nodes.map((n) => n.id), + false, + ); setErrorData({ list, title }); get().setIsBuilding(false); get().setLockChat(false); @@ -662,7 +672,7 @@ const useFlowStore = create((set, get) => ({ // reference is the id of the vertex or the id of the parent in a group node .map((element) => element.reference) .filter(Boolean) as string[]; - useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); + get().updateBuildStatus(idList, BuildStatus.BUILDING); }, onValidateNodes: validateSubgraph, nodes: get().nodes || undefined, @@ -680,6 +690,17 @@ const useFlowStore = create((set, get) => ({ viewport: get().reactFlowInstance?.getViewport()!, }; }, + updateEdgesRunningByNodes: (ids: string[], running: boolean) => { + const edges = get().edges; + const newEdges = edges.map((edge) => { + if (ids.includes(edge.source) && ids.includes(edge.target)) { + edge.animated = running; + edge.className = running ? "running" : ""; + } + return edge; + }); + set({ edges: newEdges }); + }, updateVerticesBuild: ( vertices: { verticesIds: string[]; @@ -762,6 +783,15 @@ const useFlowStore = create((set, get) => ({ setBuildController: (controller) => { set({ buildController: controller }); }, + handleDragging: undefined, + setHandleDragging: (handleDragging) => { + set({ handleDragging }); + }, + + filterType: undefined, + setFilterType: (filterType) => { + set({ filterType }); + }, })); export default useFlowStore; diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index ab6643500..3977bee7a 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -231,7 +231,7 @@ @apply fill-chat-trigger-disabled stroke-chat-trigger-disabled stroke-1; } .parent-disclosure-arrangement { - @apply flex w-full select-none items-center justify-between bg-background px-3 py-1; + @apply flex w-full select-none items-center justify-between bg-background px-5 py-3; } .components-disclosure-arrangement { @apply -mt-px flex w-full select-none items-center justify-between border-y border-y-input bg-muted px-3 py-2; @@ -240,9 +240,6 @@ /* different color than the non child */ @apply -mt-px flex w-full select-none items-center justify-between border-y border-y-input bg-muted px-3 py-2; } - .parent-disclosure-title { - @apply p-2 px-2 text-sm font-medium; - } .components-disclosure-title { @apply flex items-center text-sm text-primary; } diff --git a/src/frontend/src/style/classes.css b/src/frontend/src/style/classes.css index e12bee424..224e2a05d 100644 --- a/src/frontend/src/style/classes.css +++ b/src/frontend/src/style/classes.css @@ -169,6 +169,26 @@ textarea[class^="ag-"]:focus { cursor: grabbing !important; } +.react-flow__handle-right { + right: 0 !important; + transform: translate(50%, -50%) !important; +} + +.react-flow__handle-left { + left: 0 !important; + transform: translate(-50%, -50%) !important; +} + +.react-flow__handle-right { + right: 0 !important; + transform: translate(50%, -50%) !important; +} + +.react-flow__handle-left { + left: 0 !important; + transform: translate(-50%, -50%) !important; +} + .react-flow__node-noteNode:not(.selected) { z-index: -1 !important; } diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index d75a76a9e..287afead6 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -57,6 +57,8 @@ --hover: #f2f4f5; --disabled-run: #6366f1; + --filter-foreground: #4f46e5; + --filter-background: #eef2ff; /* Colors that are shared in dark and light mode */ --blur-shared: #151923de; --build-trigger: #dc735b; @@ -114,6 +116,9 @@ --destructive: 0 60% 25%; /* hsl(0 60% 25%) */ --destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */ + --filter-foreground: #eef2ff; + --filter-background: #4e46e599; + --ring: 216 24% 30%; /* hsl(216 24% 30%) */ --radius: 0.5rem; diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 5374d9b2e..ee0f13e3e 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -339,6 +339,8 @@ export type ShadTooltipProps = { style?: string; }; export type ShadToolTipType = { + open?: boolean; + setOpen?: (open: boolean) => void; content?: ReactNode | null; side?: "top" | "right" | "bottom" | "left"; asChild?: boolean; diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index ac4705d8e..3515b7fc1 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -180,6 +180,52 @@ export type FlowStoreType = { edges?: Edge[]; viewport?: Viewport; }) => void; + handleDragging: + | { + source: string | undefined; + sourceHandle: string | undefined; + target: string | undefined; + targetHandle: string | undefined; + type: string; + color: string; + } + | undefined; + setHandleDragging: ( + data: + | { + source: string | undefined; + sourceHandle: string | undefined; + target: string | undefined; + targetHandle: string | undefined; + type: string; + color: string; + } + | undefined, + ) => void; + + filterType: + | { + source: string | undefined; + sourceHandle: string | undefined; + target: string | undefined; + targetHandle: string | undefined; + type: string; + color: string; + } + | undefined; + setFilterType: ( + data: + | { + source: string | undefined; + sourceHandle: string | undefined; + target: string | undefined; + targetHandle: string | undefined; + type: string; + color: string; + } + | undefined, + ) => void; + updateEdgesRunningByNodes: (ids: string[], running: boolean) => void; stopBuilding: () => void; buildController: AbortController; setBuildController: (controller: AbortController) => void; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index dff2e8436..737e948db 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -215,6 +215,9 @@ export function isValidConnection( nodes: Node[], edges: Edge[], ) { + if (source === target) { + return false; + } const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!); const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!); if ( diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 9fe290ed1..fad9dd6df 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -8,6 +8,7 @@ import { AlertTriangle, ArrowBigUp, ArrowLeft, + ArrowRight, ArrowUpToLine, Bell, Binary, @@ -63,6 +64,7 @@ import { FileText, FileType2, FileUp, + Filter, FlaskConical, FolderIcon, FolderPlus, @@ -87,6 +89,7 @@ import { Layers, Link, Link2, + ListFilter, Loader2, Lock, LogIn, @@ -401,10 +404,12 @@ export const nodeIconsLucide: iconsType = { GoogleSearchRun: GoogleIcon, Google: GoogleIcon, GoogleGenerativeAI: GoogleGenerativeAIIcon, + ArrowRight, Groq: GroqIcon, HCD: HCDIcon, HNLoader: HackerNewsIcon, Unstructured: UnstructuredIcon, + Filter: Filter, HuggingFaceHub: HuggingFaceIcon, HuggingFace: HuggingFaceIcon, HuggingFaceEmbeddings: HuggingFaceIcon, @@ -414,6 +419,7 @@ export const nodeIconsLucide: iconsType = { Meta: MetaIcon, CheckCheck, Midjorney: MidjourneyIcon, + ListFilter, MongoDBAtlasVectorSearch: MongoDBIcon, MongoDB: MongoDBIcon, MongoDBChatMessageHistory: MongoDBIcon, diff --git a/src/frontend/tailwind.config.mjs b/src/frontend/tailwind.config.mjs index a3502d458..201f55b20 100644 --- a/src/frontend/tailwind.config.mjs +++ b/src/frontend/tailwind.config.mjs @@ -97,9 +97,15 @@ const config = { "status-gray": "var(--status-gray)", "success-background": "var(--success-background)", "success-foreground": "var(--success-foreground)", - "beta-background": "var(--beta-background)", - "beta-foreground": "var(--beta-foreground)", - "beta-foreground-soft": "var(--beta-foreground-soft)", + filter: { + foreground: "var(--filter-foreground)", + background: "var(--filter-background)", + }, + beta: { + background: "var(--beta-background)", + foreground: "var(--beta-foreground)", + "foreground-soft": "var(--beta-foreground-soft)", + }, "chat-bot-icon": "var(--chat-bot-icon)", "chat-user-icon": "var(--chat-user-icon)", ice: "var(--ice)", diff --git a/src/frontend/tests/core/features/filterEdge-shard-0.spec.ts b/src/frontend/tests/core/features/filterEdge-shard-0.spec.ts index 790f01ed3..233dac0ee 100644 --- a/src/frontend/tests/core/features/filterEdge-shard-0.spec.ts +++ b/src/frontend/tests/core/features/filterEdge-shard-0.spec.ts @@ -54,28 +54,21 @@ test("user must see on handle hover a tooltip with possibility connections", asy } await visibleElementHandle.hover().then(async () => { - const testIds = [ - "available-output-inputs", - "available-output-chains", - "available-output-textsplitters", - "available-output-retrievers", - "available-output-prototypes", - "available-output-embeddings", - "available-output-data", - "available-output-vectorstores", - "available-output-memories", - "available-output-models", - "available-output-outputs", - "available-output-agents", - "available-output-helpers", - ]; + await expect( + page.getByText("Drag to connect compatible inputs").first(), + ).toBeVisible(); - await Promise.all( - testIds.map((id) => expect(page.getByTestId(id).first()).toBeVisible()), - ); + await expect( + page + .getByText("Select to filter compatible inputs and components") + .first(), + ).toBeVisible(); - await page.getByTestId("icon-X").click(); - await page.waitForTimeout(500); + await expect(page.getByText("Output:").first()).toBeVisible(); + + await expect( + page.getByTestId("output-tooltip-message").first(), + ).toBeVisible(); }); await page.getByTitle("fit view").click(); @@ -96,13 +89,20 @@ test("user must see on handle hover a tooltip with possibility connections", asy await visibleElementHandle.hover().then(async () => { await expect( - page.getByTestId("available-input-models").first(), + page.getByText("Drag to connect compatible outputs").first(), ).toBeVisible(); - await page.waitForTimeout(1000); - await page.getByTestId("icon-Search").click(); + await expect( + page + .getByText("Select to filter compatible outputs and components") + .first(), + ).toBeVisible(); - await page.waitForTimeout(500); + await expect(page.getByText("Input:").first()).toBeVisible(); + + await expect( + page.getByTestId("input-tooltip-languagemodel").first(), + ).toBeVisible(); }); await page.getByTitle("fit view").click(); await page.getByTitle("zoom out").click(); @@ -121,16 +121,21 @@ test("user must see on handle hover a tooltip with possibility connections", asy } await visibleElementHandle.hover().then(async () => { - await page.waitForTimeout(2500); - await expect( - page.getByTestId("available-input-retrievers").first(), - ).toBeVisible(); - await expect( - page.getByTestId("available-input-vectorstores").first(), + page.getByText("Drag to connect compatible outputs").first(), ).toBeVisible(); - await page.waitForTimeout(500); + await expect( + page + .getByText("Select to filter compatible outputs and components") + .first(), + ).toBeVisible(); + + await expect(page.getByText("Input:").first()).toBeVisible(); + + await expect( + page.getByTestId("input-tooltip-retriever").first(), + ).toBeVisible(); }); await page.getByTitle("fit view").click(); @@ -151,7 +156,19 @@ test("user must see on handle hover a tooltip with possibility connections", asy await visibleElementHandle.hover().then(async () => { await expect( - page.getByTestId("available-input-helpers").first(), + page.getByText("Drag to connect compatible outputs").first(), + ).toBeVisible(); + + await expect( + page + .getByText("Select to filter compatible outputs and components") + .first(), + ).toBeVisible(); + + await expect(page.getByText("Input:").first()).toBeVisible(); + + await expect( + page.getByTestId("input-tooltip-basechatmemory").first(), ).toBeVisible(); }); }); diff --git a/src/frontend/tests/core/features/filterSidebar.spec.ts b/src/frontend/tests/core/features/filterSidebar.spec.ts index 8091c0b72..405a6b3fb 100644 --- a/src/frontend/tests/core/features/filterSidebar.spec.ts +++ b/src/frontend/tests/core/features/filterSidebar.spec.ts @@ -54,99 +54,86 @@ test("user must see on handle click the possibility connections - LLMChain", asy await page.getByTestId("handle-apirequest-shownode-urls-left").click(); - let disclosureTestIds = [ - "disclosure-inputs", - "disclosure-outputs", - "disclosure-prompts", - "disclosure-models", - "disclosure-helpers", - "disclosure-agents", - "disclosure-chains", - "disclosure-prototypes", - ]; + await page.waitForTimeout(500); - let specificTestIds = [ - "inputsChat Input", - "outputsChat Output", - "promptsPrompt", - "modelsAmazon Bedrock", - "helpersChat Memory", - "agentsCSVAgent", - "chainsConversationChain", - "prototypesConditional Router", - ]; + expect(await page.getByTestId("icon-ListFilter")).toBeVisible(); - await Promise.all( - disclosureTestIds.map((id) => expect(page.getByTestId(id)).toBeVisible()), - ); + await page + .getByTestId("icon-X") + .first() + .hover() + .then(async () => { + await page + .getByText("Remove filter", { + exact: false, + }) + .first() + .isVisible(); + }); - await Promise.all( - specificTestIds.map((id) => expect(page.getByTestId(id)).toBeVisible()), - ); + await expect(page.getByTestId("disclosure-inputs")).toBeVisible(); + await expect(page.getByTestId("disclosure-outputs")).toBeVisible(); + await expect(page.getByTestId("disclosure-prompts")).toBeVisible(); + await expect(page.getByTestId("disclosure-models")).toBeVisible(); + await expect(page.getByTestId("disclosure-helpers")).toBeVisible(); + await expect(page.getByTestId("disclosure-agents")).toBeVisible(); + await expect(page.getByTestId("disclosure-chains")).toBeVisible(); + await expect(page.getByTestId("disclosure-prototypes")).toBeVisible(); + + await expect(page.getByTestId("inputsChat Input")).toBeVisible(); + await expect(page.getByTestId("outputsChat Output")).toBeVisible(); + await expect(page.getByTestId("promptsPrompt")).toBeVisible(); + await expect(page.getByTestId("modelsAmazon Bedrock")).toBeVisible(); + await expect(page.getByTestId("helpersChat Memory")).toBeVisible(); + await expect(page.getByTestId("agentsCSVAgent")).toBeVisible(); + await expect(page.getByTestId("chainsConversationChain")).toBeVisible(); + await expect(page.getByTestId("prototypesConditional Router")).toBeVisible(); await page.getByPlaceholder("Search").click(); - let notVisibleTestIds = [ - "inputsChat Input", - "outputsChat Output", - "promptsPrompt", - "modelsAmazon Bedrock", - "helpersChat Memory", - "agentsTool Calling Agent", - "chainsConversationChain", - "prototypesConditional Router", - ]; - - await Promise.all( - notVisibleTestIds.map((id) => - expect(page.getByTestId(id)).not.toBeVisible(), - ), - ); + await expect(page.getByTestId("inputsChat Input")).not.toBeVisible(); + await expect(page.getByTestId("outputsChat Output")).not.toBeVisible(); + await expect(page.getByTestId("promptsPrompt")).not.toBeVisible(); + await expect(page.getByTestId("modelsAmazon Bedrock")).not.toBeVisible(); + await expect(page.getByTestId("helpersChat Memory")).not.toBeVisible(); + await expect(page.getByTestId("agentsTool Calling Agent")).not.toBeVisible(); + await expect(page.getByTestId("chainsConversationChain")).not.toBeVisible(); + await expect( + page.getByTestId("prototypesConditional Router"), + ).not.toBeVisible(); await page.getByTestId("handle-apirequest-shownode-headers-left").click(); - disclosureTestIds = [ - "disclosure-data", - "disclosure-helpers", - "disclosure-vector stores", - "disclosure-utilities", - "disclosure-prototypes", - "disclosure-retrievers", - "disclosure-tools", - ]; + await expect(page.getByTestId("disclosure-data")).toBeVisible(); + await expect(page.getByTestId("disclosure-helpers")).toBeVisible(); + await expect(page.getByTestId("disclosure-vector stores")).toBeVisible(); + await expect(page.getByTestId("disclosure-utilities")).toBeVisible(); + await expect(page.getByTestId("disclosure-prototypes")).toBeVisible(); + await expect(page.getByTestId("disclosure-retrievers")).toBeVisible(); + await expect(page.getByTestId("disclosure-embeddings")).toBeVisible(); + await expect(page.getByTestId("disclosure-tools")).toBeVisible(); - specificTestIds = [ - "dataAPI Request", - "helpersChat Memory", - "vectorstoresAstra DB", - "toolsSearch API", - "prototypesSub Flow", - "retrieversSelf Query Retriever", - ]; + await expect(page.getByTestId("dataAPI Request")).toBeVisible(); + await expect(page.getByTestId("helpersChat Memory")).toBeVisible(); + await expect(page.getByTestId("vectorstoresAstra DB")).toBeVisible(); + await expect(page.getByTestId("toolsSearch API")).toBeVisible(); + await expect(page.getByTestId("prototypesSub Flow")).toBeVisible(); + await expect( + page.getByTestId("retrieversSelf Query Retriever"), + ).toBeVisible(); + await expect(page.getByTestId("helpersSplit Text")).toBeVisible(); + await expect(page.getByTestId("toolsSearch API")).toBeVisible(); - await Promise.all( - disclosureTestIds.map((id) => expect(page.getByTestId(id)).toBeVisible()), - ); + await page.getByTestId("icon-X").first().click(); - await Promise.all( - specificTestIds.map((id) => expect(page.getByTestId(id)).toBeVisible()), - ); - - await page.getByPlaceholder("Search").click(); - - notVisibleTestIds = [ - "dataAPI Request", - "helpersChat Memory", - "vectorstoresAstra DB", - "toolsSearch API", - "prototypesSub Flow", - "retrieversSelf Query Retriever", - "textsplittersCharacterTextSplitter", - ]; - - await Promise.all( - notVisibleTestIds.map((id) => - expect(page.getByTestId(id)).not.toBeVisible(), - ), - ); + await expect(page.getByTestId("dataAPI Request")).not.toBeVisible(); + await expect(page.getByTestId("helpersChat Memory")).not.toBeVisible(); + await expect(page.getByTestId("vectorstoresAstra DB")).not.toBeVisible(); + await expect(page.getByTestId("toolsSearch API")).not.toBeVisible(); + await expect(page.getByTestId("prototypesSub Flow")).not.toBeVisible(); + await expect( + page.getByTestId("retrieversSelf Query Retriever"), + ).not.toBeVisible(); + await expect(page.getByTestId("helpersSplit Text")).not.toBeVisible(); + await expect(page.getByTestId("toolsSearch API")).not.toBeVisible(); }); diff --git a/src/frontend/tests/core/regression/generalBugs-shard-4.spec.ts b/src/frontend/tests/core/regression/generalBugs-shard-4.spec.ts index 3f2a4142b..bf6554ada 100644 --- a/src/frontend/tests/core/regression/generalBugs-shard-4.spec.ts +++ b/src/frontend/tests/core/regression/generalBugs-shard-4.spec.ts @@ -1,6 +1,4 @@ import { expect, test } from "@playwright/test"; -import * as dotenv from "dotenv"; -import path from "path"; test("should be able to move flow from folder, rename it and be displayed on correct folder", async ({ page, diff --git a/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts b/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts index 1cb1d2b54..4ac3987a8 100644 --- a/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts +++ b/src/frontend/tests/core/regression/generalBugs-shard-5.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -test("should be able to see output preview from grouped components", async ({ +test("should be able to see output preview from grouped components and connect components with a single click", async ({ page, }) => { await page.goto("/"); @@ -51,7 +51,7 @@ test("should be able to see output preview from grouped components", async ({ .hover() .then(async () => { await page.mouse.down(); - await page.mouse.move(-1000, 500); + await page.mouse.move(-600, 300); await page.waitForTimeout(400); }); @@ -71,7 +71,7 @@ test("should be able to see output preview from grouped components", async ({ .hover() .then(async () => { await page.mouse.down(); - await page.mouse.move(-1000, 800); + await page.mouse.move(-600, 300); await page.waitForTimeout(400); }); @@ -86,7 +86,7 @@ test("should be able to see output preview from grouped components", async ({ .hover() .then(async () => { await page.mouse.down(); - await page.mouse.move(-800, 800); + await page.mouse.move(-600, 300); await page.waitForTimeout(400); }); @@ -101,7 +101,7 @@ test("should be able to see output preview from grouped components", async ({ .hover() .then(async () => { await page.mouse.down(); - await page.mouse.move(-200, 800); + await page.mouse.move(-600, 300); await page.waitForTimeout(200); }); @@ -117,7 +117,7 @@ test("should be able to see output preview from grouped components", async ({ .hover() .then(async () => { await page.mouse.down(); - await page.mouse.move(-200, 500); + await page.mouse.move(-600, 300); }); await page.mouse.up(); @@ -134,13 +134,118 @@ test("should be able to see output preview from grouped components", async ({ const elementCombineTextOutput0 = await page .getByTestId("handle-combinetext-shownode-combined text-right") .nth(0); - await elementCombineTextOutput0.hover(); - await page.mouse.down(); + await elementCombineTextOutput0.click(); + + const blockedHandle = await page + .getByTestId("gradient-handle-textinput-shownode-text-right") + .nth(2); + const secondBlockedHandle = await page + .getByTestId("gradient-handle-combinetext-shownode-combined text-right") + .nth(2); + const thirdBlockedHandle = await page + .getByTestId("gradient-handle-textoutput-shownode-text-right") + .nth(0); + + const hasGradient = await blockedHandle?.evaluate((el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(203, 213, 225)") + ); + }); + + await page.waitForTimeout(500); + + const secondHasGradient = await secondBlockedHandle?.evaluate((el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(203, 213, 225)") + ); + }); + + await page.waitForTimeout(500); + + const thirdHasGradient = await thirdBlockedHandle?.evaluate((el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(203, 213, 225)") + ); + }); + + await page.waitForTimeout(500); + + expect(hasGradient).toBe(true); + expect(secondHasGradient).toBe(true); + expect(thirdHasGradient).toBe(true); + + const unlockedHandle = await page + .getByTestId("gradient-handle-textinput-shownode-text-left") + .last(); + const secondUnlockedHandle = await page + .getByTestId("gradient-handle-combinetext-shownode-second text-left") + .last(); + const thirdUnlockedHandle = await page + .getByTestId("gradient-handle-combinetext-shownode-second text-left") + .first(); + const fourthUnlockedHandle = await page + .getByTestId("gradient-handle-textoutput-shownode-text-left") + .first(); + + const hasGradientUnlocked = await unlockedHandle?.evaluate((el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(79, 70, 229)") + ); + }); + + await page.waitForTimeout(500); + + const secondHasGradientUnlocked = await secondUnlockedHandle?.evaluate( + (el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(79, 70, 229)") + ); + }, + ); + + await page.waitForTimeout(500); + + const thirdHasGradientLocked = await thirdUnlockedHandle?.evaluate((el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(203, 213, 225)") + ); + }); + + await page.waitForTimeout(500); + + const fourthHasGradientUnlocked = await fourthUnlockedHandle?.evaluate( + (el) => { + const style = window.getComputedStyle(el); + return ( + style.backgroundImage.includes("conic-gradient") && + style.backgroundImage.includes("rgb(79, 70, 229)") + ); + }, + ); + + await page.waitForTimeout(500); + + expect(hasGradientUnlocked).toBe(true); + expect(secondHasGradientUnlocked).toBe(true); + expect(thirdHasGradientLocked).toBe(true); + expect(fourthHasGradientUnlocked).toBe(true); + const elementCombineTextInput1 = await page .getByTestId("handle-combinetext-shownode-first text-left") .nth(1); - await elementCombineTextInput1.hover(); - await page.mouse.up(); + await elementCombineTextInput1.click(); await page .getByTestId("title-Combine Text") @@ -157,68 +262,60 @@ test("should be able to see output preview from grouped components", async ({ const elementTextOutput0 = await page .getByTestId("handle-textinput-shownode-text-right") .nth(0); - await elementTextOutput0.hover(); - await page.mouse.down(); + await elementTextOutput0.click(); const elementGroupInput0 = await page.getByTestId( "handle-groupnode-shownode-first text-left", ); - - await elementGroupInput0.hover(); - await page.mouse.up(); + await elementGroupInput0.click(); //connection 3 const elementTextOutput1 = await page .getByTestId("handle-textinput-shownode-text-right") .nth(2); - await elementTextOutput1.hover(); - await page.mouse.down(); + await elementTextOutput1.click(); const elementGroupInput1 = await page .getByTestId("handle-groupnode-shownode-second text-left") .nth(1); - - await elementGroupInput1.hover(); - await page.mouse.up(); + await elementGroupInput1.click(); //connection 4 const elementGroupOutput = await page .getByTestId("handle-groupnode-shownode-combined text-right") .nth(0); - await elementGroupOutput.hover(); - await page.mouse.down(); + await elementGroupOutput.click(); const elementTextOutputInput = await page .getByTestId("handle-textoutput-shownode-text-left") .nth(0); - await elementTextOutputInput.hover(); - await page.mouse.up(); + await elementTextOutputInput.click(); await page.getByTestId("textarea_str_input_value").nth(0).fill(randomName); - await page.waitForTimeout(1000); + await page.waitForTimeout(500); await page .getByTestId("textarea_str_input_value") .nth(1) .fill(secondRandomName); - await page.waitForTimeout(1000); + await page.waitForTimeout(500); await page .getByPlaceholder("Type something...", { exact: true }) .nth(6) .fill(thirdRandomName); - await page.waitForTimeout(1000); + await page.waitForTimeout(500); await page .getByPlaceholder("Type something...", { exact: true }) .nth(3) .fill("-"); - await page.waitForTimeout(1000); + await page.waitForTimeout(500); await page .getByPlaceholder("Type something...", { exact: true }) .nth(4) .fill("-"); - await page.waitForTimeout(3000); + await page.waitForTimeout(500); await page.getByTestId("button_run_text output").last().click(); @@ -227,13 +324,13 @@ test("should be able to see output preview from grouped components", async ({ await page.getByText("built successfully").last().click({ timeout: 15000, }); - await page.waitForTimeout(3000); + await page.waitForTimeout(500); expect( await page.getByTestId("output-inspection-combined text").first(), ).not.toBeDisabled(); await page.getByTestId("output-inspection-combined text").first().click(); - await page.waitForTimeout(1000); + await page.waitForTimeout(500); await page.getByText("Component Output").isVisible();