Merge branch 'feature/output_dropdown' into two_edges

This commit is contained in:
ogabrielluiz 2024-05-29 09:42:29 -03:00
commit c6b315bf60
21 changed files with 844 additions and 87 deletions

View file

@ -201,7 +201,9 @@ class Vertex:
self.description = self.data["node"].get("description", "")
self.frozen = self.data["node"].get("frozen", False)
self.selected_output_type = self.data["node"].get("selected_output_type")
self.selected_output_type = (
str(self.data.get("selected_output_type")).strip() if self.data.get("selected_output_type") else None
)
self.is_input = self.data["node"].get("is_input") or self.is_input
self.is_output = self.data["node"].get("is_output") or self.is_output
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}

File diff suppressed because one or more lines are too long

View file

@ -253,6 +253,9 @@ def build_class_constructor(compiled_class, exec_globals, class_name):
globals()[module_name] = module
instance = exec_globals[class_name](*args, **kwargs)
# Get selected type from global scope
if instance.selected_output_type in exec_globals:
instance.selected_output_type = exec_globals[instance.selected_output_type]
return instance
build_custom_class.__globals__.update(exec_globals)

View file

@ -0,0 +1,31 @@
import { useRef } from "react";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { groupByFamily } from "../../../../utils/utils";
import TooltipRenderComponent from "../tooltipRenderComponent";
import { useTypesStore } from "../../../../stores/typesStore";
import { NodeType } from "../../../../types/flow";
import useFlowStore from "../../../../stores/flowStore";
export default function HandleTooltips({
left,
tooltipTitle,
}: {
left: boolean;
nodes: NodeType[];
tooltipTitle: string;
}) {
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>;
}
}

View file

