refactor: handle rendering and filtering functionality (#3512)

This commit is contained in:
Lucas Oliveira 2024-09-13 17:27:30 -03:00 committed by GitHub
commit 2f1f1808b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 755 additions and 316 deletions

View file

@ -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%;

View file

@ -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 <BaseEdge path={edgePath} {...props} />;
}

View file

@ -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 <TooltipRenderComponent index={index} item={item} left={left} />;
});
} else {
//@ts-ignore
return <span data-testid={`empty-tooltip-filter`}>{TOOLTIP_EMPTY}</span>;
}
const tooltips = tooltipTitle.split("\n");
const plural = tooltips.length > 1 ? "s" : "";
return (
<div className="py-1.5 font-medium text-muted-foreground">
{isSameNode ? (
"Can't connect to the same node"
) : (
<div className="flex items-start gap-1.5">
{isConnecting ? (
isCompatible ? (
<span>
<span className="font-semibold text-foreground">Connect</span>{" "}
to
</span>
) : (
<span>Incompatible with</span>
)
) : (
<span className="text-foreground">
{isInput ? `Input${plural}` : `Output${plural}`}:{" "}
</span>
)}
{tooltips.map((word, index) => (
<div
className="rounded-sm px-1.5 text-background"
style={{ backgroundColor: colors[index] }}
data-testid={`${isInput ? "input" : "output"}-tooltip-${convertTestName(word)}`}
>
{word}
</div>
))}
{isConnecting && <span>{isInput ? `input` : `output`}</span>}
</div>
)}
{!isConnecting && (
<div className="mt-2 flex flex-col gap-0.5 text-xs">
<div>
<b>Drag</b> to connect compatible {!isInput ? "inputs" : "outputs"}
</div>
<div>
<b>Select</b> to filter compatible {!isInput ? "inputs" : "outputs"}{" "}
and components
</div>
</div>
)}
</div>
);
}

View file