@ -0,0 +1,59 @@
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import { outputComponentType } from "../../../../types/components";
import { cn } from "../../../../utils/utils";
import useFlowStore from "../../../../stores/flowStore";
import { NodeDataType } from "../../../../types/flow";
import { cloneDeep } from "lodash";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../../../../components/ui/dropdown-menu";
import { useUpdateNodeInternals } from "reactflow";
export default function OutputComponent({
selected,
types,
frozen = false,
nodeId,
idx,
}: outputComponentType) {
const setNode = useFlowStore((state) => state.setNode);
const updateNodeInternals = useUpdateNodeInternals();
if (types.length < 2) {
return <span className={cn(frozen ? " text-ice" : "")}>{selected}</span>;
}
return (
<DropdownMenu>
<DropdownMenuTrigger>
<span
className={cn(frozen ? " text-ice" : "", "flex items-center gap-1")}
>
{selected}
<ForwardedIconComponent name="ChevronDown" className="h-4 w-4" />
</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
{types.map((type) => (
<DropdownMenuItem
onSelect={() => {
// TODO: UDPDATE SET NODE TO NEW NODE FORM
setNode(nodeId, (node) => {
const newNode = cloneDeep(node);
(newNode.data as NodeDataType).node!.outputs![idx].selected =
type;
return newNode;
});
updateNodeInternals(nodeId);
}}
>
{type}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -0,0 +1,591 @@
import { cloneDeep } from "lodash";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
import Dropdown from "../../../../components/dropdownComponent";
import FloatComponent from "../../../../components/floatComponent";
import ForwardedIconComponent, {
default as IconComponent,
} from "../../../../components/genericIconComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent";
import KeypairListComponent from "../../../../components/keypairListComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../../../../components/ui/dropdown-menu";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { Case } from "../../../../shared/components/caseComponent";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import {
debouncedHandleUpdateValues,
handleUpdateValues,
} from "../../../../utils/parameterUtils";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import { nodeColors } from "../../../../utils/styleUtils";
import { classNames, cn, groupByFamily } from "../../../../utils/utils";
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import TooltipRenderComponent from "../tooltipRenderComponent";
import HandleTooltips from "../HandleTooltipComponent";
import OutputComponent from "../OutputComponent";
export default function ParameterComponent({
left,
id,
data,
tooltipTitle,
title,
color,
type,
name = "",
required = false,
optionalHandle = null,
info = "",
proxy,
showNode,
index,
}: ParameterComponentType): JSX.Element {
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [isLoading, setIsLoading] = useState(false);
const updateNodeInternals = useUpdateNodeInternals();
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
isLoading,
setIsLoading,
);
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
);
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
useHandleRefreshButtonPress(setIsLoading, setNode);
let disabled =
edges.some(
(edge) =>
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id),
) ?? false;
const handleRefreshButtonPress = async (name, data) => {
handleRefreshButtonPressHook(name, data);
};
useFetchDataOnMount(data, name, handleUpdateValues, setNode, setIsLoading);
const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[],
skipSnapshot: boolean | undefined = false,
): Promise<void> => {
handleOnNewValueHook(newValue, skipSnapshot);
};
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
handleNodeClassHook(newNodeClass, code);
};
useEffect(() => {
// @ts-ignore
infoHtml.current = (
<div className="h-full w-full break-words">
{info.split("\n").map((line, index) => (
<p key={index} className="block">
{line}
</p>
))}
</div>
);
}, [info]);
function renderTitle() {
return !left ? (
<OutputComponent
idx={index}
types={type?.split("|") ?? []}
selected={title}
nodeId={data.id}
frozen={data.node?.frozen}
/>
) : (
<span>{title}</span>
);
}
// If optionalHandle is an empty list, then it is not an optional handle
if (optionalHandle && optionalHandle.length === 0) {
optionalHandle = null;
}
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={1000}
content={
<HandleTooltips
left={left}
nodes={nodes}
tooltipTitle={tooltipTitle!}
/>
}
side={left ? "left" : "right"}
>
<Handle
data-test-id={`handle-${title.toLowerCase()}-${
left ? "target" : "source"
}`}
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background",
!showNode ? "mt-0" : "",
)}
style={{
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(
groupByFamily(myData, tooltipTitle!, left, nodes!),
);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)
) : (
<div
className={
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
((name === "code" && type === "code") ||
(name.includes("code") && proxy)
? " hidden "
: "")
}
>
<>
<div
className={
"flex w-full items-center truncate text-sm" +
(left ? "" : " justify-end")
}
>
<Case condition={left && data.node?.frozen}>
<div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
</Case>
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
{renderTitle()}
</ShadTooltip>
) : (
renderTitle()
)}
<span className={(required ? "ml-2 " : "") + "text-status-red"}>
{required ? "*" : ""}
</span>
<div className="">
{info !== "" && (
<ShadTooltip content={infoHtml.current}>
{/* put div to avoid bug that does not display tooltip */}
<div>
<IconComponent
name="Info"
className="relative bottom-px ml-1.5 h-3 w-4"
/>
</div>
</ShadTooltip>
)}
</div>
</div>
{left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={1000}
content={
<HandleTooltips
left={left}
nodes={nodes}
tooltipTitle={tooltipTitle!}
/>
}
side={left ? "left" : "right"}
>
<Handle
data-test-id={`handle-${title.toLowerCase()}-${
left ? "left" : "right"
}`}
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "-ml-0.5" : "-mr-0.5",
"h-3 w-3 rounded-full border-2 bg-background",
)}
style={{ borderColor: color ?? nodeColors.unknown }}
onClick={() => {
setFilterEdge(
groupByFamily(myData, tooltipTitle!, left, nodes!),
);
}}
/>
</ShadTooltip>
</div>
</Button>
)}
<Case
condition={
left === true &&
type === "str" &&
!data.node?.template[name]?.options
}
>
<div className="w-full">
<Case condition={data.node?.template[name]?.list}>
<div
className={
// Commenting this out until we have a better
// way to display
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
"flex-grow"
}
>
<InputListComponent
componentName={name}
disabled={disabled}
value={
!data.node!.template[name]?.value ||
data.node!.template[name]?.value === ""
? [""]
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
/>
</div>
</Case>
<Case condition={data.node?.template[name]?.multiline}>
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
value={data.node!.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node!.template[name]?.name}
data-testid={"textarea-" + data.node!.template[name]?.name}
/>
</div>
{data.node?.template[name]?.refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
</Case>
<Case
condition={
!data.node?.template[name]?.multiline &&
!data.node?.template[name]?.list
}
>
<div className="mt-2 flex w-full items-center">
<div
className={
"flex-grow " +
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
}
>
<InputGlobalComponent
disabled={disabled}
onChange={handleOnNewValue}
setDb={(value) => {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[name].load_from_db = value;
return newNode;
});
}}
name={name}
data={data}
/>
</div>
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
</Case>
</div>
</Case>
<Case condition={left === true && type === "bool"}>
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name]?.value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
</Case>
<Case condition={left === true && type === "float"}>
<div className="mt-2 w-full">
<FloatComponent
disabled={disabled}
value={data.node?.template[name]?.value ?? ""}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>
</Case>
<Case
condition={
left === true &&
type === "str" &&
(data.node?.template[name]?.options ||
data.node?.template[name]?.real_time_refresh)
}
>
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node!.template[name]?.options}
onSelect={handleOnNewValue}
value={data.node!.template[name]?.value}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
</Case>
<Case condition={left === true && type === "code"}>
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name]?.dynamic
? true
: false
}
dynamic={data.node?.template[name]?.dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + name}
/>
</div>
</Case>
<Case condition={left === true && type === "file"}>
<div className="mt-2 w-full">
<InputFileComponent
disabled={disabled}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name]?.fileTypes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
></InputFileComponent>
</div>
</Case>
<Case condition={left === true && type === "int"}>
<div className="mt-2 w-full">
<IntComponent
rangeSpec={data.node?.template[name]?.rangeSpec}
disabled={disabled}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + name}
/>
</div>
</Case>
<Case condition={left === true && type === "prompt"}>
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
</Case>
<Case condition={left === true && type === "NestedDict"}>
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
!data.node!.template[name]?.value ||
data.node!.template[name]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
id="div-dict-input"
/>
</div>
</Case>
<Case condition={left === true && type === "dict"}>
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name]?.value?.length === 0 ||
!data.node!.template[name]?.value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name]?.value, type!)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
// if data.node?.template[name]?.list is true, then the value is an array of objects
// else we need to get the first object of the array
if (data.node?.template[name]?.list) {
handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]);
}}
isList={data.node?.template[name]?.list ?? false}
/>
</div>
</Case>
</>
</div>
);
}

View file

@ -553,7 +553,7 @@ export default function GenericNode({
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
index={idx.toString()}
index={idx}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
@ -626,6 +626,7 @@ export default function GenericNode({
)
)}
<ParameterComponent
index={0}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
@ -636,7 +637,7 @@ export default function GenericNode({
title={
data.node?.output_types &&
data.node.output_types.length > 0
? data.node.output_types.join(" | ")
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
@ -644,6 +645,7 @@ export default function GenericNode({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
idx: 0,
}}
type={data.node?.base_classes.join("|")}
left={false}
@ -803,7 +805,7 @@ export default function GenericNode({
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
index={idx.toString()}
index={idx}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
@ -880,29 +882,36 @@ export default function GenericNode({
>
{" "}
</div>
<div>
{data.node!.base_classes.length > 0 &&
// if conditional_paths in data.node.conditional_paths
// then buildParameterComponent for each conditionalPath
// else buildParameterComponent for each data.node.base_classes
// first we check if there are any conditional paths
data.node!.conditional_paths &&
data.node!.conditional_paths.length > 0
? data.node!.conditional_paths.map((conditionalPath) =>
buildParameterComponent({
data,
conditionalPath,
showNode,
left: false,
})
)
: buildParameterComponent({
data,
conditionalPath: null,
showNode,
left: false,
{data.node!.outputs &&
data.node!.outputs.length > 0 &&
data.node!.outputs.map((output, idx) => (
<ParameterComponent
index={idx}
key={scapedJSONStringfy({
baseClasses: [output.selected ?? output.types[0]],
id: data.id,
dataType: data.type,
})}
</div>
data={data}
color={
nodeColors[output.selected ?? output.types[0]] ??
nodeColors[types[output.selected ?? output.types[0]]] ??
nodeColors[types[data.type]] ??
nodeColors.unknown
}
title={output.selected ?? output.types[0]}
tooltipTitle={output.selected ?? output.types[0]}
id={{
baseClasses: [output.selected ?? output.types[0]],
id: data.id,
dataType: data.type,
idx: idx,
}}
type={output.types.join("|")}
left={false}
showNode={showNode}
/>
))}
</>
</div>
)}

View file

@ -8,7 +8,6 @@ const useFetchDataOnMount = (
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -44,7 +43,6 @@ const useFetchDataOnMount = (
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();

View file

@ -9,7 +9,6 @@ const useHandleOnNewValue = (
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
) => {
@ -65,8 +64,6 @@ const useHandleOnNewValue = (
return newNode;
});
renderTooltips();
};
return { handleOnNewValue };

View file

@ -6,7 +6,6 @@ const useHandleNodeClass = (
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
) => {
const handleNodeClass = (newNodeClass, code) => {
if (!data.node) return;
@ -30,8 +29,6 @@ const useHandleNodeClass = (
});
updateNodeInternals(data.id);
renderTooltips();
};
return { handleNodeClass };

View file

@ -3,7 +3,7 @@ import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { handleUpdateValues } from "../../utils/parameterUtils";
const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
const useHandleRefreshButtonPress = (setIsLoading, setNode) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleRefreshButtonPress = async (name, data) => {
@ -30,7 +30,6 @@ const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
});
}
setIsLoading(false);
renderTooltips();
};
return { handleRefreshButtonPress };

View file

@ -30,12 +30,12 @@ export async function addFolder(data: AddFolderType): Promise<FolderType> {
export async function updateFolder(
body: FolderType,
folderId: string
folderId: string,
): Promise<FolderType> {
try {
const response = await api.patch(
`${BASE_URL_API}folders/${folderId}`,
body
body,
);
return response?.data;
} catch (error) {
@ -68,7 +68,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{
}> {
try {
const response = await api.get(
`${BASE_URL_API}folders/download/${folderId}`
`${BASE_URL_API}folders/download/${folderId}`,
);
if (response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
@ -82,7 +82,7 @@ export async function downloadFlowsFromFolders(folderId: string): Promise<{
}
export async function uploadFlowsFromFolders(
flows: FormData
flows: FormData,
): Promise<FlowType[]> {
try {
const response = await api.post(`${BASE_URL_API}folders/upload/`, flows);
@ -99,11 +99,11 @@ export async function uploadFlowsFromFolders(
export async function moveFlowToFolder(
flowId: string,
folderId: string
folderId: string,
): Promise<FlowType> {
try {
const response = await api.patch(
`${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`
`${BASE_URL_API}folders/move_to_folder/${flowId}/${folderId}`,
);
return response?.data;
} catch (error) {

View file

@ -199,11 +199,10 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
position?: XYPosition,
fromDragAndDrop?: boolean,
): Promise<string | undefined> => {
let flowData = flow
? processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
if (newProject) {
let flowData = flow
? processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
const folder_id = useFolderStore.getState().folderUrl;
const my_collection_id = useFolderStore.getState().myCollectionId;
@ -272,7 +271,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
useFlowStore
.getState()
.paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
{ nodes: flowData?.nodes, edges: flowData?.edges },
position ?? { x: 10, y: 10 },
);
}

View file

@ -28,6 +28,7 @@ export type APIClassType = {
documentation: string;
error?: string;
official?: boolean;
outputs?: Array<{ types: Array<string>; selected?: string }>;
frozen?: boolean;
flow?: FlowType;
field_order?: string[];
@ -39,7 +40,8 @@ export type APIClassType = {
| FlowType
| CustomFieldsType
| boolean
| undefined;
| undefined
| Array<{ types: Array<string>; selected?: string }>;
};
export type TemplateVariableType = {

View file

@ -68,7 +68,7 @@ export type ParameterComponentType = {
info?: string;
proxy?: { field: string; id: string };
showNode?: boolean;
index?: string;
index: number;
onCloseModal?: (close: boolean) => void;
};
export type InputListComponentType = {
@ -112,6 +112,14 @@ export type TextAreaComponentType = {
readonly?: boolean;
};
export type outputComponentType = {
types: string[];
selected: string;
nodeId: string;
frozen?: boolean;
idx: number;
};
export type PromptAreaComponentType = {
field_name?: string;
nodeClass?: APIClassType;

View file

@ -34,6 +34,7 @@ export type NodeDataType = {
node?: APIClassType;
id: string;
output_types?: string[];
selected_output_type?: string;
buildStatus?: BuildStatus;
};
// FlowStyleType is the type of the style object that is used to style the
@ -58,6 +59,7 @@ export type sourceHandleType = {
id: string;
baseClasses: string[];
conditionalPath?: string | null;
idx: number;
};
//left side
export type targetHandleType = {

View file

@ -16,7 +16,7 @@ type BuildVerticesParams = {
onBuildUpdate?: (
data: VertexBuildTypeAPI,
status: BuildStatus,
buildId: string
buildId: string,
) => void; // Replace any with the actual type if it's not any
onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
@ -53,7 +53,7 @@ export async function updateVerticesOrder(
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?: Node[],
edges?: Edge[]
edges?: Edge[],
): Promise<{
verticesLayers: VertexLayerElementType[][];
verticesIds: string[];
@ -69,7 +69,7 @@ export async function updateVerticesOrder(
startNodeId,
stopNodeId,
nodes,
edges
edges,
);
} catch (error: any) {
setErrorData({
@ -125,7 +125,7 @@ export async function buildVertices({
startNodeId,
stopNodeId,
nodes,
edges
edges,
);
if (onValidateNodes) {
try {
@ -187,14 +187,14 @@ export async function buildVertices({
onBuildUpdate(
getInactiveVertexData(element.id),
BuildStatus.INACTIVE,
runId
runId,
);
}
if (element.reference) {
onBuildUpdate(
getInactiveVertexData(element.reference),
BuildStatus.INACTIVE,
runId
runId,
);
}
buildResults.push(false);
@ -219,7 +219,7 @@ export async function buildVertices({
if (stop) {
return;
}
})
}),
);
// Once the current layer is built, move to the next layer
currentLayerIndex += 1;
@ -262,7 +262,7 @@ async function buildVertex({
onBuildError!(
"Error Building Component",
[buildData.params],
verticesIds.map((id) => ({ id }))
verticesIds.map((id) => ({ id })),
);
stopBuild();
}
@ -273,7 +273,7 @@ async function buildVertex({
onBuildError!(
"Error Building Component",
[(error as AxiosError<any>).response?.data?.detail ?? "Unknown Error"],
verticesIds.map((id) => ({ id }))
verticesIds.map((id) => ({ id })),
);
stopBuild();
}

View file

@ -16,6 +16,8 @@ import {
specialCharsRegex,
} from "../constants/constants";
import { downloadFlowsFromDatabase } from "../controllers/API";
import getFieldTitle from "../customNodes/utils/get-field-title";
import { DESCRIPTIONS } from "../flow_constants";
import {
APIClassType,
APIKindType,
@ -37,8 +39,6 @@ import {
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { createRandomKey, toTitleCase } from "./utils";
import { DESCRIPTIONS } from "../flow_constants";
import getFieldTitle from "../customNodes/utils/get-field-title";
const uid = new ShortUniqueId({ length: 5 });
export function checkChatInput(nodes: Node[]) {
@ -46,6 +46,7 @@ export function checkChatInput(nodes: Node[]) {
}
export function cleanEdges(nodes: Node[], edges: Edge[]) {
console.log("cleanEdges");
let newEdges = cloneDeep(edges);
edges.forEach((edge) => {
// check if the source and target node still exists
@ -75,10 +76,12 @@ export function cleanEdges(nodes: Node[], edges: Edge[]) {
}
}
if (sourceHandle) {
const index = scapeJSONParse(sourceHandle).idx ?? 0;
const id: sourceHandleType = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
baseClasses: [sourceNode.data.node.outputs[index].selected],
dataType: sourceNode.data.type,
idx: index,
};
if (scapedJSONStringfy(id) !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
@ -191,8 +194,8 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null;
if (data) {
processFlowEdges(flow);
//prevent node update for now
// processFlowNodes(flow);
//add dropdown option to nodeOutputs
processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
// updateNodes(data.nodes, data.edges);
@ -397,6 +400,7 @@ export function updateEdgesHandleIds({
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
idx: 0,
};
}
edge.sourceHandle = scapedJSONStringfy(newSource!);
@ -410,6 +414,49 @@ export function updateEdgesHandleIds({
return newEdges;
}
export function updateNewOutput({ nodes, edges }: updateEdgesHandleIdsType) {
console.log("updateNewOutput");
let newEdges = cloneDeep(edges);
let newNodes = cloneDeep(nodes);
newEdges.forEach((edge) => {
if (edge.sourceHandle && edge.targetHandle) {
let newSourceHandle: sourceHandleType = scapeJSONParse(edge.sourceHandle);
let newTargetHandle: targetHandleType = scapeJSONParse(edge.targetHandle);
let intersection;
if (newTargetHandle.inputTypes && newTargetHandle.inputTypes.length > 0) {
//conjuction subtraction
intersection = newSourceHandle.baseClasses.filter((type) =>
newTargetHandle.inputTypes!.includes(type),
);
} else {
intersection = newSourceHandle.baseClasses.filter(
(type) => type === newTargetHandle.type,
);
}
const selected = intersection[0];
newSourceHandle.baseClasses = [selected];
const id = newSourceHandle.id;
newSourceHandle.idx = 0;
const sourceNodeIndex = newNodes.findIndex((node) => node.id === id);
if (sourceNodeIndex > -1) {
const sourceNode = newNodes[sourceNodeIndex];
if (
!sourceNode.data.node?.outputs ||
sourceNode.data.node!.outputs!.length === 0
) {
sourceNode.data.node!.outputs = [
{ types: sourceNode.data.node!.base_classes, selected: selected },
];
}
}
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
edge.data.sourceHandle = newSourceHandle;
}
});
return { nodes: newNodes, edges: newEdges };
}
export function handleKeyDown(
e:
| React.KeyboardEvent<HTMLInputElement>
@ -561,6 +608,10 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean {
);
}
export function checkOldNodesOutput(nodes: NodeType[]): boolean {
return nodes.some((node) => !node.data.node?.outputs);
}
export function customStringify(obj: any): string {
if (typeof obj === "undefined") {
return "null";
@ -1017,6 +1068,15 @@ export function processFlowEdges(flow: FlowType) {
});
}
export function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
if (checkOldNodesOutput(flow.data.nodes)) {
const { nodes, edges } = updateNewOutput(flow.data);
flow.data.nodes = nodes;
flow.data.edges = edges;
}
}
export function expandGroupNode(
id: string,
flow: FlowType,

View file

@ -24,7 +24,7 @@ test("chat_io_teste", async ({ page }) => {
const jsonContent = readFileSync(
"tests/end-to-end/assets/ChatTest.json",
"utf-8"
"utf-8",
);
await page.getByTestId("blank-flow").click();
@ -47,7 +47,7 @@ test("chat_io_teste", async ({ page }) => {
"drop",
{
dataTransfer,
}
},
);
await page.getByLabel("fit view").click();
await page.getByText("Playground", { exact: true }).click();

View file

@ -57,7 +57,7 @@ test("user must interact with chat with Input/Output", async ({ page }) => {
.getByTestId("textarea-input_value")
.nth(1)
.fill(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!"
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
);
await page
.getByTestId("popover-anchor-input-sender_name")
@ -85,8 +85,8 @@ test("user must interact with chat with Input/Output", async ({ page }) => {
await page
.getByText(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
{ exact: true }
{ exact: true },
)
.isVisible()
.isVisible(),
);
});

View file

@ -41,19 +41,19 @@ test("InputListComponent", async ({ page }) => {
await page.getByTestId("edit-button-modal").click();
expect(
await page.getByTestId("showmetadata_indexing_exclude").isChecked()
await page.getByTestId("showmetadata_indexing_exclude").isChecked(),
).toBeFalsy();
await page.getByTestId("showmetadata_indexing_exclude").click();
expect(
await page.getByTestId("showmetadata_indexing_exclude").isChecked()
await page.getByTestId("showmetadata_indexing_exclude").isChecked(),
).toBeTruthy();
expect(
await page.getByTestId("showmetadata_indexing_include").isChecked()
await page.getByTestId("showmetadata_indexing_include").isChecked(),
).toBeFalsy();
await page.getByTestId("showmetadata_indexing_include").click();
expect(
await page.getByTestId("showmetadata_indexing_include").isChecked()
await page.getByTestId("showmetadata_indexing_include").isChecked(),
).toBeTruthy();
await page
@ -93,7 +93,7 @@ test("InputListComponent", async ({ page }) => {
.click();
const plusButtonLocator = page.getByTestId(
"input-list-plus-btn_metadata_indexing_include-1"
"input-list-plus-btn_metadata_indexing_include-1",
);
const elementCount = await plusButtonLocator?.count();
@ -164,12 +164,12 @@ test("InputListComponent", async ({ page }) => {
.click();
const plusButtonLocatorEdit0 = await page.getByTestId(
"input-list-plus-btn-edit_metadata_indexing_include-0"
"input-list-plus-btn-edit_metadata_indexing_include-0",
);
const elementCountEdit0 = await plusButtonLocatorEdit0?.count();
const plusButtonLocatorEdit2 = await page.getByTestId(
"input-list-plus-btn-edit_metadata_indexing_include-2"
"input-list-plus-btn-edit_metadata_indexing_include-2",
);
const elementCountEdit2 = await plusButtonLocatorEdit2?.count();
@ -178,13 +178,13 @@ test("InputListComponent", async ({ page }) => {
}
const minusButtonLocatorEdit1 = await page.getByTestId(
"input-list-minus-btn-edit_metadata_indexing_include-1"
"input-list-minus-btn-edit_metadata_indexing_include-1",
);
const elementCountMinusEdit1 = await minusButtonLocatorEdit1?.count();
const minusButtonLocatorEdit2 = await page.getByTestId(
"input-list-minus-btn-edit_metadata_indexing_include-2"
"input-list-minus-btn-edit_metadata_indexing_include-2",
);
const elementCountMinusEdit2 = await minusButtonLocatorEdit2?.count();