@ -81,6 +81,7 @@ export default function NodeInputField({
setFilterEdge={setFilterEdge}
showNode={showNode}
testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`}
nodeId={data.id}
/>
);

View file

@ -110,6 +110,7 @@ export default function NodeOutputField({
id={id}
title={title}
edges={edges}
nodeId={data.id}
myData={myData}
colors={colors}
setFilterEdge={setFilterEdge}

View file

@ -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 (
<div>
<ShadTooltip
open={openTooltip}
setOpen={setOpenTooltip}
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={1000}
content={
<HandleTooltips
left={left}
nodes={nodes}
tooltipTitle={tooltipTitle!}
<HandleTooltipComponent
isInput={left}
colors={colors}
tooltipTitle={tooltipTitle}
isConnecting={!!filterPresent && !ownHandle}
isCompatible={openHandle}
isSameNode={sameNode && !ownHandle}
/>
}
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);
}
}}
>
<div
className={cn(
"pointer-events-none absolute left-1/2 top-[50%] z-30 flex h-0 w-0 -translate-x-1/2 translate-y-[-50%] items-center justify-center rounded-full bg-background transition-all group-hover/handle:bg-transparent",
filterPresent
? openHandle || ownHandle
? cn(
"h-4 w-4",
ownHandle ? "bg-transparent" : "bg-background",
)
: ""
: "group-hover/node:h-4 group-hover/node:w-4",
)}
></div>
<div
className="pointer-events-none absolute left-1/2 top-[50%] z-10 flex h-3 w-3 -translate-x-1/2 translate-y-[-50%] items-center justify-center rounded-full opacity-50 transition-all"
style={{
background: handleColor,
}}
/>
<div
data-testid={`gradient-handle-${testIdComplement}-${title.toLowerCase()}-${
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
}`}
className={classNames(
`pointer-events-none absolute left-1/2 top-[50%] z-10 flex -translate-x-1/2 translate-y-[-50%] items-center justify-center rounded-full transition-all`,
filterPresent
? openHandle || ownHandle
? cn("h-5 w-5")
: cn("h-1.5 w-1.5")
: cn("h-1.5 w-1.5 group-hover/node:h-5 group-hover/node:w-5"),
)}
style={{
background: handleColor,
}}
/>
</Handle>
</ShadTooltip>
<div
className={cn(
"absolute top-[50%] z-10 h-3 w-3 translate-y-[-50%] rounded-full bg-background",
left ? "-left-[4px] -ml-0.5" : "-right-[4px] -mr-0.5",
)}
/>
</div>
);
}

View file

@ -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 (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span
className="ps-2 text-xs text-foreground"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
};
export default TooltipRenderComponent;

View file

@ -9,12 +9,19 @@ export default function ShadTooltip({
children,
styleClasses,
delayDuration = 500,
open,
setOpen,
}: ShadToolTipType): JSX.Element {
return content ? (
<Tooltip defaultOpen={!children} delayDuration={delayDuration}>
<Tooltip
defaultOpen={!children}
open={open}
onOpenChange={setOpen}
delayDuration={delayDuration}
>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent
className={cn(styleClasses, "max-w-96")}
className={cn("max-w-96", styleClasses)}
side={side}
avoidCollisions={false}
sticky="always"

View file

@ -1,3 +1,4 @@
import { DefaultEdge } from "@/CustomEdges";
import NoteNode from "@/CustomNodes/NoteNode";
import IconComponent from "@/components/genericIconComponent";
import LoadingComponent from "@/components/loadingComponent";
@ -8,7 +9,6 @@ import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { getNodeRenderType, isSupportedNodeTypes } from "@/utils/utils";
import { ENABLE_MVPS } from "@/customization/feature-flags";
import _, { cloneDeep } from "lodash";
import {
KeyboardEvent,
@ -457,6 +457,8 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
connectionRadius={25}
edgeTypes={{ default: DefaultEdge }}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onNodeDragStop={onNodeDragStop}

View file

@ -18,7 +18,7 @@ export default function ParentDisclosureComponent({
data-testid={testId}
>
<div className="flex items-baseline gap-1 align-baseline">
<span className="parent-disclosure-title">{title}</span>
<span className="text-sm font-medium">{title}</span>
{beta && (
<div className="h-fit rounded-full bg-beta-background px-2 py-1 text-xs/3 font-semibold text-beta-foreground-soft">
BETA

View file

@ -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 {
<div className="side-bar-components-div-arrangement">
<div className="parent-disclosure-arrangement">
<div className="flex items-center gap-4 align-middle">
<span className="parent-disclosure-title">Components</span>
<div className="flex w-full flex-col items-start justify-between gap-2.5">
<span className="text-sm font-medium">Components</span>
{filterType && (
<SidebarFilterComponent
isInput={!!filterType.source}
type={filterType.type}
resetFilters={() => {
setFilterEdge([]);
setFilterData(data);
}}
/>
)}
</div>
</div>
<Separator />

View file

@ -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 (
<div className="mb-0.5 flex w-full items-center justify-between rounded border bg-muted p-1 px-2 text-xs font-medium text-primary">
<div className="flex flex-1 items-center gap-1.5">
<ForwardedIconComponent
name="ListFilter"
className="h-4 w-4 shrink-0 stroke-2"
/>
<div className="flex-1 overflow-hidden truncate">
{isInput ? "Input" : "Output"}: {type}
</div>
</div>
<ShadTooltip
side="right"
styleClasses="max-w-full"
content="Remove filter"
>
<Button unstyled className="shrink-0" onClick={resetFilters}>
<ForwardedIconComponent
name="X"
className="h-4 w-4 stroke-2"
aria-hidden="true"
/>
</Button>
</ShadTooltip>
</div>
);
}

View file

@ -431,6 +431,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
},
setFilterEdge: (newState) => {
if (newState.length === 0) {
set({ filterType: undefined });
}
set({ getFilterEdge: newState });
},
getFilterEdge: [],
@ -469,8 +472,6 @@ const useFlowStore = create<FlowStoreType>((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<FlowStoreType>((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<FlowStoreType>((set, get) => ({
});
}
}
get().updateEdgesRunningByNodes(
get().nodes.map((n) => n.id),
false,
);
get().setIsBuilding(false);
get().setLockChat(false);
},
@ -653,6 +659,10 @@ const useFlowStore = create<FlowStoreType>((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<FlowStoreType>((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<FlowStoreType>((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<FlowStoreType>((set, get) => ({
setBuildController: (controller) => {
set({ buildController: controller });
},
handleDragging: undefined,
setHandleDragging: (handleDragging) => {
set({ handleDragging });
},
filterType: undefined,
setFilterType: (filterType) => {
set({ filterType });
},
}));
export default useFlowStore;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 (

View file

@ -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,

View file

@ -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)",

View file

@ -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();
});
});

View file

@ -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();
});

View file

@ -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,

View file

@ -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();