Merge remote-tracking branch 'origin/dev' into aws-cdk

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-12-10 12:15:08 -03:00
commit 01c23fdb77
316 changed files with 13915 additions and 7688 deletions

File diff suppressed because it is too large Load diff

View file

@ -111,7 +111,7 @@
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"daisyui": "^3.6.3",
"daisyui": "^4.0.4",
"postcss": "^8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3",

View file

@ -42,9 +42,7 @@ export default function App() {
successData,
successOpen,
setSuccessOpen,
setErrorData,
loading,
setLoading,
} = useContext(alertContext);
const navigate = useNavigate();
const { fetchError } = useContext(typesContext);

View file

@ -22,11 +22,20 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { postCustomComponentUpdate } from "../../../../controllers/API";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import {
cleanEdges,
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
@ -44,7 +53,6 @@ export default function ParameterComponent({
left,
id,
data,
setData,
tooltipTitle,
title,
color,
@ -60,9 +68,13 @@ export default function ParameterComponent({
const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const { setErrorData, modalContextOpen } = useContext(alertContext);
const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0);
const { setTabsState, tabId, flows } = useContext(FlowsContext);
const { setTabsState, tabId, flows, tabsState, updateFlow } =
useContext(FlowsContext);
const [dataRef, setDataRef] = useState(data);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -80,38 +92,103 @@ export default function ParameterComponent({
const groupedEdge = useRef(null);
useEffect(() => {
setDataRef(data);
}, [modalContextOpen]);
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled =
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
.some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(proxy ? { ...id, proxy } : id)
) ?? false;
const { data: myData } = useContext(typesContext);
const { takeSnapshot } = useContext(undoRedoContext);
const handleUpdateValues = async (name: string, data: NodeDataType) => {
const code = data.node?.template["code"]?.value;
if (!code) {
console.error("Code not found in the template");
return;
}
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
};
const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
let newData = cloneDeep(data);
newData.node!.template[name].value = newValue;
setData(newData);
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
data.node!.template[name].value = newValue;
updateNodeInternals(data.id);
setDataRef((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
// Set state to pending
//@ts-ignore
setTabsState((prev: TabsState) => {
if (!prev[tabId]) {
return prev;
}
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
formKeysData: prev[tabId].formKeysData,
},
};
});
if (data.node!.template[name].value !== newValue) {
const tabs = cloneDeep(tabsState);
tabs[tabId].isPending = false;
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
setTabsState({
...tabs,
});
}
renderTooltips();
};
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (data.node!.template[name].value !== newNodeClass.template[name].value) {
takeSnapshot();
}
data.node = newNodeClass;
data.node.template[name].value = code;
updateNodeInternals(data.id);
// Set state to pending
//@ts-ignore
if (data.node!.template[name].value !== code) {
const tabs = cloneDeep(tabsState);
tabs[tabId].isPending = false;
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
setTabsState({
...tabs,
});
}
renderTooltips();
let flow = flows.find((flow) => flow.id === tabId);
if (reactFlowInstance && flow && flow.data) {
cleanEdges({
flow: {
edges: flow.data!.edges,
nodes: flow.data!.nodes,
},
updateEdge: (edge) => {
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
@ -168,21 +245,40 @@ export default function ParameterComponent({
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}{" "}
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
{item?.display_name && item?.display_name?.length > 0 ? (
<span className="text-xs">
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
@ -199,17 +295,7 @@ export default function ParameterComponent({
}, [tooltipTitle, flow]);
return !showNode ? (
left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
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">
@ -282,17 +368,7 @@ export default function ParameterComponent({
)}
</div>
</div>
{left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
{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">
@ -352,6 +428,7 @@ export default function ParameterComponent({
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + index}
data-testid={"textarea-" + index}
/>
) : (
<InputComponent
@ -386,12 +463,26 @@ export default function ParameterComponent({
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
<div className="mt-2 w-full">
<Dropdown
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
></Dropdown>
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
)}
</div>
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
@ -402,9 +493,7 @@ export default function ParameterComponent({
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
@ -419,7 +508,6 @@ export default function ParameterComponent({
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
suffixes={data.node?.template[name].suffixes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
@ -439,19 +527,13 @@ export default function ParameterComponent({
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
const clone = cloneDeep(data);
clone.node = nodeClass;
setData(clone);
}}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={(e) => {
handleOnNewValue(e);
}}
onChange={handleOnNewValue}
id={"prompt-input-" + index}
data-testid={"prompt-input-" + index}
/>
</div>
) : left === true && type === "NestedDict" ? (
@ -471,6 +553,7 @@ export default function ParameterComponent({
data.node!.template[name].value = newValue;
handleOnNewValue(newValue);
}}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
@ -479,10 +562,10 @@ export default function ParameterComponent({
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
dataRef.node!.template[name].value?.length === 0 ||
!dataRef.node!.template[name].value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
: convertObjToArray(dataRef.node!.template[name].value)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {

View file

@ -1,4 +1,3 @@
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
@ -6,23 +5,22 @@ import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { priorityFields } from "../../constants/constants";
import { useSSE } from "../../contexts/SSEContext";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { undoRedoContext } from "../../contexts/undoRedoContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import {
cleanEdges,
handleKeyDown,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
data: olddata,
data,
xPos,
yPos,
selected,
@ -32,13 +30,13 @@ export default function GenericNode({
xPos: number;
yPos: number;
}): JSX.Element {
const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(FlowsContext);
const { updateFlow, flows, tabId, saveCurrentFlow } =
useContext(FlowsContext);
const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(true);
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
@ -46,9 +44,11 @@ export default function GenericNode({
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
const [handles, setHandles] = useState<boolean[] | []>([]);
let numberOfInputs: boolean[] = [];
const { modalContextOpen } = useContext(alertContext);
const { takeSnapshot } = useContext(undoRedoContext);
function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template)
@ -81,37 +81,18 @@ export default function GenericNode({
useEffect(() => {
countHandles();
}, []);
}, [data, data.node]);
// State for outline color
const { sseData, isBuilding } = useSSE();
useEffect(() => {
olddata.node = data.node;
let myFlow = flows.find((flow) => flow.id === tabId);
if (reactFlowInstance && myFlow) {
let flow = cloneDeep(myFlow);
flow.data = reactFlowInstance.toObject();
cleanEdges({
flow: {
edges: flow.data.edges,
nodes: flow.data.nodes,
},
updateEdge: (edge) => {
flow.data!.edges = edge;
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
countHandles();
}, [data]);
useEffect(() => {
setTimeout(() => {
updateNodeInternals(data.id);
}, 300);
}, [showNode]);
setNodeDescription(data.node!.description);
}, [data.node!.description]);
useEffect(() => {
setNodeName(data.node!.display_name);
}, [data.node!.display_name]);
// New useEffect to watch for changes in sseData and update validation status
useEffect(() => {
@ -123,15 +104,23 @@ export default function GenericNode({
setValidationStatus(null);
}
}, [sseData, data.id]);
const showNode = data.showNode ?? true;
return (
<>
<NodeToolbar>
<NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data}
setData={setData}
deleteNode={deleteNode}
setShowNode={setShowNode}
deleteNode={(id) => {
takeSnapshot();
deleteNode(id);
saveCurrentFlow();
}}
setShowNode={(show: boolean) => {
data.showNode = show;
}}
numberOfHandles={handles}
showNode={showNode}
></NodeToolbarComponent>
@ -140,10 +129,7 @@ export default function GenericNode({
<div
className={classNames(
selected ? "border border-ring" : "border",
" transition-transform ",
showNode
? " w-96 scale-100 transform rounded-lg duration-500 ease-in-out "
: " transform-width w-26 h-26 scale-90 transform rounded-full duration-500 ",
showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
"generic-node-div"
)}
>
@ -154,6 +140,7 @@ export default function GenericNode({
)}
<div>
<div
data-testid={"div-generic-node"}
className={
"generic-node-div-title " +
(!showNode
@ -168,10 +155,10 @@ export default function GenericNode({
}
>
<IconComponent
name={data.node?.flow ? "Ungroup" : name}
name={data.node?.flow ? "group_components" : name}
className={
"generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12")
(!showNode ? "absolute inset-x-6 h-12 w-12" : "")
}
iconColor={`${nodeColors[types[data.type]]}`}
/>
@ -180,7 +167,6 @@ export default function GenericNode({
{data.node?.flow && inputName ? (
<div>
<InputComponent
autoFocus
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
@ -199,10 +185,21 @@ export default function GenericNode({
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="generic-node-tooltip-div text-primary"
onDoubleClick={() => setInputName(true)}
className="flex"
onDoubleClick={() => {
setInputName(true);
takeSnapshot();
}}
>
{data.node?.display_name}
<div className="generic-node-tooltip-div pr-2 text-primary">
{data.node?.display_name}
</div>
{data.node?.flow && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
/>
)}
</div>
</ShadTooltip>
)}
@ -229,7 +226,6 @@ export default function GenericNode({
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
@ -278,7 +274,6 @@ export default function GenericNode({
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types &&
@ -366,7 +361,7 @@ export default function GenericNode({
<div
className={
showNode
? "generic-node-desc " +
? "generic-node-desc overflow-hidden " +
(data.node?.description !== "" ? "py-5" : "pb-5")
: ""
}
@ -408,8 +403,11 @@ export default function GenericNode({
/>
) : (
<div
className="generic-node-desc-text"
onDoubleClick={() => setInputDescription(true)}
className="generic-node-desc-text truncate-multiline word-break-break-word"
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
}}
>
{data.node?.description}
</div>
@ -417,7 +415,15 @@ export default function GenericNode({
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort()
.sort((a, b) => {
if (priorityFields.has(a.toLowerCase())) {
return -1;
} else if (priorityFields.has(b.toLowerCase())) {
return 1;
} else {
return a.localeCompare(b);
}
})
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
@ -433,13 +439,12 @@ export default function GenericNode({
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
data.node?.template[templateField].type!
] ??
nodeColors[
data.node?.template[templateField].type!
types[data.node?.template[templateField].type!]
] ??
nodeColors.unknown
}
@ -483,30 +488,37 @@ export default function GenericNode({
>
{" "}
</div>
<ParameterComponent
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
{data.node!.base_classes.length > 0 && (
<ParameterComponent
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
color={
(data.node?.output_types &&
data.node.output_types.length > 0
? nodeColors[data.node.output_types[0]] ??
nodeColors[types[data.node.output_types[0]]]
: nodeColors[types[data.type]]) ?? nodeColors.unknown
}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
)}
</>
</div>
)}

View file

@ -30,7 +30,7 @@ export default function AlertDropdown({
}}
>
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="flex h-[500px] w-[500px] flex-col">
<PopoverContent className="nocopy nopan nodelete nodrag noundo flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">

View file

@ -40,7 +40,7 @@ export default function ErrorAlert({
removeAlert(id);
}, 500);
}}
className="error-build-message"
className="error-build-message nocopy nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -36,7 +36,7 @@ export default function NoticeAlert({
setShow(false);
removeAlert(id);
}}
className="mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
className="nocopy nopan nodelete nodrag noundo mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -34,7 +34,7 @@ export default function SuccessAlert({
setShow(false);
removeAlert(id);
}}
className="success-alert"
className="success-alert nocopy nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -1,31 +1,18 @@
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import React, { ChangeEvent, useState } from "react";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea";
import { readFlowsFromDatabase } from "../../controllers/API";
import { InputProps } from "../../types/components";
import { FlowType } from "../../types/flow";
export const EditFlowSettings: React.FC<InputProps> = ({
name,
invalidName,
setInvalidName,
invalidNameList,
description,
maxLength = 50,
flows,
tabId,
setName,
setDescription,
}: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const nameLists = useRef<string[]>([]);
useEffect(() => {
readFlowsFromDatabase().then((flows) => {
flows.forEach((flow: FlowType) => {
nameLists.current.push(flow.name);
});
});
}, []);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
@ -34,19 +21,6 @@ export const EditFlowSettings: React.FC<InputProps> = ({
} else {
setIsMaxLength(false);
}
if (invalidName !== undefined) {
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
}
setName(value);
};
@ -62,9 +36,6 @@ export const EditFlowSettings: React.FC<InputProps> = ({
{isMaxLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
{invalidName && (
<span className="edit-flow-span">Name already in use</span>
)}
</div>
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"

View file

@ -11,11 +11,12 @@ import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function PaginatorComponent({
pageSize = 10,
pageSize = 12,
pageIndex = 1,
rowsCount = [10, 20, 50, 100],
rowsCount = [12, 24, 48, 96],
totalRowsCount = 0,
paginate,
storeComponent = false,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [maxIndex, setMaxPageIndex] = useState(
@ -30,7 +31,13 @@ export default function PaginatorComponent({
<>
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"></div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div
className={
storeComponent
? "flex items-center lg:space-x-8 "
: "flex items-center space-x-6 lg:space-x-8 "
}
>
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
@ -54,7 +61,8 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pageIndex} of {maxIndex}
Page {pageIndex}
{!storeComponent && <> of {maxIndex}</>}
</div>
<div className="flex items-center space-x-2">
<Button

View file

@ -11,6 +11,7 @@ const SanitizedHTMLWrapper = ({
return (
<div
data-testid="edit-prompt-sanitized"
className={className}
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
suppressContentEditableWarning={suppressWarning}

View file

@ -1,8 +1,16 @@
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { cardComponentPropsType } from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import { StoreContext } from "../../contexts/storeContext";
import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent from "../../utils/storeUtils";
import { cn } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Card,
CardDescription,
@ -11,48 +19,326 @@ import {
CardTitle,
} from "../ui/card";
export const CardComponent = ({
flow,
id,
onDelete,
export default function CollectionCardComponent({
data,
authorized = true,
disabled = false,
button,
}: cardComponentPropsType): JSX.Element => {
const { removeFlow } = useContext(FlowsContext);
onDelete,
}: {
data: storeComponent;
authorized?: boolean;
disabled?: boolean;
button?: JSX.Element;
onDelete?: () => void;
}) {
const { addFlow } = useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { setValidApiKey } = useContext(StoreContext);
const isStore = false;
const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false);
const [liked_by_user, setLiked_by_user] = useState(
data?.liked_by_user ?? false
);
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0
);
const name = data.is_component ? "Component" : "Flow";
useEffect(() => {
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
setLikes_count(data?.liked_by_count ?? 0);
setDownloads_count(data?.downloads_count ?? 0);
}
}, [data, data.liked_by_count, data.liked_by_user, data.downloads_count]);
function handleInstall() {
const temp = downloads_count;
setDownloads_count((old) => Number(old) + 1);
setLoading(true);
getComponent(data.id)
.then((res) => {
const newFlow = cloneFLowWithParent(res, res.id, data.is_component);
addFlow(true, newFlow)
.then((id) => {
setSuccessData({
title: `${name} ${
isStore ? "Downloaded" : "Installed"
} Successfully.`,
});
setLoading(false);
})
.catch((error) => {
setLoading(false);
setErrorData({
title: `Error ${
isStore ? "downloading" : "installing"
} the ${name}`,
list: [error["response"]["data"]["detail"]],
});
});
})
.catch((err) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [err["response"]["data"]["detail"]],
});
setDownloads_count(temp);
});
}
function handleLike() {
setLoadingLike(true);
if (liked_by_user !== undefined || liked_by_user !== null) {
const temp = liked_by_user;
const tempNum = likes_count;
setLiked_by_user((prev) => !prev);
if (!temp) {
setLikes_count((prev) => Number(prev) + 1);
} else {
setLikes_count((prev) => Number(prev) - 1);
}
postLikeComponent(data.id)
.then((response) => {
setLoadingLike(false);
setLikes_count(response.data.likes_count);
setLiked_by_user(response.data.liked_by_user);
})
.catch((error) => {
setLoadingLike(false);
setLikes_count(tempNum);
setLiked_by_user(temp);
if (error.response.status === 403) {
setValidApiKey(false);
} else {
console.error(error);
setErrorData({
title: `Error liking ${name}.`,
list: [error["response"]["data"]["detail"]],
});
}
});
}
}
return (
<Card className="group">
<CardHeader>
<CardTitle className="card-component-title-display">
<span
className={
"card-component-image " +
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
}
></span>
<span className="card-component-title-size">{flow.name}</span>
{onDelete && (
<button className="card-component-delete-button" onClick={onDelete}>
<Card
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<IconComponent
name="Trash2"
className="card-component-delete-icon"
className={cn(
"flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon"
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
</button>
)}
</CardTitle>
<CardDescription className="card-component-desc">
<div className="card-component-desc-text">
{flow.description}
{/* {flow.description} */}
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
{data?.metadata?.total ?? 0}
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
{likes_count ?? 0}
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
{downloads_count ?? 0}
</span>
</ShadTooltip>
</div>
)}
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/>
</DeleteConfirmationModal>
)}
</CardTitle>
</div>
</CardDescription>
</CardHeader>
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
⛓︎ v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="card-component-footer-arrangement">
<div className="card-component-footer"></div>
{button && button}
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<Button
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
>
<IconComponent
name="Heart"
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip
content={
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loading}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (loading || !authorized) {
return;
}
handleInstall();
}}
>
<IconComponent
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
className={cn(
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
</div>
)}
{button && button}
</div>
</div>
</CardFooter>
</Card>
);
};
}

View file

@ -0,0 +1,63 @@
import { useState } from "react";
import IconComponent from "../../components/genericIconComponent";
export default function CardsWrapComponent({
onFileDrop,
children,
dragMessage,
}: {
onFileDrop?: (e: any) => void;
children: JSX.Element | JSX.Element[];
dragMessage?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
if (onFileDrop) setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (onFileDrop) onFileDrop(e);
setIsDragging(false);
};
return (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<IconComponent name="ArrowUpToLine" className="h-12 w-12 stroke-1" />
{dragMessage ? dragMessage : "Drop your file here"}
</>
) : (
children
)}
</div>
);
}

View file

@ -26,7 +26,7 @@ export default function BuildTrigger({
}): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(FlowsContext);
const { setTabsState, saveFlow } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
@ -76,6 +76,7 @@ export default function BuildTrigger({
}
async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
const id = saveFlow(flow, true);
const response = await postBuildInit(flow);
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource

View file

@ -36,7 +36,7 @@ export default function Chat({ flow }: ChatType): JSX.Element {
// Define an async function within the useEffect hook
const fetchBuildStatus = async () => {
const response = await getBuildStatus(flow.id);
setIsBuilt(response.built);
setIsBuilt(response.data.built);
};
// Call the async function

View file

@ -45,6 +45,7 @@ export default function CodeAreaComponent({
<div className="flex w-full items-center">
<span
id={id}
data-testid={id}
className={
editNode
? "input-edit-node input-dialog"

View file

@ -27,6 +27,7 @@ import {
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import { darkContext } from "../../contexts/darkContext";
import { typesContext } from "../../contexts/typesContext";
import { codeTabsPropsType } from "../../types/components";
@ -242,24 +243,10 @@ export default function CodeTabsComponent({
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField]
.show &&
(node.data.node.template[templateField]
.type === "str" ||
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField]
.type === "bool" ||
node.data.node.template[templateField]
.type === "float" ||
node.data.node.template[templateField]
.type === "code" ||
node.data.node.template[templateField]
.type === "prompt" ||
node.data.node.template[templateField]
.type === "file" ||
node.data.node.template[templateField]
.type === "int" ||
node.data.node.template[templateField]
.type === "dict" ||
node.data.node.template[templateField]
.type === "NestedDict")
.type
)
)
.map((templateField, indx) => {
return (
@ -457,11 +444,6 @@ export default function CodeTabsComponent({
templateField
].fileTypes
}
suffixes={
node.data.node.template[
templateField
].suffixes
}
onFileChange={(
value: any
) => {

View file

@ -10,6 +10,7 @@ export default function DictComponent({
onChange,
disabled,
editNode = false,
id = "",
}: DictComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
@ -30,7 +31,7 @@ export default function DictComponent({
)}
>
{
<div className="flex w-full gap-3">
<div className="flex w-full gap-3" data-testid={id}>
<DictAreaModal
value={ref.current}
onChange={(obj) => {
@ -45,6 +46,7 @@ export default function DictComponent({
: "input-disable pointer-events-none cursor-pointer"
}
placeholder="Click to edit your dictionary..."
data-testid="dict-input"
/>
</DictAreaModal>
</div>

View file

@ -11,6 +11,7 @@ export default function Dropdown({
editNode = false,
numberOfOptions = 0,
apiModal = false,
id = "",
}: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
@ -31,15 +32,19 @@ export default function Dropdown({
>
{({ open }) => (
<>
<div className={editNode ? "mt-1" : "relative mt-1"}>
<div className={"relative mt-1"}>
<Listbox.Button
data-test={`${id ?? ""}`}
className={
editNode
? "dropdown-component-outline"
: "dropdown-component-false-outline"
}
>
<span className="dropdown-component-display">
<span
className="dropdown-component-display"
data-testid={`${id ?? ""}-display`}
>
{internalValue}
</span>
<span className={"dropdown-component-arrow"}>
@ -63,7 +68,7 @@ export default function Dropdown({
editNode
? "dropdown-component-true-options nowheel custom-scroll"
: "dropdown-component-false-options nowheel custom-scroll",
apiModal ? "mb-2 w-[250px]" : "absolute"
apiModal ? "mb-2 w-[250px]" : "absolute w-full"
)}
>
{options.map((option, id) => (
@ -86,6 +91,7 @@ export default function Dropdown({
selected ? "font-semibold" : "font-normal",
"block truncate "
)}
data-testid={`${option}-${id ?? ""}-option`}
>
{option}
</span>

View file

@ -3,14 +3,24 @@ import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => {
(
{ name, className, iconColor, stroke, id = "" }: IconComponentProps,
ref
) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
const style = {
strokeWidth: 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
return (
<TargetIcon
strokeWidth={1.5}
className={className}
style={iconColor ? { color: iconColor } : {}}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
);
}

View file

@ -8,7 +8,7 @@ import {
DropdownMenuTrigger,
} from "../../../ui/dropdown-menu";
import { Link, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { alertContext } from "../../../../contexts/alertContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
@ -38,9 +38,13 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
return (
<div className="round-button-div">
<Link to="/">
<button
onClick={() => {
navigate(-1);
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</Link>
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>

View file

@ -6,6 +6,8 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext";
import { StoreContext } from "../../contexts/storeContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
@ -27,12 +29,13 @@ export default function Header(): JSX.Element {
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const { hasStore } = useContext(StoreContext);
const { stars, gradientIndex } = useContext(darkContext);
const navigate = useNavigate();
return (
<div className="header-arrangement">
<div className="header-start-display">
<div className="header-start-display lg:w-[30%]">
<Link to="/">
<span className="ml-4 text-2xl">⛓️</span>
</Link>
@ -45,14 +48,19 @@ export default function Header(): JSX.Element {
<Link to="/">
<Button
className="gap-2"
variant={location.pathname === "/" ? "primary" : "secondary"}
variant={
location.pathname === "/flows" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm"
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="flex-1">{USER_PROJECTS_HEADER}</div>
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button>
</Link>
<Link to="/community">
{/* <Link to="/community">
<Button
className="gap-2"
variant={
@ -63,18 +71,30 @@ export default function Header(): JSX.Element {
<IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div>
</Button>
</Link>
</Link> */}
{hasStore && (
<Link to="/store">
<Button
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
>
<IconComponent name="Store" className="h-4 w-4" />
<div className="flex-1">Store</div>
</Button>
</Link>
)}
</div>
<div className="header-end-division">
<div className="header-end-division lg:w-[30%]">
<div className="header-end-display">
<a
href="https://github.com/logspace-ai/langflow"
target="_blank"
rel="noreferrer"
className="header-github-link"
className="header-github-link gap-2"
>
<FaGithub className="mr-2 h-5 w-5" />
Star
<FaGithub className="h-5 w-5" />
<div className="hidden lg:block">Star</div>
<div className="header-github-display">{stars}</div>
</a>
<a

View file

@ -55,7 +55,13 @@ export default function InputComponent({
onChange={(e) => {
onChange(e.target.value);
}}
onCopy={(e) => {
e.preventDefault();
}}
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "c") {
// Perform any actions you need when Ctrl+C is detected
}
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}

View file

@ -9,7 +9,6 @@ export default function InputFileComponent({
value,
onChange,
disabled,
suffixes,
fileTypes,
onFileChange,
editNode = false,
@ -29,8 +28,9 @@ export default function InputFileComponent({
}, [disabled, onChange]);
function checkFileType(fileName: string): boolean {
for (let index = 0; index < suffixes.length; index++) {
if (fileName.endsWith(suffixes[index])) {
if (fileTypes === undefined) return true;
for (let index = 0; index < fileTypes.length; index++) {
if (fileName.endsWith(fileTypes[index])) {
return true;
}
}
@ -45,7 +45,7 @@ export default function InputFileComponent({
// Create a file input element
const input = document.createElement("input");
input.type = "file";
input.accept = suffixes.join(",");
input.accept = fileTypes?.join(",");
input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection

View file

@ -36,6 +36,8 @@ export default function IntComponent({
event.key !== "c" &&
event.key !== "v" &&
event.key !== "a" &&
event.key !== "ArrowUp" &&
event.key !== "ArrowDown" &&
!/^[-]?\d*$/.test(event.key)
) {
event.preventDefault();

View file

@ -12,8 +12,6 @@ export default function KeypairListComponent({
disabled,
editNode = false,
duplicateKey,
advanced = false,
dataValue,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
@ -55,7 +53,10 @@ export default function KeypairListComponent({
return (
<div key={idx} className="flex w-full gap-2">
<Input
id={"keypair" + index}
data-testid={
editNode ? "editNodekeypair" + index : "keypair" + index
}
id={editNode ? "editNodekeypair" + index : "keypair" + index}
type="text"
value={key.trim()}
className={classNames(
@ -73,7 +74,16 @@ export default function KeypairListComponent({
/>
<Input
id={"keypair" + (index + 100).toString()}
data-testid={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
id={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
type="text"
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
@ -90,7 +100,16 @@ export default function KeypairListComponent({
newInputList.push({ "": "" });
onChange(newInputList);
}}
id={"plusbtn" + index.toString()}
id={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
data-testid={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
>
<IconComponent
name="Plus"
@ -104,7 +123,16 @@ export default function KeypairListComponent({
newInputList.splice(index, 1);
onChange(newInputList);
}}
id={"minusbtn" + index.toString()}
data-testid={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
id={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
>
<IconComponent
name="X"

View file

@ -0,0 +1,31 @@
import Header from "../headerComponent";
import { Separator } from "../ui/separator";
export default function PageLayout({
title,
description,
children,
button,
}: {
title: string;
description: string;
children: React.ReactNode;
button?: React.ReactNode;
}) {
return (
<div className="flex h-screen w-full flex-col">
<Header />
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5 py-8 pb-2">
<div className="flex w-full flex-col">
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
<div className="flex-shrink-0">{button && button}</div>
</div>
<Separator className="my-6 flex" />
{children}
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { useEffect } from "react";
import { TypeModal } from "../../constants/enums";
import { postValidatePrompt } from "../../controllers/API";
import GenericModal from "../../modals/genericModal";
import { PromptAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
@ -23,18 +22,6 @@ export default function PromptAreaComponent({
}
}, [disabled]);
useEffect(() => {
//prevent update from prompt template after group node if prompt is wrongly marked as not dynamic
if (value !== "" && !editNode && !readonly && !nodeClass?.flow) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node);
// need to update reactFlowInstance to re-render the nodes.
}
});
}
}, []);
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
@ -44,15 +31,14 @@ export default function PromptAreaComponent({
value={value}
buttonText="Check & Save"
modalTitle="Edit Prompt"
setValue={(value: string) => {
onChange(value);
}}
setValue={onChange}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
>
<div className="flex w-full items-center">
<span
id={id}
data-testid={id}
className={
editNode
? "input-edit-node input-dialog"
@ -64,6 +50,7 @@ export default function PromptAreaComponent({
</span>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +

View file

@ -0,0 +1,47 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "../../utils/utils";
import { buttonVariants } from "../ui/button";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
icon: React.ReactNode;
}[];
}
export default function SidebarNav({
className,
items,
...props
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"justify-start gap-2"
)}
>
{item.icon}
{item.title}
</Link>
))}
</nav>
);
}

View file

@ -0,0 +1,30 @@
import React, { ReactNode } from "react";
interface ElementStackProps {
children: ReactNode[];
}
const ElementStack: React.FC<ElementStackProps> = ({ children }) => {
return (
<div
className={`grid grid-cols-1`}
style={{ display: "grid", gridAutoFlow: "row" }}
>
{children.map((child, index) => (
<div
key={index}
style={{
gridColumn: 1,
gridRow: 1,
transform: `translateX(${index * 0.1}rem)`,
zIndex: children.length - index,
}}
>
{child}
</div>
))}
</div>
);
};
export default ElementStack;

View file

@ -0,0 +1,12 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { StoreContext } from "../../contexts/storeContext";
export const StoreGuard = ({ children }) => {
const { hasStore } = useContext(StoreContext);
if (!hasStore) {
return <Navigate to="/flows" replace />;
}
return children;
};

View file

@ -0,0 +1,111 @@
import { useContext, useEffect, useRef, useState } from "react";
import { darkContext } from "../../contexts/darkContext";
import { cn } from "../../utils/utils";
import { Badge } from "../ui/badge";
export function TagsSelector({
tags,
disabled = false,
loadingTags,
selectedTags,
setSelectedTags,
}: {
tags: { id: string; name: string }[];
disabled?: boolean;
loadingTags: boolean;
selectedTags: any[];
setSelectedTags: (tags: any[]) => void;
}) {
const updateTags = (tagName: string) => {
const index = selectedTags.indexOf(tagName);
let newArray =
index === -1
? [...selectedTags, tagName]
: selectedTags.filter((_, i) => i !== index);
setSelectedTags(newArray);
};
const { dark } = useContext(darkContext);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, loadingTags]); // Depend on divWidth
return (
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex min-w-min gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
key={idx}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
);
}

View file

@ -20,29 +20,39 @@ export default function TextAreaComponent({
}, [disabled]);
return (
<div className="flex w-full items-center">
<Input
id={id}
<div
className={
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
}
>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
setValue={(value: string) => {
onChange(value);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
setValue={(value: string) => {
onChange(value);
}}
>
>
<div className="flex w-full items-center" data-testid={"div-" + id}>
<Input
id={id}
data-testid={id}
value={value}
disabled={disabled}
className={
editNode
? "input-edit-node pointer-events-none "
: " pointer-events-none"
}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
}}
/>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +
@ -50,8 +60,8 @@ export default function TextAreaComponent({
}
/>
)}
</GenericModal>
</div>
</div>
</GenericModal>
</div>
);
}

View file

@ -14,12 +14,14 @@ const badgeVariants = cva(
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground",
outline: "text-primary/80 hover:bg-muted border-ring/60",
},
size: {
sm: "h-4 text-xs",
md: "h-5 text-sm",
lg: "h-6 text-base",
sq: "h-6 text-sm font-normal rounded-md",
xq: "h-5 text-xs font-normal rounded-md",
},
},
defaultVariants: {

View file

@ -14,15 +14,16 @@ const buttonVariants = cva(
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
primary:
"border bg-background text-secondary-foreground hover:bg-background/80 dark:hover:bg-background/10 hover:shadow-sm",
"border bg-background text-secondary-foreground hover:bg-secondary-foreground/5 dark:hover:bg-background/10 hover:shadow-sm",
secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
xs: "py-1 px-1 rounded-md",
lg: "h-11 px-8 rounded-md",
},
},

View file

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"flex flex-col justify-between rounded-lg border bg-card text-card-foreground shadow-sm transition-all hover:shadow-lg",
"flex flex-col justify-between rounded-lg border bg-muted text-card-foreground shadow-sm transition-all hover:shadow-lg",
className
)}
{...props}

View file

@ -0,0 +1,79 @@
"use client";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Button } from "./button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "./command";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
export function Combobox({
items,
onChange,
}: {
items: { value: string; label: string }[];
onChange: (value: string[]) => void;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<string[]>([]);
React.useEffect(() => {
onChange(value);
}, [value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? items.find((framework) => value.includes(framework.value))?.label
: "Select filter..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search filters..." />
<CommandEmpty>No filters found.</CommandEmpty>
<CommandGroup>
{items.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue((old) => {
if (old.includes(currentValue)) {
return old.filter((item) => item !== currentValue);
}
return [...old, currentValue];
});
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(framework.value)
? "opacity-100"
: "opacity-0"
)}
/>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -0,0 +1,155 @@
"use client";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Dialog, DialogContent } from "./dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
};

View file

@ -18,14 +18,14 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-8 items-center justify-between rounded-md border border-ring/60 px-4 py-2 text-sm text-primary ring-offset-background placeholder:text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDown className="ml-2 h-4 w-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
"z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}

View file

@ -517,16 +517,19 @@ export const ADMIN_HEADER_TITLE = "Admin Page";
export const ADMIN_HEADER_DESCRIPTION =
"Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts.";
export const BASE_URL_API = "/api/v1/";
/**
* URLs excluded from error retries.
* @constant
*
*/
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
"http://localhost:7860/login",
`${BASE_URL_API}validate/code`,
`${BASE_URL_API}custom_component`,
`${BASE_URL_API}validate/prompt`,
`http://localhost:7860/login`,
`${BASE_URL_API}api_key/store`,
];
export const skipNodeUpdate = [
@ -561,10 +564,6 @@ export const CONTROL_NEW_USER = {
is_superuser: false,
};
export const CONTROL_NEW_API_KEY = {
apikeyname: "",
};
export const tabsCode = [];
export function tabsArray(codes: string[], method: number) {
@ -649,8 +648,6 @@ export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
export const FETCH_ERROR_DESCRIPION =
"Check if everything is working properly and try again.";
export const BASE_URL_API = "/api/v1/";
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
export const API_PAGE_PARAGRAPH_1 =
@ -666,3 +663,17 @@ export const LAST_USED_SPAN_1 = "The last time this key was used.";
export const LAST_USED_SPAN_2 =
"Accurate to within the hour from the most recent usage.";
export const LANGFLOW_SUPPORTED_TYPES = new Set([
"str",
"bool",
"float",
"code",
"prompt",
"file",
"int",
"dict",
"NestedDict",
]);
export const priorityFields = new Set(["code", "template"]);

View file

@ -26,6 +26,8 @@ const initialValue: alertContextType = {
pushNotificationList: () => {},
clearNotificationList: () => {},
removeFromNotificationList: () => {},
modalContextOpen: false,
setModalContextOpen: (open: boolean) => {},
};
export const alertContext = createContext<alertContextType>(initialValue);
@ -49,6 +51,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
const [notificationCenter, setNotificationCenter] = useState(false);
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
const [isTweakPage, setIsTweakPage] = useState<boolean>(false);
const [modalContextOpen, setModalContextOpen] = useState<boolean>(false);
const pushNotificationList = (notification: AlertItemType) => {
setNotificationList((old) => {
let newNotificationList = _.cloneDeep(old);
@ -141,6 +144,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
setSuccessData,
successOpen,
setSuccessOpen,
modalContextOpen,
setModalContextOpen,
}}
>
{children}

View file

@ -19,6 +19,9 @@ const initialValue: AuthContextType = {
authenticationErrorCount: 0,
autoLogin: false,
setAutoLogin: () => {},
setApiKey: () => {},
apiKey: null,
storeApiKey: () => {},
};
export const AuthContext = createContext<AuthContextType>(initialValue);
@ -36,6 +39,10 @@ export function AuthProvider({ children }): React.ReactElement {
const [userData, setUserData] = useState<Users | null>(null);
const [autoLogin, setAutoLogin] = useState<boolean>(false);
const { setLoading } = useContext(alertContext);
const [apiKey, setApiKey] = useState<string | null>(
cookies.get("apikey_tkn_lflw")
);
useEffect(() => {
const storedAccessToken = cookies.get("access_tkn_lflw");
if (storedAccessToken) {
@ -43,6 +50,13 @@ export function AuthProvider({ children }): React.ReactElement {
}
}, []);
useEffect(() => {
const apiKey = cookies.get("apikey_tkn_lflw");
if (apiKey) {
setApiKey(apiKey);
}
}, []);
useEffect(() => {
const isLoginPage = location.pathname.includes("login");
@ -74,7 +88,7 @@ export function AuthProvider({ children }): React.ReactElement {
setLoading(false);
}
});
}, []);
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
function getAuthentication() {
const storedRefreshToken = cookies.get("refresh_tkn_lflw");
@ -94,6 +108,7 @@ export function AuthProvider({ children }): React.ReactElement {
function logout() {
cookies.remove("access_tkn_lflw", { path: "/" });
cookies.remove("refresh_tkn_lflw", { path: "/" });
cookies.remove("apikey_tkn_lflw", { path: "/" });
setIsAdmin(false);
setUserData(null);
setAccessToken(null);
@ -101,6 +116,11 @@ export function AuthProvider({ children }): React.ReactElement {
setIsAuthenticated(false);
}
function storeApiKey(apikey: string) {
cookies.set("apikey_tkn_lflw", apikey, { path: "/" });
setApiKey(apikey);
}
return (
// !! to convert string to boolean
<AuthContext.Provider
@ -118,6 +138,9 @@ export function AuthProvider({ children }): React.ReactElement {
authenticationErrorCount: 0,
setAutoLogin,
autoLogin,
setApiKey,
apiKey,
storeApiKey,
}}
>
{children}

View file

@ -8,12 +8,18 @@ import {
useRef,
useState,
} from "react";
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
import {
Edge,
Node,
ReactFlowJsonObject,
XYPosition,
addEdge,
} from "reactflow";
import ShortUniqueId from "short-unique-id";
import { skipNodeUpdate } from "../constants/constants";
import {
deleteFlowFromDatabase,
downloadFlowsFromDatabase,
getVersion,
readFlowsFromDatabase,
saveFlowToDatabase,
updateFlowInDatabase,
@ -28,17 +34,23 @@ import {
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { FlowsContextType, TabsState } from "../types/tabs";
import { FlowsContextType, FlowsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
createFlowComponent,
removeFileNameFromComponents,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds,
updateTemplate,
} from "../utils/reactflowUtils";
import { getRandomDescription, getRandomName } from "../utils/utils";
import {
createRandomKey,
getRandomDescription,
getRandomName,
} from "../utils/utils";
import { alertContext } from "./alertContext";
import { AuthContext } from "./authContext";
import { typesContext } from "./typesContext";
@ -51,7 +63,11 @@ const FlowsContextInitialValue: FlowsContextType = {
isLoading: true,
flows: [],
removeFlow: (id: string) => {},
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
addFlow: async (
newProject: boolean,
flowData?: FlowType,
override?: boolean
) => "",
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {},
@ -65,7 +81,8 @@ const FlowsContextInitialValue: FlowsContextType = {
lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {},
tabsState: {},
setTabsState: (state: TabsState) => {},
setTabsState: (state: FlowsState) => {},
saveCurrentFlow: () => {},
getNodeId: (nodeType: string) => "",
setTweak: (tweak: any) => {},
getTweak: [],
@ -73,29 +90,35 @@ const FlowsContextInitialValue: FlowsContextType = {
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => {},
saveComponent: async (component: NodeDataType, override: boolean) => "",
deleteComponent: (key: string) => {},
version: "",
nodesOnFlow: "",
setNodesOnFlow: (nodes: string) => "",
};
export const FlowsContext = createContext<FlowsContextType>(
FlowsContextInitialValue
);
export function TabsProvider({ children }: { children: ReactNode }) {
export function FlowsProvider({ children }: { children: ReactNode }) {
const { setErrorData, setNoticeData, setSuccessData } =
useContext(alertContext);
const { getAuthentication, isAuthenticated } = useContext(AuthContext);
const [tabId, setTabId] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [nodesOnFlow, setNodesOnFlow] = useState("");
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uid());
const { templates, reactFlowInstance } = useContext(typesContext);
const { reactFlowInstance, setData, data } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState<{
nodes: any;
edges: any;
} | null>(null);
const [tabsState, setTabsState] = useState<TabsState>({});
const [tabsState, setTabsState] = useState<FlowsState>({});
const [getTweak, setTweak] = useState<tweakType>([]);
useEffect(() => {
@ -113,9 +136,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function refreshFlows() {
setIsLoading(true);
getTabsDataFromDB().then((DbData) => {
if (DbData && Object.keys(templates).length > 0) {
if (DbData) {
try {
processDBData(DbData);
processFlows(DbData, false);
updateStateWithDbData(DbData);
setIsLoading(false);
} catch (e) {}
@ -127,25 +150,42 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
if (getAuthentication() === true) {
// get data from db
//get tabs locally saved
// let tabsData = getLocalStorageTabsData();
refreshFlows();
}
}, [templates, getAuthentication()]);
}, [getAuthentication(), tabId]);
function getTabsDataFromDB() {
//get tabs from db
return readFlowsFromDatabase();
}
function processDBData(DbData: FlowType[]) {
function processFlows(DbData: FlowType[], skipUpdate = true) {
let savedComponents: { [key: string]: APIClassType } = {};
DbData.forEach((flow: FlowType) => {
try {
if (!flow.data) {
return;
}
processDataFromFlow(flow, false);
} catch (e) {}
if (flow.data && flow.is_component) {
(flow.data.nodes[0].data as NodeDataType).node!.display_name =
flow.name;
savedComponents[
createRandomKey(
(flow.data.nodes[0].data as NodeDataType).type,
uid()
)
] = _.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return;
}
if (!skipUpdate) processDataFromFlow(flow, false);
} catch (e) {
console.log(e);
}
});
setData((prev) => {
let newData = cloneDeep(prev);
newData["saved_components"] = cloneDeep(savedComponents);
return newData;
});
}
@ -170,28 +210,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
node.data.node!.documentation = template["documentation"];
}
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
return;
}
if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template);
updateNodeBaseClasses(node, template);
//update baseclasses in edges
updateNodeEdges(flow, node, template);
updateNodeDescription(node, template);
updateNodeTemplate(node, template);
updateNodeDocumentation(node, template);
}
});
}
function updateNodeBaseClasses(node: NodeType, template: APIClassType) {
node.data.node!.base_classes = template["base_classes"];
}
@ -244,9 +262,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
flowName: string,
flowDescription?: string
) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify({ ...flow, name: flowName, description: flowDescription })
JSON.stringify({
...clonedFlow,
name: flowName,
description: flowDescription,
})
)}`;
// create a link element and set its properties
@ -287,46 +311,70 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* If the file type is application/json, the file is read and parsed into a JSON object.
* The resulting JSON object is passed to the addFlow function.
*/
async function uploadFlow(
newProject: boolean,
file?: File
): Promise<String | undefined> {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(newProject, flow);
});
}
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
id = await addFlow(newProject, flow);
} else {
// create a file input
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
// add a change event listener to the file input
id = await new Promise((resolve) => {
async function uploadFlow({
newProject,
file,
isComponent = false,
position = { x: 10, y: 10 },
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
position?: XYPosition;
}): Promise<String | never> {
return new Promise(async (resolve, reject) => {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (
newProject &&
((!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent))
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(newProject, flow, undefined, position);
});
resolve("");
} else {
id = await addFlow(newProject, fileData, undefined, position);
resolve(id);
}
}
} else {
// create a file input
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
// add a change event listener to the file input
input.onchange = async (e: Event) => {
if (
(e.target as HTMLInputElement).files![0].type === "application/json"
) {
const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text();
let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(newProject, flow);
resolve(flowId);
let fileData: FlowType = await JSON.parse(text);
if (
(!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent)
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
id = await addFlow(newProject, fileData);
resolve(id);
}
}
};
// trigger the file input click event to open the file dialog
input.click();
});
}
return id;
}
});
}
function uploadFlows() {
@ -357,12 +405,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove.
*/
function removeFlow(id: string) {
async function removeFlow(id: string) {
const index = flows.findIndex((flow) => flow.id === id);
if (index >= 0) {
deleteFlowFromDatabase(id).then(() => {
setFlows(flows.filter((flow) => flow.id !== id));
});
await deleteFlowFromDatabase(id);
//removes component from data if there is any
setFlows(flows.filter((flow) => flow.id !== id));
processFlows(flows.filter((flow) => flow.id !== id));
}
}
/**
@ -389,7 +438,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const insidePosition = position.paneX
? { x: position.paneX + position.x, y: position.paneY! + position.y }
: reactFlowInstance!.project({ x: position.x, y: position.y });
: reactFlowInstance!.screenToFlowPosition({
x: position.x,
y: position.y,
});
selectionInstance.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
@ -470,7 +522,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const addFlow = async (
newProject: Boolean,
flow?: FlowType
flow?: FlowType,
override?: boolean,
position?: XYPosition
): Promise<String | undefined> => {
if (newProject) {
let flowData = flow
@ -478,12 +532,21 @@ export function TabsProvider({ children }: { children: ReactNode }) {
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
if (override) {
deleteComponent(flow!.name);
const newFlow = createNewFlow(flowData, flow!);
const { id } = await saveFlowToDatabase(newFlow);
newFlow.id = id;
addFlowToLocalState(newFlow);
return;
}
const newFlow = createNewFlow(flowData, flow!);
const flowName = addVersionToDuplicates(newFlow, flows);
newFlow.name = flowName;
const newName = addVersionToDuplicates(newFlow, flows);
newFlow.name = newName;
try {
const { id } = await saveFlowToDatabase(newFlow);
// Change the id to the new id.
@ -501,7 +564,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} else {
paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
{ x: 10, y: 10 }
position ?? { x: 10, y: 10 }
);
}
};
@ -522,46 +585,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
};
const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className =
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = targetHandleObject.type === "Text";
});
};
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
return;
}
if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => {
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
if (edge.source === node.id) {
let newSourceHandle = sourceHandleObject;
newSourceHandle.baseClasses.concat(template["base_classes"]);
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
});
node.data.node.description = template["description"];
node.data.node.template = updateTemplate(
template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType
if (edges)
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
}
});
edge.className =
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = targetHandleObject.type === "Text";
});
};
const createNewFlow = (
@ -572,12 +606,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
name: flow?.name ?? getRandomName(),
data: flowData,
id: "",
is_component: flow?.is_component ?? false,
});
const addFlowToLocalState = (newFlow: FlowType) => {
let newFlows: FlowType[] = [];
setFlows((prevState) => {
newFlows = newFlows.concat(prevState);
newFlows.push(newFlow);
return [...prevState, newFlow];
});
processFlows(newFlows);
};
/**
@ -593,10 +632,20 @@ export function TabsProvider({ children }: { children: ReactNode }) {
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
}
newFlow = {
...newFlow,
};
return newFlows;
});
}
function saveCurrentFlow() {
const currentFlow = flows.find((flow) => flow.id === tabId);
if (currentFlow && reactFlowInstance && currentFlow.data) {
updateFlow({ ...currentFlow, data: reactFlowInstance?.toObject()! });
}
}
async function saveFlow(newFlow: FlowType, silent?: boolean) {
try {
// updates flow in db
@ -635,16 +684,41 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
}
function saveComponent(component: NodeDataType, override: boolean) {
component.node!.official = false;
return addFlow(true, createFlowComponent(component, version), override);
}
function deleteComponent(key: string) {
let componentFlow = flows.find(
(componentFlow) =>
componentFlow.is_component && componentFlow.name === key
);
if (componentFlow) {
removeFlow(componentFlow.id);
}
}
const [isBuilt, setIsBuilt] = useState(false);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<FlowsContext.Provider
value={{
version,
saveFlow,
isBuilt,
setIsBuilt,
lastCopiedSelection,
setLastCopiedSelection,
saveCurrentFlow,
hardReset,
tabId,
setTabId,
@ -664,6 +738,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
getTweak,
setTweak,
isLoading,
saveComponent,
deleteComponent,
nodesOnFlow,
setNodesOnFlow,
}}
>
{children}

View file

@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext";
import { TabsProvider } from "./flowsContext";
import { FlowsProvider } from "./flowsContext";
import { LocationProvider } from "./locationContext";
import { StoreProvider } from "./storeContext";
import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext";
@ -26,9 +28,11 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<LocationProvider>
<ApiInterceptor />
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
<FlowsProvider>
<UndoRedoProvider>
<StoreProvider>{children}</StoreProvider>
</UndoRedoProvider>
</FlowsProvider>
</SSEProvider>
</LocationProvider>
</TypesProvider>

View file

@ -0,0 +1,74 @@
import { createContext, useContext, useEffect, useState } from "react";
import { checkHasApiKey, checkHasStore } from "../controllers/API";
import { storeContextType } from "../types/contexts/store";
import { AuthContext } from "./authContext";
//store context to share user components and update them
const initialValue = {
hasStore: true,
setHasStore: () => {},
validApiKey: false,
setValidApiKey: () => {},
hasApiKey: false,
setHasApiKey: () => {},
loadingApiKey: true,
};
export const StoreContext = createContext<storeContextType>(initialValue);
export function StoreProvider({ children }) {
const [hasStore, setHasStore] = useState(false);
const [loadingApiKey, setLoadingApiKey] = useState(true);
const [hasApiKey, setHasApiKey] = useState(true);
const [validApiKey, setValidApiKey] = useState(false);
const [storeChecked, setStoreChecked] = useState(false);
const { apiKey } = useContext(AuthContext);
useEffect(() => {
const fetchStoreData = async () => {
try {
if (storeChecked) return;
const res = await checkHasStore();
setHasStore(res?.enabled ?? false);
setStoreChecked(true);
} catch (e) {
console.log(e);
}
};
fetchStoreData();
}, []);
const fetchApiData = async () => {
setLoadingApiKey(true);
try {
const res = await checkHasApiKey();
setHasApiKey(res?.has_api_key ?? false);
setValidApiKey(res?.is_valid ?? false);
setLoadingApiKey(false);
} catch (e) {
setLoadingApiKey(false);
console.log(e);
}
};
useEffect(() => {
fetchApiData();
}, [storeChecked, apiKey]);
return (
<StoreContext.Provider
value={{
hasStore,
setHasStore,
hasApiKey,
setHasApiKey,
validApiKey,
setValidApiKey,
loadingApiKey,
}}
>
{children}
</StoreContext.Provider>
);
}

View file

@ -1,3 +1,4 @@
import _ from "lodash";
import {
createContext,
ReactNode,
@ -5,7 +6,7 @@ import {
useEffect,
useState,
} from "react";
import { Edge, Node, ReactFlowInstance } from "reactflow";
import { ReactFlowInstance } from "reactflow";
import { getAll, getHealth } from "../controllers/API";
import { APIKindType } from "../types/api";
import { typesContextType } from "../types/typesContext";
@ -59,11 +60,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Make sure to only update the state if the component is still mounted.
if (isMounted && result?.status === 200) {
setLoading(false);
setData(result.data);
let { data } = _.cloneDeep(result);
setData((old) => ({ ...old, ...data }));
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c];
Object.keys(data).reduce((acc, curr) => {
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
//prevent wrong overwriting of the component template by a group of the same type
if (!data[curr][c].flow) acc[c] = data[curr][c];
});
return acc;
}, {})
@ -71,13 +74,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
// Reverse the keys so the tool world does not overlap
Object.keys(result.data)
Object.keys(data)
.reverse()
.reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => {
data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
@ -87,6 +90,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}
} catch (error) {
console.error("An error has occurred while fetching types.");
console.log(error);
await getHealth().catch((e) => {
setFetchError(true);
});
@ -94,31 +98,25 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}
function deleteNode(idx: string | Array<string>) {
reactFlowInstance!.setNodes(
reactFlowInstance!
.getNodes()
.filter((node: Node) =>
typeof idx === "string" ? node.id !== idx : !idx.includes(node.id)
)
);
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge) =>
typeof idx === "string"
? edge.source !== idx && edge.target !== idx
: !idx.includes(edge.source) && !idx.includes(edge.target)
)
);
if (reactFlowInstance === null) return;
const edges = reactFlowInstance!
.getEdges()
.filter((edge) =>
typeof idx === "string"
? edge.source == idx || edge.target == idx
: idx.includes(edge.source) || idx.includes(edge.target)
);
reactFlowInstance!.deleteElements({
nodes:
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
edges,
});
}
function deleteEdge(idx: string | Array<string>) {
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge: Edge) =>
typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id)
)
);
reactFlowInstance!.deleteElements({
edges:
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
});
}
return (

View file

@ -52,15 +52,23 @@ export function UndoRedoProvider({ children }) {
const takeSnapshot = useCallback(() => {
// push the current graph to the past state
setPast((old) => {
let newPast = cloneDeep(old);
newPast[tabIndex] = old[tabIndex].slice(
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
old[tabIndex].length
let newPast = cloneDeep(past);
let newState = {
nodes: cloneDeep(getNodes()),
edges: cloneDeep(getEdges()),
};
if (
past[tabIndex] &&
JSON.stringify(past[tabIndex][past[tabIndex].length - 1]) !==
JSON.stringify(newState)
) {
newPast[tabIndex] = past[tabIndex].slice(
past[tabIndex].length - defaultOptions.maxHistorySize + 1,
past[tabIndex].length
);
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
return newPast;
});
newPast[tabIndex].push(newState);
}
setPast(newPast);
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
setFuture((old) => {

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
APIObjectType,
Component,
LoginType,
Users,
changeUser,
@ -12,6 +13,7 @@ import {
} from "../../types/api/index";
import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import {
APIClassType,
BuildStatusTypeAPI,
@ -112,12 +114,14 @@ export async function saveFlowToDatabase(newFlow: {
data: ReactFlowJsonObject | null;
description: string;
style?: FlowStyleType;
is_component?: boolean;
}): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}flows/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
});
if (response.status !== 201) {
@ -353,7 +357,21 @@ export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component`, { code });
let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
code,
template,
});
}
export async function postCustomComponentUpdate(
code: string,
field: string
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
field,
});
}
export async function onLogin(user: LoginType) {
@ -522,3 +540,323 @@ export async function deleteApiKey(api_key: string) {
throw error;
}
}
export async function addApiKeyStore(key: string) {
try {
const res = await api.post(`${BASE_URL_API}api_key/store`, {
api_key: key,
});
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
/**
* Saves a new flow to the database.
*
* @param {FlowType} newFlow - The flow data to save.
* @returns {Promise<any>} The saved flow data.
* @throws Will throw an error if saving fails.
*/
export async function saveFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
status: publicFlow ? "Public" : "Private",
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches the flows from the store.
* @returns {Promise<>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function getFlowsStore(): Promise<AxiosResponse<FlowType[]>> {
return await api.get(`${BASE_URL_API}store/`);
}
export async function getStoreComponents({
component_id = null,
page = 1,
limit = 9999999,
is_component = null,
sort = "-count(liked_by)",
tags = [] || null,
liked = null,
isPrivate = null,
search = null,
filterByUser = null,
fields = null,
}: {
component_id?: string | null;
page?: number;
limit?: number;
is_component?: boolean | null;
sort?: string;
tags?: string[] | null;
liked?: boolean | null;
isPrivate?: boolean | null;
search?: string | null;
filterByUser?: boolean | null;
fields?: Array<string> | null;
}): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (component_id !== undefined && component_id !== null) {
queryParams.push(`component_id=${component_id}`);
}
if (search !== undefined && search !== null) {
queryParams.push(`search=${search}`);
}
if (isPrivate !== undefined && isPrivate !== null) {
queryParams.push(`private=${isPrivate}`);
}
if (tags !== undefined && tags !== null && tags.length > 0) {
queryParams.push(`tags=${tags.join(encodeURIComponent(","))}`);
}
if (fields !== undefined && fields !== null && fields.length > 0) {
queryParams.push(`fields=${fields.join(encodeURIComponent(","))}`);
}
if (sort !== undefined && sort !== null) {
queryParams.push(`sort=${sort}`);
} else {
queryParams.push(`sort=-count(liked_by)`); // default sort
}
if (liked !== undefined && liked !== null) {
queryParams.push(`liked=${liked}`);
}
if (filterByUser !== undefined && filterByUser !== null) {
queryParams.push(`filter_by_user=${filterByUser}`);
}
if (page !== undefined) {
queryParams.push(`page=${page ?? 1}`);
}
if (limit !== undefined) {
queryParams.push(`limit=${limit ?? 9999999}`);
}
if (is_component !== null && is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function postStoreComponents(component: Component) {
try {
const res = await api.post(`${BASE_URL_API}store/components/`, component);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function searchComponent(
query: string | null,
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (query !== undefined && query !== null) {
queryParams.push(`search=${query}`);
}
if (page !== undefined && page !== null) {
queryParams.push(`page=${page}`);
}
if (limit !== undefined && limit !== null) {
queryParams.push(`limit=${limit}`);
}
if (status !== undefined && status !== null) {
queryParams.push(`status=${status}`);
}
if (tags !== undefined && tags !== null) {
queryParams.push(`tags=${tags}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasApiKey() {
try {
const res = await api.get(`${BASE_URL_API}store/check/api_key`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasStore() {
try {
const res = await api.get(`${BASE_URL_API}store/check/`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getCountComponents(is_component?: boolean | null) {
try {
let url = `${BASE_URL_API}store/components/count`;
const queryParams: any = [];
if (is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getStoreTags() {
try {
const res = await api.get(`${BASE_URL_API}store/tags`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export const postLikeComponent = (componentId: string) => {
return api.post(`${BASE_URL_API}store/users/likes/${componentId}`);
};
/**
* Updates an existing flow in the Store.
*
* @param {FlowType} updatedFlow - The updated flow data.
* @returns {Promise<any>} The updated flow data.
* @throws Will throw an error if the update fails.
*/
export async function updateFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false,
id: string
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}

View file

@ -1,7 +1,8 @@
import { InfinityIcon } from "lucide-react";
import { Code } from "lucide-react";
import { forwardRef } from "react";
import ForwardedIconComponent from "../../components/genericIconComponent";
export const GradientSparkles = forwardRef<
export const GradientInfinity = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
@ -15,7 +16,63 @@ export const GradientSparkles = forwardRef<
</linearGradient>
</defs>
</svg>
<InfinityIcon stroke="url(#grad1)" ref={ref} {...props} />
<Code stroke="url(#grad1)" ref={ref} {...props} />
</>
);
});
export const GradientSave = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent
name="Save"
stroke="url(#grad2)"
ref={ref}
{...props}
/>
</>
);
});
export const GradientGroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
</>
);
};
export const GradientUngroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad4" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Ungroup" stroke="url(#grad4)" {...props} />
</>
);
};

View file

@ -13,7 +13,10 @@ import {
// import "ace-builds/webpack-resolver";
import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import {
EXPORT_CODE_DIALOG,
LANGFLOW_SUPPORTED_TYPES,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api";
@ -99,15 +102,9 @@ const ApiModal = forwardRef(
(templateField) =>
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show &&
(node.data.node.template[templateField].type === "str" ||
node.data.node.template[templateField].type === "bool" ||
node.data.node.template[templateField].type === "float" ||
node.data.node.template[templateField].type === "code" ||
node.data.node.template[templateField].type === "prompt" ||
node.data.node.template[templateField].type === "file" ||
node.data.node.template[templateField].type === "int" ||
node.data.node.template[templateField].type === "dict" ||
node.data.node.template[templateField].type === "NestedDict")
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type
)
)
.map((n, i) => {
arrNodesWithValues.push(node["id"]);
@ -146,9 +143,9 @@ const ApiModal = forwardRef(
);
if (existingTweak) {
existingTweak[tw][template["name"]] = changes as string;
existingTweak[tw][template["name"]!] = changes as string;
if (existingTweak[tw][template["name"]] == template.value) {
if (existingTweak[tw][template["name"]!] == template.value) {
tweak.current.forEach((element) => {
if (element[tw] && Object.keys(element[tw])?.length === 0) {
tweak.current = tweak.current.filter((obj) => {
@ -161,7 +158,7 @@ const ApiModal = forwardRef(
} else {
const newTweak = {
[tw]: {
[template["name"]]: changes,
[template["name"]!]: changes,
},
} as uniqueTweakType;
tweak.current.push(newTweak);

View file

@ -1,30 +1,65 @@
import { useState } from "react";
import React, { useEffect, useState } from "react";
import ShadTooltip from "../../components/ShadTooltipComponent";
import { Button } from "../../components/ui/button";
import { ConfirmationModalType } from "../../types/components";
import { ConfirmationModalType, ContentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
export default function ConfirmationModal({
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>;
};
const Trigger: React.FC<ContentProps> = ({
children,
tolltipContent,
side,
}) => {
return tolltipContent ? (
<ShadTooltip side={side} content={tolltipContent}>
<div className="h-full w-full">{children}</div>
</ShadTooltip>
) : (
<div className="h-full w-full">{children}</div>
);
};
function ConfirmationModal({
title,
asChild,
titleHeader,
modalContent,
modalContentTitle,
cancelText,
confirmationText,
children,
destructive = false,
icon,
data,
index,
onConfirm,
size,
open,
onClose,
onCancel,
}: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false);
useEffect(() => {
if (open) setModalOpen(open);
}, [open]);
useEffect(() => {
if (onClose) onClose!(modalOpen);
}, [modalOpen]);
const triggerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Trigger
);
const ContentChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Content
);
const [open, setOpen] = useState(false);
return (
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger asChild={asChild}>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span>
<Icon
name="icon"
@ -33,20 +68,21 @@ export default function ConfirmationModal({
/>
</BaseModal.Header>
<BaseModal.Content>
{modalContentTitle != "" && (
{modalContentTitle && modalContentTitle != "" && (
<>
<strong>{modalContentTitle}</strong>
<br></br>
</>
)}
<span>{modalContent}</span>
{ContentChild}
</BaseModal.Content>
<BaseModal.Footer>
<Button
className="ml-3"
variant={destructive ? "destructive" : "default"}
onClick={() => {
setOpen(false);
setModalOpen(false);
onConfirm(index, data);
}}
>
@ -54,9 +90,11 @@ export default function ConfirmationModal({
</Button>
<Button
className=""
variant="outline"
onClick={() => {
setOpen(false);
if (onCancel) onCancel();
setModalOpen(false);
}}
>
{cancelText}
@ -65,3 +103,7 @@ export default function ConfirmationModal({
</BaseModal>
);
}
ConfirmationModal.Content = Content;
ConfirmationModal.Trigger = Trigger;
export default ConfirmationModal;

View file

@ -0,0 +1,62 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { Trash2 } from "lucide-react";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
export default function DeleteConfirmationModal({
children,
onConfirm,
description,
}: {
children: JSX.Element;
onConfirm: () => void;
description?: string;
}) {
return (
<Dialog>
<DialogTrigger tabIndex={-1}>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex items-center">
<span className="pr-2">Delete</span>
<Trash2
className="h-6 w-6 pl-1 text-foreground"
strokeWidth={1.5}
/>
</div>
</DialogTitle>
</DialogHeader>
<span>
Are you sure you want to delete this {description ?? "component"}?
<br></br>
This action cannot be undone.
</span>
<DialogFooter>
<DialogClose>
<Button className="mr-3" variant="outline">
Cancel
</Button>
<Button
type="submit"
variant="destructive"
onClick={() => {
onConfirm();
}}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -1,12 +1,5 @@
import { cloneDeep } from "lodash";
import {
ReactNode,
forwardRef,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { forwardRef, useContext, useEffect, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
@ -32,7 +25,11 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { limitScrollFieldsModal } from "../../constants/constants";
import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow";
@ -41,6 +38,7 @@ import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
@ -49,70 +47,68 @@ const EditNodeModal = forwardRef(
(
{
data,
setData,
nodeLength,
children,
open,
onClose,
setOpen,
}: {
data: NodeDataType;
setData: (data: NodeDataType) => void;
nodeLength: number;
children: ReactNode;
open?: boolean;
onClose?: (close: boolean) => void;
open: boolean;
setOpen: (open: boolean) => void;
},
ref
) => {
const [modalOpen, setModalOpen] = useState(open ?? false);
const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data);
const [myData, setMyData] = useState(data);
const { setTabsState, tabId } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
let disabled =
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === data.id) ?? false;
const { setModalContextOpen } = useContext(alertContext);
function changeAdvanced(n) {
myData.current.node!.template[n].advanced =
!myData.current.node!.template[n].advanced;
setAdv(!adv);
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[n].advanced =
!newData.node!.template[n].advanced;
return newData;
});
}
const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue;
setDataValue(newValue);
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
updateNodeInternals(data.id);
};
useEffect(() => {
if (modalOpen) {
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
if (open) {
setMyData(data); // reset data to what it is on node when opening modal
}
}, [modalOpen]);
setModalContextOpen(open);
}, [open]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [adv, setAdv] = useState<boolean | null>(null);
const [dataValue, setDataValue] = useState(data);
return (
<BaseModal
key={data.id}
size="large-h-full"
open={modalOpen}
setOpen={setModalOpen}
open={open}
setOpen={setOpen}
onChangeOpenModal={(open) => {
myData.current = data;
setMyData(data);
}}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={myData.current.node?.description!}>
<span className="pr-2">{myData.current.type}</span>
<Badge variant="secondary">ID: {myData.current.id}</Badge>
<BaseModal.Trigger>
<></>
</BaseModal.Trigger>
<BaseModal.Header description={myData.node?.description!}>
<span className="pr-2">{myData.type}</span>
<Badge variant="secondary">ID: {myData.id}</Badge>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex pb-2">
@ -129,7 +125,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: "overflow-hidden"
: ""
)}
>
{nodeLength > 0 && (
@ -145,387 +141,383 @@ const EditNodeModal = forwardRef(
</TableRow>
</TableHeader>
<TableBody className="p-0">
{Object.keys(myData.current.node!.template)
{Object.keys(myData.node!.template)
.filter(
(templateParam) =>
templateParam.charAt(0) !== "_" &&
myData.current.node?.template[templateParam].show &&
(myData.current.node.template[templateParam]
.type === "str" ||
myData.current.node.template[templateParam]
.type === "bool" ||
myData.current.node.template[templateParam]
.type === "float" ||
myData.current.node.template[templateParam]
.type === "code" ||
myData.current.node.template[templateParam]
.type === "prompt" ||
myData.current.node.template[templateParam]
.type === "file" ||
myData.current.node.template[templateParam]
.type === "int" ||
myData.current.node.template[templateParam]
.type === "dict" ||
myData.current.node.template[templateParam]
.type === "NestedDict")
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node.template[templateParam].type
)
)
.map((templateParam, index) => (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
content={
myData.current.node?.template[templateParam]
.proxy
? myData.current.node?.template[
templateParam
].proxy?.id
: null
}
>
<span>
{myData.current.node?.template[templateParam]
.display_name
? myData.current.node.template[
templateParam
].display_name
: myData.current.node?.template[
templateParam
].name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam]
.type === "str" &&
!myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
{myData.current.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.current.node.template[
templateParam
].value ||
myData.current.node.template[
templateParam
].value === ""
? [""]
: myData.current.node.template[
templateParam
].value
.map((templateParam, index) => {
let id = {
inputTypes:
myData.node!.template[templateParam].input_types,
type: myData.node!.template[templateParam].type,
id: myData.id,
fieldName: templateParam,
};
let disabled =
reactFlowInstance?.getEdges().some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(
myData.node!.template[templateParam].proxy
? {
...id,
proxy:
myData.node?.template[templateParam]
.proxy,
}
onChange={(value: string[]) => {
handleOnNewValue(value, templateParam);
: id
)
) ?? false;
return (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
content={
myData.node?.template[templateParam].proxy
? myData.node?.template[templateParam]
.proxy?.id
: null
}
>
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type ===
"str" &&
!myData.node.template[templateParam].options ? (
<div className="mx-auto">
{myData.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.node.template[templateParam]
.value ||
myData.node.template[templateParam]
.value === ""
? [""]
: myData.node.template[
templateParam
].value
}
onChange={(value: string[]) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
) : myData.node.template[templateParam]
.multiline ? (
<TextAreaComponent
id={"textarea-edit-" + index}
data-testid={"textarea-edit-" + index}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(
value: string | string[]
) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
) : (
<InputComponent
id={"input-" + index}
editNode={true}
disabled={disabled}
password={
myData.node.template[templateParam]
.password ?? false
}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
)}
</div>
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[
templateParam
]?.value?.toString() === "{}"
? {
yourkey: "value",
}
: myData.node!.template[templateParam]
.value
}
onChange={(newValue) => {
myData.node!.template[
templateParam
].value = newValue;
handleOnNewValue(
newValue,
templateParam
);
}}
id="editnode-div-dict-input"
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.node!.template[templateParam].value
?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value?.length === 0 ||
!myData.node!.template[templateParam]
.value
? [{ "": "" }]
: convertObjToArray(
myData.node!.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
);
handleOnNewValue(
valueToNumbers,
templateParam
);
}}
/>
) : myData.current.node.template[
templateParam
].multiline ? (
<TextAreaComponent
id={"textarea-edit-" + index}
</div>
) : myData.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
enabled={
myData.node.template[templateParam]
.value
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
);
}}
size="small"
/>
) : (
<InputComponent
id={"input-" + index}
editNode={true}
</div>
) : myData.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
password={
myData.current.node.template[
templateParam
].password ?? false
}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
)}
</div>
) : myData.current.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value.toString() === "{}"
? {
yourkey: "value",
}
: myData.current.node!.template[
templateParam
].value
}
onChange={(newValue) => {
myData.current.node!.template[
templateParam
].value = newValue;
handleOnNewValue(newValue, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.current.node!.template[templateParam]
.value?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
dataValue={dataValue}
advanced={adv}
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value?.length === 0 ||
!myData.current.node!.template[
templateParam
].value
? [{ "": "" }]
: convertObjToArray(
myData.current.node!.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.current.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
);
handleOnNewValue(
valueToNumbers,
templateParam
);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
</div>
) : myData.node?.template[templateParam]
.type === "str" &&
myData.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.node.template[templateParam]
.options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node.template[templateParam]
.value ?? "Choose an option"
}
id={"dropdown-edit-" + index}
></Dropdown>
</div>
) : myData.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
id={"int-input-" + index}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node.template[templateParam]
.fileTypes
}
onFileChange={(filePath: string) => {
data.node!.template[
templateParam
].file_path = filePath;
}}
></InputFileComponent>
</div>
) : myData.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.node?.flow ? true : false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.node}
setNodeClass={(nodeClass) => {
myData.node = nodeClass;
}}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"prompt-area-edit" + index}
data-testid={
"modal-prompt-input-" + index
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node.template[templateParam]
.dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
.dynamic ?? false
}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"code-area-edit" + index}
/>
</div>
) : myData.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled}
enabled={
myData.current.node.template[
templateParam
].value
id={
"show" +
myData.node?.template[templateParam].name
}
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
);
enabled={
!myData.node?.template[templateParam]
.advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "str" &&
myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.current.node.template[
templateParam
].options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.current.node.template[
templateParam
].value ?? "Choose an option"
}
></Dropdown>
</div>
) : myData.current.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
id={"int-input-" + index}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.current.node.template[
templateParam
].fileTypes
}
suffixes={
myData.current.node.template[
templateParam
].suffixes
}
onFileChange={(filePath: string) => {
data.node!.template[
templateParam
].file_path = filePath;
}}
></InputFileComponent>
</div>
) : myData.current.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.current.node?.flow ? true : false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.current.node}
setNodeClass={(nodeClass) => {
myData.current.node = nodeClass;
}}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"prompt-area-edit" + index}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
.dynamic ?? false
}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"code-area-edit" + index}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
id={
"show" +
myData.current.node?.template[templateParam]
.name
}
enabled={
!myData.current.node?.template[
templateParam
].advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
/>
</div>
</TableCell>
</TableRow>
))}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
@ -539,8 +531,7 @@ const EditNodeModal = forwardRef(
id={"saveChangesBtn"}
className="mt-3"
onClick={() => {
const newData = cloneDeep(myData.current);
myData.current = newData;
data.node = myData.node;
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
@ -551,7 +542,7 @@ const EditNodeModal = forwardRef(
},
};
});
setModalOpen(false);
setOpen(false);
}}
type="submit"
>

View file

@ -3,14 +3,9 @@ import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CONTROL_NEW_API_KEY } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { createApiKey } from "../../controllers/API";
import {
ApiKeyInputType,
ApiKeyType,
inputHandlerEventType,
} from "../../types/components";
import { ApiKeyType } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
@ -27,19 +22,11 @@ export default function SecretKeyModal({
const [open, setOpen] = useState(false);
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
const [apiKeyValue, setApiKeyValue] = useState("");
const [inputState, setInputState] =
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
const [renderKey, setRenderKey] = useState(false);
const [textCopied, setTextCopied] = useState(true);
const { setSuccessData } = useContext(alertContext);
const inputRef = useRef<HTMLInputElement | null>(null);
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
useEffect(() => {
if (open) {
setRenderKey(false);
@ -101,14 +88,7 @@ export default function SecretKeyModal({
</span>
<div className="flex pt-3">
<div className="w-full">
<Input
ref={inputRef}
onChange={(event) => {
setApiKeyValue(event.target.value);
}}
readOnly={true}
value={apiKeyValue}
/>
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
</div>
<div>
@ -153,7 +133,6 @@ export default function SecretKeyModal({
<Form.Control asChild>
<input
onChange={({ target: { value } }) => {
handleInput({ target: { name: "apikeyname", value } });
setApiKeyName(value);
}}
value={apiKeyName}

View file

@ -0,0 +1,124 @@
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { StoreContext } from "../../contexts/storeContext";
import { addApiKeyStore } from "../../controllers/API";
import { StoreApiKeyType } from "../../types/components";
import BaseModal from "../baseModal";
export default function StoreApiKeyModal({
children,
disabled = false,
}: StoreApiKeyType) {
if (disabled) return <>{children}</>;
const [open, setOpen] = useState(false);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { storeApiKey } = useContext(AuthContext);
const { hasApiKey, validApiKey } = useContext(StoreContext);
const [apiKeyValue, setApiKeyValue] = useState("");
const handleSaveKey = () => {
if (apiKeyValue) {
addApiKeyStore(apiKeyValue).then(
() => {
setSuccessData({
title: "Success! Your API Key has been saved.",
});
storeApiKey(apiKeyValue);
setOpen(false);
},
(error) => {
setErrorData({
title: "There was an error saving the API Key, please try again.",
list: [error["response"]["data"]["detail"]],
});
}
);
}
};
return (
<BaseModal size="small-h-full" open={open && !disabled} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
(hasApiKey && !validApiKey
? "Your API key is not valid. "
: !hasApiKey
? "You don't have an API key. "
: "") + "Insert your Langflow API key."
}
>
<span className="pr-2">API Key</span>
<IconComponent
name="Key"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
}}
>
<div className="grid gap-5">
<Form.Field name="apikey">
<div className="flex items-center justify-between gap-2">
<Form.Control asChild>
<Input
//fake api key
value={apiKeyValue}
type="password"
onChange={({ target: { value } }) => {
setApiKeyValue(value);
}}
placeholder="Insert your API Key"
/>
</Form.Control>
</div>
</Form.Field>
</div>
<div className="flex items-end justify-between">
<span className="pr-1 text-xs text-muted-foreground">
Don’t have an API key? Sign up at{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
<div className="">
<Button
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Form.Submit asChild>
<Button
className="mt-8"
onClick={() => {
handleSaveKey();
}}
>
Save
</Button>
</Form.Submit>
</div>
</div>
</Form.Root>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -67,7 +67,8 @@ interface BaseModalProps {
| "large"
| "large-h-full"
| "small-h-full"
| "medium-h-full";
| "medium-h-full"
| "smaller-h-full";
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
@ -98,11 +99,15 @@ function BaseModal({
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-[10vh]";
height = " ";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[27vh]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
@ -145,9 +150,7 @@ function BaseModal({
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div className={`mt-2 flex flex-col ${height!} w-full `}>
{ContentChild}
</div>
<div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}

View file

@ -88,8 +88,7 @@ export default function CodeAreaModal({
.then((apiReturn) => {
const { data } = apiReturn;
if (data) {
setNodeClass(data);
setValue(code);
setNodeClass(data, code);
setError({ detail: { error: undefined, traceback: undefined } });
setOpen(false);
}

View file

@ -54,6 +54,7 @@ export default function DictAreaModal({
/>
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {

View file

@ -12,7 +12,7 @@ import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(FlowsContext);
const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
@ -26,7 +26,7 @@ const ExportModal = forwardRef(
const [open, setOpen] = useState(false);
return (
<BaseModal size="smaller" open={open} setOpen={setOpen}>
<BaseModal size="smaller-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{props.children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
<span className="pr-2">Export</span>
@ -40,8 +40,6 @@ const ExportModal = forwardRef(
<EditFlowSettings
name={name}
description={description}
flows={flows}
tabId={tabId}
setName={setName}
setDescription={setDescription}
/>
@ -57,7 +55,7 @@ const ExportModal = forwardRef(
Save with my API keys
</label>
</div>
<span className="text-xs text-destructive">
<span className=" text-xs text-destructive ">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
@ -70,9 +68,11 @@ const ExportModal = forwardRef(
downloadFlow(
{
id: tabId,
data: reactFlowInstance?.toObject()!,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!,
description
@ -85,9 +85,11 @@ const ExportModal = forwardRef(
downloadFlow(
removeApiKeys({
id: tabId,
data: reactFlowInstance?.toObject()!,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!,
description

View file

@ -5,13 +5,14 @@ import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import { FlowsContext } from "../../contexts/flowsContext";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import BaseModal from "../baseModal";
export default function FlowSettingsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const { flows, tabId, updateFlow, saveFlow } = useContext(FlowsContext);
const { flows, tabId, saveFlow } = useContext(FlowsContext);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -19,7 +20,6 @@ export default function FlowSettingsModal({
}, [flow!.name, flow!.description]);
const [name, setName] = useState(flow!.name);
const [description, setDescription] = useState(flow!.description);
const [invalidName, setInvalidName] = useState(false);
function handleClick(): void {
let savedFlow = flows.find((flow) => flow.id === tabId);
@ -28,6 +28,17 @@ export default function FlowSettingsModal({
saveFlow(savedFlow!);
setOpen(false);
}
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== flow!.name));
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="smaller">
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
@ -36,19 +47,20 @@ export default function FlowSettingsModal({
</BaseModal.Header>
<BaseModal.Content>
<EditFlowSettings
invalidName={invalidName}
setInvalidName={setInvalidName}
invalidNameList={nameLists}
name={name}
description={description}
flows={flows}
tabId={tabId}
setName={setName}
setDescription={setDescription}
/>
</BaseModal.Content>
<BaseModal.Footer>
<Button disabled={invalidName} onClick={handleClick} type="submit">
<Button
disabled={nameLists.includes(name) && name !== flow!.name}
onClick={handleClick}
type="submit"
>
Save
</Button>
</BaseModal.Footer>

View file

@ -394,7 +394,7 @@ export default function FormModal({
const message = inputs;
addChatHistory(message!, true, chatKey!, template.current);
sendAll({
...reactFlowInstance?.toObject()!,
...flow.data!,
inputs: inputs!,
chatHistory,
name: flow.name,

View file

@ -40,7 +40,7 @@ export default function GenericModal({
const [inputValue, setInputValue] = useState(value);
const [isEdit, setIsEdit] = useState(true);
const [wordsHighlight, setWordsHighlight] = useState<string[]>([]);
const { setErrorData, setSuccessData, setNoticeData } =
const { setErrorData, setSuccessData, setNoticeData, setModalContextOpen } =
useContext(alertContext);
const ref = useRef();
const divRef = useRef(null);
@ -106,15 +106,14 @@ export default function GenericModal({
: "code-nohighlight";
}
// Function need some review, working for now
function validatePrompt(closeModal: boolean): void {
//nodeClass is always null on tweaks
postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => {
// if field_name is an empty string, then we need to set it
// to the first key of the custom_fields object
if (field_name === "") {
console.log(apiReturn.data?.frontend_node?.custom_fields);
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""]
)
@ -123,38 +122,23 @@ export default function GenericModal({
}
if (apiReturn.data) {
let inputVariables = apiReturn.data.input_variables ?? [];
if (inputVariables && inputVariables.length === 0) {
if (!inputVariables || inputVariables.length === 0) {
setIsEdit(true);
setNoticeData({
title: "Your template does not have any variables.",
});
setModalOpen(false);
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
}
} else {
setIsEdit(false);
setSuccessData({
title: "Prompt is ready",
});
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
) {
setNodeClass!(apiReturn.data?.frontend_node, inputValue);
setModalOpen(closeModal);
setIsEdit(false);
setSuccessData({
title: "Prompt is ready",
});
}
}
} else {
@ -174,6 +158,10 @@ export default function GenericModal({
});
}
useEffect(() => {
setModalContextOpen(modalOpen);
}, [modalOpen]);
return (
<BaseModal
onChangeOpenModal={(open) => {}}
@ -195,7 +183,9 @@ export default function GenericModal({
}
})()}
>
<span className="pr-2">{myModalTitle}</span>
<span className="pr-2" data-testid="modal-title">
{myModalTitle}
</span>
<IconComponent
name="FileText"
className="h-6 w-6 pl-1 text-primary "
@ -213,6 +203,7 @@ export default function GenericModal({
{type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea
id={"modal-" + id}
data-testid={"modal-" + id}
ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue}
@ -252,6 +243,8 @@ export default function GenericModal({
handleKeyDown(e, value, "");
}}
readOnly={readonly}
id={"text-area-modal"}
data-testid={"text-area-modal"}
/>
) : (
<></>
@ -308,6 +301,7 @@ export default function GenericModal({
)}
</div>
<Button
data-testid="genericModalBtnSave"
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {

View file

@ -0,0 +1,311 @@
import { Loader2 } from "lucide-react";
import { ReactNode, useContext, useEffect, useMemo, useState } from "react";
import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { typesContext } from "../../contexts/typesContext";
import {
getStoreComponents,
getStoreTags,
saveFlowStore,
updateFlowStore,
} from "../../controllers/API";
import { FlowType } from "../../types/flow";
import {
removeApiKeys,
removeFileNameFromComponents,
} from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import ConfirmationModal from "../ConfirmationModal";
import BaseModal from "../baseModal";
export default function ShareModal({
component,
is_component,
children,
open,
setOpen,
disabled,
}: {
children?: ReactNode;
is_component: boolean;
component: FlowType;
open?: boolean;
setOpen?: (open: boolean) => void;
disabled?: boolean;
}): JSX.Element {
const { version, addFlow } = useContext(FlowsContext);
const { hasApiKey, hasStore } = useContext(StoreContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { reactFlowInstance } = useContext(typesContext);
const [checked, setChecked] = useState(false);
const [name, setName] = useState(component?.name ?? "");
const [description, setDescription] = useState(component?.description ?? "");
const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "Component" : "Flow";
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [loadingTags, setLoadingTags] = useState<boolean>(false);
const [sharePublic, setSharePublic] = useState(true);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [unavaliableNames, setUnavaliableNames] = useState<
{ id: string; name: string }[]
>([]);
const { saveFlow, flows, tabId } = useContext(FlowsContext);
const [loadingNames, setLoadingNames] = useState(false);
useEffect(() => {
if (open || internalOpen) {
if (hasApiKey && hasStore) {
handleGetTags();
handleGetNames();
}
}
}, [open, internalOpen, hasApiKey, hasStore]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags().then((res) => {
setTags(res);
setLoadingTags(false);
});
}
async function handleGetNames() {
setLoadingNames(true);
const unavaliableNames: Array<{ id: string; name: string }> = [];
await getStoreComponents({
fields: ["name", "id", "is_component"],
filterByUser: true,
}).then((res) => {
res?.results?.forEach((element: any) => {
if ((element.is_component ?? false) === is_component)
unavaliableNames.push({ name: element.name, id: element.id });
});
setUnavaliableNames(unavaliableNames);
setLoadingNames(false);
});
}
useEffect(() => {
setName(component?.name ?? "");
setDescription(component?.description ?? "");
}, [component, open, internalOpen]);
const handleShareComponent = async (update = false) => {
//remove file names from flows before sharing
removeFileNameFromComponents(component);
const flow: FlowType = checked
? {
id: component!.id,
data: component!.data,
description,
name,
last_tested_version: version,
is_component: is_component,
}
: removeApiKeys({
id: component!.id,
data: component!.data,
description,
name,
last_tested_version: version,
is_component: is_component,
});
await saveFlow(flow!, true);
function successShare() {
if (is_component) {
addFlow(true, flow);
}
setSuccessData({
title: `${nameComponent} shared successfully`,
});
}
if (!update)
saveFlowStore(flow!, getTagsIds(selectedTags, tags), sharePublic).then(
successShare,
(err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
});
};
const handleUpdateComponent = () => {
handleShareComponent(true);
if (setOpen) setOpen(false);
else internalSetOpen(false);
};
let modalConfirmation = useMemo(() => {
return (
<>
<ConfirmationModal
open={openConfirmationModal}
title={`Replace`}
cancelText="Cancel"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={() => {
handleUpdateComponent();
setOpenConfirmationModal(false);
}}
onCancel={() => {
setOpenConfirmationModal(false);
}}
>
<ConfirmationModal.Content>
<span>
It seems {name} already exists. Do you want to replace it with the
current?
</span>
<br></br>
<span className=" text-xs text-destructive ">
Warning: This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</>
);
}, [
unavaliableNames,
name,
loadingNames,
handleShareComponent,
openConfirmationModal,
]);
return (
<>
<BaseModal
size="smaller-h-full"
open={(!disabled && open) ?? internalOpen}
setOpen={setOpen ?? internalSetOpen}
>
<BaseModal.Trigger>{children ? children : <></>}</BaseModal.Trigger>
<BaseModal.Header
description={`Share your ${nameComponent} to the Langflow Store.`}
>
<span className="pr-2">Share</span>
<IconComponent
name="Share2"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<EditFlowSettings
name={name}
invalidNameList={unavaliableNames.map((element) => element.name)}
description={description}
setName={setName}
setDescription={setDescription}
/>
<div className="mt-3 flex h-8 w-full">
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={false}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
</div>
<div className="mt-5 flex items-center space-x-2">
<Checkbox
id="public"
checked={sharePublic}
onCheckedChange={(event: boolean) => {
setSharePublic(event);
setChecked(false);
}}
/>
<label htmlFor="public" className="export-modal-save-api text-sm ">
Make {nameComponent} public
</label>
</div>
<div className="mt-3 flex items-center space-x-2">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(event: boolean) => {
setChecked(event);
}}
disabled={sharePublic}
/>
<label htmlFor="terms" className="export-modal-save-api text-sm ">
Save with my API keys
</label>
</div>
<span className=" text-xs text-destructive ">
Warning: Deselecting this box will exclusively eliminate API keys
from fields explicitly designated for API keys.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<div className="text-right">
<Button
disabled={loadingNames}
type="button"
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
);
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
} else {
setOpenConfirmationModal(true);
}
}}
>
{loadingNames ? (
<>
<div className="center w-16">
<Loader2 className="m-auto h-4 w-4 animate-spin"></Loader2>
</div>
</>
) : (
<>
{is_component && !loadingNames ? "Save and " : ""}Share{" "}
{!is_component && !loadingNames ? "Flow" : ""}
</>
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
<>{modalConfirmation}</>
</>
);
}

View file

@ -312,7 +312,6 @@ export default function AdminPage() {
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
@ -326,12 +325,20 @@ export default function AdminPage() {
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_active"
checked={user.is_active}
/>
</div>
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit">
<Checkbox
id="is_active"
checked={user.is_active}
/>
</div>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
@ -340,7 +347,6 @@ export default function AdminPage() {
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
@ -354,12 +360,20 @@ export default function AdminPage() {
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</div>
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit">
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</div>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
@ -402,7 +416,6 @@ export default function AdminPage() {
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
@ -412,12 +425,20 @@ export default function AdminPage() {
handleDeleteUser(user);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
<ConfirmationModal.Content>
<span>
Are you sure you want to delete this user?
This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</div>
</TableCell>

View file

@ -224,7 +224,6 @@ export default function ApiKeysPage() {
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this key? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
@ -234,15 +233,19 @@ export default function ApiKeysPage() {
handleDeleteKey(keys);
}}
>
<ShadTooltip
content="Delete"
side="top"
>
<ConfirmationModal.Content>
<span>
Are you sure you want to delete
this key? This action cannot be
undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</div>
</TableCell>

View file

@ -1,116 +0,0 @@
import { useContext, useEffect, useState } from "react";
import { Button } from "../../components/ui/button";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { useNavigate } from "react-router-dom";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element {
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
useContext(FlowsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const { setErrorData } = useContext(alertContext);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
// Show community examples on screen
function handleExamples(): void {
setLoadingExamples(true);
getExamples()
.then((result) => {
setLoadingExamples(false);
setExamples(result);
})
.catch((error) =>
setErrorData({
title: "there was an error loading examples, please try again",
list: [error.message],
})
);
}
const navigate = useNavigate();
// Show community examples on page start
useEffect(() => {
handleExamples();
}, []);
return (
<>
<Header />
<div className="community-page-arrangement">
<div className="community-page-nav-arrangement">
<span className="community-page-nav-title">
<IconComponent name="Users2" className="w-6" />
Community Examples
</span>
<div className="community-page-nav-button">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
rel="noreferrer"
>
<Button variant="primary">
<IconComponent
name="GithubIcon"
className="main-page-nav-button"
/>
Add Your Example
</Button>
</a>
</div>
</div>
<span className="community-page-description-text">
Discover and learn from shared examples by the Langflow community. We
welcome new example contributions that can help our community explore
new and powerful features.
</span>
<div className="community-pages-flows-panel">
{loadingExamples ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
examples.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
onClick={() => {
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
});
}}
>
<IconComponent
name="GitFork"
className="main-page-nav-button"
/>
Fork Example
</Button>
}
/>
))
)}
</div>
</div>
</>
);
}

View file

@ -13,10 +13,8 @@ import ReactFlow, {
Controls,
Edge,
EdgeChange,
Node,
NodeChange,
NodeDragHandler,
OnEdgesDelete,
OnSelectionChangeParams,
SelectionDragHandler,
addEdge,
@ -27,6 +25,7 @@ import ReactFlow, {
} from "reactflow";
import GenericNode from "../../../../CustomNodes/GenericNode";
import Chat from "../../../../components/chatComponent";
import Loading from "../../../../components/ui/loading";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { locationContext } from "../../../../contexts/locationContext";
@ -42,7 +41,7 @@ import {
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
@ -61,7 +60,6 @@ export default function Page({
let {
updateFlow,
uploadFlow,
addFlow,
getNodeId,
paste,
lastCopiedSelection,
@ -70,6 +68,7 @@ export default function Page({
saveFlow,
setTabsState,
tabId,
saveCurrentFlow,
} = useContext(FlowsContext);
const {
types,
@ -83,16 +82,24 @@ export default function Page({
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { takeSnapshot } = useContext(undoRedoContext);
const { nodesOnFlow, setNodesOnFlow } = useContext(FlowsContext);
const [position, setPosition] = useState({ x: 0, y: 0 });
const position = useRef({ x: 0, y: 0 });
const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null);
useEffect(() => {
// this effect is used to attach the global event handlers
const saveCurrentFlowTimeout = () => {
setTimeout(() => {
saveCurrentFlow();
}, 500); // need to do this because ReactFlow is not asynchronous.
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (!isWrappedWithClass(event, "nocopy")) {
if (
!isWrappedWithClass(event, "nocopy") &&
window.getSelection()?.toString().length === 0
) {
if (
(event.ctrlKey || event.metaKey) &&
event.key === "c" &&
@ -107,10 +114,10 @@ export default function Page({
lastCopiedSelection
) {
event.preventDefault();
let bounds = reactFlowWrapper.current?.getBoundingClientRect();
takeSnapshot();
paste(lastCopiedSelection, {
x: position.x - bounds!.left,
y: position.y - bounds!.top,
x: position.current.x,
y: position.current.y,
});
}
if (
@ -127,13 +134,16 @@ export default function Page({
lastSelection
) {
event.preventDefault();
takeSnapshot();
deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id));
saveCurrentFlowTimeout();
}
}
};
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
position.current = { x: event.clientX, y: event.clientY };
};
document.addEventListener("keydown", onKeyDown);
@ -143,7 +153,12 @@ export default function Page({
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove);
};
}, [position, lastCopiedSelection, lastSelection]);
}, [
lastCopiedSelection,
lastSelection,
takeSnapshot,
saveCurrentFlowTimeout,
]);
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
@ -152,48 +167,43 @@ export default function Page({
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
const [loading, setLoading] = useState(true);
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [edges]);
//update flow when tabs change
useEffect(() => {
setLoading(true);
setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
reactFlowInstance.fitView();
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 });
const [seconds, setSeconds] = useState(0);
// Clear the previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Create a new timeout
timeoutRef.current = setTimeout(() => {
setLoading(false);
}, 300);
// Clear the timeout when the component is unmounted
return () => {
clearTimeout(timeoutRef.current);
};
}, [flow, reactFlowInstance]);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prevSeconds) => {
let updatedSeconds = prevSeconds + 1;
if (updatedSeconds % 30 === 0) {
saveFlow(flow, true);
updatedSeconds = 0;
}
return updatedSeconds;
});
}, 1000);
saveFlow(flow, true);
}, 30000);
return () => {
clearInterval(interval);
@ -203,10 +213,6 @@ export default function Page({
const onEdgesChangeMod = useCallback(
(change: EdgeChange[]) => {
onEdgesChange(change);
setNodes((node) => {
let newX = _.cloneDeep(node);
return newX;
});
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
@ -217,27 +223,37 @@ export default function Page({
},
};
});
saveCurrentFlowTimeout();
},
[onEdgesChange, setNodes, setTabsState, tabId]
[onEdgesChange, setNodes, setTabsState, saveCurrentFlowTimeout, tabId]
);
const onNodesChangeMod = useCallback(
(change: NodeChange[]) => {
onNodesChange(change);
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
const changeString = JSON.stringify(change);
if (changeString !== nodesOnFlow) {
onNodesChange(change);
updateNodeFlow(changeString);
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
saveCurrentFlowTimeout();
}
},
[onNodesChange, setTabsState, tabId]
[onNodesChange, setTabsState, tabId, updateNodeFlow, saveCurrentFlowTimeout]
);
function updateNodeFlow(changeString: string) {
setNodesOnFlow(changeString);
}
const onConnect = useCallback(
(params: Connection) => {
takeSnapshot();
@ -267,7 +283,7 @@ export default function Page({
return newX;
});
},
[setEdges, setNodes, takeSnapshot]
[setEdges, setNodes, takeSnapshot, addEdge]
);
const onNodeDragStart: NodeDragHandler = useCallback(() => {
@ -281,11 +297,6 @@ export default function Page({
takeSnapshot();
}, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
if (event.dataTransfer.types.some((types) => types === "nodedata")) {
@ -311,9 +322,9 @@ export default function Page({
);
// Calculate the position where the node should be created
const position = reactFlowInstance!.project({
x: event.clientX - reactflowBounds!.left,
y: event.clientY - reactflowBounds!.top,
const position = reactFlowInstance!.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// Generate a unique node ID
@ -350,7 +361,21 @@ export default function Page({
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
if (event.dataTransfer.files.item(0)!.type === "application/json") {
uploadFlow(false, event.dataTransfer.files.item(0)!);
const position = {
x: event.clientX,
y: event.clientY,
};
uploadFlow({
newProject: false,
isComponent: false,
file: event.dataTransfer.files.item(0)!,
position: position,
}).catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
@ -364,6 +389,9 @@ export default function Page({
);
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
return () => {
if (tabsState && tabsState[flow.id]?.isPending) {
saveFlow(flow);
@ -371,21 +399,6 @@ export default function Page({
};
}, []);
const onDelete = useCallback(
(mynodes: Node[]) => {
takeSnapshot();
setEdges(
edges.filter(
(edge) =>
!mynodes.some(
(node) => edge.source === node.id || edge.target === node.id
)
)
);
},
[takeSnapshot, edges, setEdges]
);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
@ -437,6 +450,20 @@ export default function Page({
setFilterEdge([]);
}, []);
const onMove = useCallback(() => {
saveCurrentFlowTimeout();
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
}, [setTabsState, saveCurrentFlowTimeout]);
return (
<div className="flex h-full overflow-hidden">
{!view && <ExtraSidebar />}
@ -448,15 +475,17 @@ export default function Page({
{Object.keys(templates).length > 0 &&
Object.keys(types).length > 0 ? (
<div id="react-flow-id" className="h-full w-full">
<div
className={cn(
"relative flex h-full w-full items-center justify-center bg-background",
!loading ? "hidden" : ""
)}
>
<Loading />
</div>
<ReactFlow
nodes={nodes}
onMove={() => {
if (reactFlowInstance)
updateFlow({
...flow,
data: reactFlowInstance.toObject(),
});
}}
onMove={onMove}
edges={edges}
onNodesChange={onNodesChangeMod}
onEdgesChange={onEdgesChangeMod}
@ -471,12 +500,10 @@ export default function Page({
onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onSelectionChange={onSelectionChange}
onNodesDelete={onDelete}
deleteKeyCode={[]}
className="theme-attribution"
minZoom={0.01}
@ -498,6 +525,7 @@ export default function Page({
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
takeSnapshot();
if (
validateSelection(lastSelection!, edges).length === 0
) {

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import IconComponent from "../../../../components/genericIconComponent";
import { GradientGroup } from "../../../../icons/GradientSparkles";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
@ -37,15 +37,19 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
<div className="h-10 w-28 overflow-hidden">
<div
className={
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
(isTransitioning ? " translate-y-0" : " translate-y-10")
"duration-400 h-10 w-24 rounded-md border border-indigo-300 bg-background px-2.5 text-primary shadow-inner transition-all ease-in-out" +
(isTransitioning ? " opacity-100" : " opacity-0 ")
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
onClick={onClick}
>
<IconComponent name="Group" className="w-6" />
<GradientGroup
strokeWidth={1.5}
size={22}
className="text-primary"
/>
Group
</button>
</div>

View file

@ -1,29 +1,37 @@
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { typesContext } from "../../../../contexts/typesContext";
import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal";
import { APIClassType, APIObjectType } from "../../../../types/api";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
import {
classNames,
removeCountFromString,
sensitiveSort,
} from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
export default function ExtraSidebar(): JSX.Element {
const { data, templates, getFilterEdge, setFilterEdge } =
const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } =
useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
const { setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const isPending = tabsState[tabId]?.isPending;
@ -52,8 +60,10 @@ export default function ExtraSidebar(): JSX.Element {
let ret = {};
Object.keys(data).forEach((d: keyof APIObjectType, i) => {
ret[d] = {};
let keys = Object.keys(data[d]).filter((nd) =>
nd.toLowerCase().includes(e.toLowerCase())
let keys = Object.keys(data[d]).filter(
(nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
);
keys.forEach((element) => {
ret[d][element] = data[d][element];
@ -76,7 +86,8 @@ export default function ExtraSidebar(): JSX.Element {
}, []);
function handleBlur() {
if (!search && search === "") {
// check if search is search to reset fitler on click input
if ((!search && search === "") || search === "search") {
setFilterData(data);
setFilterEdge([]);
setSearch("");
@ -84,12 +95,19 @@ export default function ExtraSidebar(): JSX.Element {
}
useEffect(() => {
if (getFilterEdge.length === 0 && search === "") {
setFilterData(data);
setFilterEdge([]);
if (getFilterEdge.length !== 0) {
setSearch("");
}
}, [getFilterEdge]);
if (getFilterEdge.length === 0 && search === "") {
setSearch("");
setFilterData(data);
}
}, [getFilterEdge, data]);
useEffect(() => {
handleSearchInput(search);
}, [data]);
useEffect(() => {
if (getFilterEdge?.length > 0) {
@ -128,6 +146,108 @@ export default function ExtraSidebar(): JSX.Element {
}
}, [getFilterEdge]);
useEffect(() => {
if (getFilterEdge?.length > 0) {
setFilterData((_) => {
let dataClone = cloneDeep(data);
let ret = {};
Object.keys(dataClone).forEach((d: keyof APIObjectType, i) => {
ret[d] = {};
if (getFilterEdge.some((x) => x.family === d)) {
ret[d] = dataClone[d];
const filtered = getFilterEdge
.filter((x) => x.family === d)
.pop()
.type.split(",");
for (let i = 0; i < filtered.length; i++) {
filtered[i] = filtered[i].trimStart();
}
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
delete ret[d][element];
}
});
}
}
});
setSearch("");
return ret;
});
}
}, [getFilterEdge, data]);
const ModalMemo = useMemo(
() =>
!hasApiKey || !validApiKey ? (
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"extra-side-bar-buttons",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default"
: ""
)}
>
<IconComponent
name="Share2"
className={classNames(
"side-bar-button-size",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
)}
/>
</button>
) : (
<ShareModal
is_component={false}
component={flow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"extra-side-bar-buttons",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default"
: ""
)}
>
<IconComponent
name="Share2"
className={classNames(
"side-bar-button-size",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
)}
/>
</button>
</ShareModal>
),
[hasApiKey, validApiKey, flow, hasStore]
);
const ExportMemo = useMemo(
() => (
<ExportModal>
<ShadTooltip content="Export" side="top">
<button className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</button>
</ShadTooltip>
</ExportModal>
),
[]
);
return (
<div className="side-bar-arrangement">
<div className="side-bar-buttons-arrangement">
@ -136,25 +256,21 @@ export default function ExtraSidebar(): JSX.Element {
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow(false);
uploadFlow({ newProject: false, isComponent: false }).catch(
(error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
}
);
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
</div>
<div className="side-bar-button">
<ExportModal>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="FileDown"
className="side-bar-button-size"
/>
</div>
</ShadTooltip>
</ExportModal>
</div>
<div className="side-bar-button">{ExportMemo}</div>
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
@ -177,30 +293,44 @@ export default function ExtraSidebar(): JSX.Element {
</div>
</ShadTooltip>
<div className="side-bar-button">
<ShadTooltip content="Save" side="top">
<button
className={
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
}
onClick={(event) => {
saveFlow(flow!);
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
}
/>
</button>
</ShadTooltip>
{flow && flow.data && (
<ShadTooltip content="Save" side="top">
<div>
<button
disabled={flow?.data?.nodes.length === 0}
className={
"extra-side-bar-buttons " +
(isPending && flow!.data!.nodes?.length > 0
? ""
: "button-disable")
}
onClick={(event) => {
saveFlow(flow!);
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending && flow!.data!.nodes?.length > 0
? " "
: " extra-side-bar-save-disable")
}
/>
</button>
</div>
</ShadTooltip>
)}
</div>
<ShadTooltip content="Share" side="top" styleClasses="cursor-default">
<div className="side-bar-button">{ModalMemo}</div>
</ShadTooltip>
</div>
<Separator />
<div className="side-bar-search-div-placement">
<Input
onFocusCapture={() => handleBlur()}
value={search}
type="text"
name="search"
id="search"
@ -223,7 +353,19 @@ export default function ExtraSidebar(): JSX.Element {
<div className="side-bar-components-div-arrangement">
{Object.keys(dataFilter)
.sort()
.sort((a, b) => {
if (a.toLowerCase() === "saved_components") {
return -1;
} else if (b.toLowerCase() === "saved_components") {
return 1;
} else if (a.toLowerCase() === "custom_components") {
return -2;
} else if (b.toLowerCase() === "custom_components") {
return 2;
} else {
return a.localeCompare(b);
}
})
.map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<DisclosureComponent
@ -241,57 +383,45 @@ export default function ExtraSidebar(): JSX.Element {
>
<div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName])
.sort()
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
)
.map((SBItemName: string, index) => (
<ShadTooltip
content={data[SBSectionName][SBItemName].display_name}
content={
dataFilter[SBSectionName][SBItemName].display_name
}
side="right"
key={index}
>
<div key={index} data-tooltip-id={SBItemName}>
<div
draggable={!data[SBSectionName][SBItemName].error}
className={
"side-bar-components-border bg-background" +
(data[SBSectionName][SBItemName].error
? " cursor-not-allowed select-none"
: "")
}
style={{
borderLeftColor:
nodeColors[SBSectionName] ?? nodeColors.unknown,
}}
onDragStart={(event) =>
onDragStart(event, {
type: SBItemName,
node: data[SBSectionName][SBItemName],
})
}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName(
"cursor-grabbing"
)[0]
);
}}
>
<div
className="side-bar-components-div-form"
id={
"side" +
data[SBSectionName][SBItemName].display_name
}
>
<span className="side-bar-components-text">
{data[SBSectionName][SBItemName].display_name}
</span>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
</div>
</div>
</div>
<SidebarDraggableComponent
sectionName={SBSectionName as string}
apiClass={dataFilter[SBSectionName][SBItemName]}
key={index}
onDragStart={(event) =>
onDragStart(event, {
//split type to remove type in nodes saved with same name removing it's
type: removeCountFromString(SBItemName),
node: dataFilter[SBSectionName][SBItemName],
})
}
color={nodeColors[SBSectionName]}
itemName={SBItemName}
//convert error to boolean
error={!!dataFilter[SBSectionName][SBItemName].error}
display_name={
dataFilter[SBSectionName][SBItemName].display_name
}
official={
dataFilter[SBSectionName][SBItemName].official ===
false
? false
: true
}
/>
</ShadTooltip>
))}
</div>

View file

@ -0,0 +1,152 @@
import { DragEventHandler, useContext, useRef, useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../../components/ui/select-custom";
import { AuthContext } from "../../../../../contexts/authContext";
import { FlowsContext } from "../../../../../contexts/flowsContext";
import { APIClassType } from "../../../../../types/api";
import {
createFlowComponent,
downloadNode,
} from "../../../../../utils/reactflowUtils";
import { removeCountFromString } from "../../../../../utils/utils";
export default function SidebarDraggableComponent({
sectionName,
display_name,
itemName,
error,
color,
onDragStart,
apiClass,
official,
}: {
sectionName: string;
apiClass: APIClassType;
display_name: string;
itemName: string;
error: boolean;
color: string;
onDragStart: DragEventHandler<HTMLDivElement>;
official: boolean;
}) {
const [open, setOpen] = useState(false);
const { getNodeId, deleteComponent, version } = useContext(FlowsContext);
const { autoLogin, userData } = useContext(AuthContext);
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const popoverRef = useRef<HTMLDivElement>(null);
const handlePointerDown = (e) => {
if (!open) {
const rect = popoverRef.current?.getBoundingClientRect() ?? {
left: 0,
top: 0,
};
setCursorPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}
};
function handleSelectChange(value: string) {
switch (value) {
case "share":
break;
case "download":
const type = removeCountFromString(itemName);
downloadNode(
createFlowComponent(
{ id: getNodeId(type), type, node: apiClass },
version
)
);
break;
case "delete":
deleteComponent(display_name);
break;
}
}
return (
<Select
onValueChange={handleSelectChange}
onOpenChange={(change) => setOpen(change)}
open={open}
key={itemName}
>
<div
onPointerDown={handlePointerDown}
onContextMenuCapture={(e) => {
e.preventDefault();
setOpen(true);
}}
key={itemName}
data-tooltip-id={itemName}
>
<div
draggable={!error}
className={
"side-bar-components-border bg-background" +
(error ? " cursor-not-allowed select-none" : "")
}
style={{
borderLeftColor: color,
}}
onDragStart={onDragStart}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0]
);
}}
>
<div
data-testid={sectionName + display_name}
id={sectionName + display_name}
className="side-bar-components-div-form"
>
<span className="side-bar-components-text">{display_name}</span>
<div ref={popoverRef}>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
<SelectTrigger></SelectTrigger>
<SelectContent
position="popper"
side="bottom"
sideOffset={-25}
style={{
position: "absolute",
left: cursorPos.x,
top: cursorPos.y,
}}
>
<SelectItem value={"download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
{!official && (
<SelectItem value={"delete"}>
<div className="flex">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Delete{" "}
</div>{" "}
</SelectItem>
)}
</SelectContent>
</div>
</div>
</div>
</div>
</Select>
);
}

View file

@ -1,4 +1,5 @@
import { useContext, useState } from "react";
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useReactFlow, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
@ -9,17 +10,23 @@ import {
SelectTrigger,
} from "../../../../components/ui/select-custom";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import ConfirmationModal from "../../../../modals/ConfirmationModal";
import EditNodeModal from "../../../../modals/EditNodeModal";
import ShareModal from "../../../../modals/shareModal";
import { nodeToolbarPropsType } from "../../../../types/components";
import { FlowType } from "../../../../types/flow";
import {
createFlowComponent,
downloadNode,
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
import { classNames } from "../../../../utils/utils";
export default function NodeToolbarComponent({
data,
setData,
deleteNode,
position,
setShowNode,
@ -38,10 +45,14 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "prompt" ||
data.node.template[templateField].type === "file" ||
data.node.template[templateField].type === "Any" ||
data.node.template[templateField].type === "int")
data.node.template[templateField].type === "int" ||
data.node.template[templateField].type === "dict" ||
data.node.template[templateField].type === "NestedDict")
).length
);
const updateNodeInternals = useUpdateNodeInternals();
const { getNodeId } = useContext(FlowsContext);
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
function canMinimize() {
let countHandles: number = 0;
@ -54,30 +65,56 @@ export default function NodeToolbarComponent({
const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false;
const { paste } = useContext(FlowsContext);
const { paste, saveComponent, version, flows } = useContext(FlowsContext);
const { takeSnapshot } = useContext(undoRedoContext);
const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState("");
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>();
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [data, data.node, showModalAdvanced]);
const handleSelectChange = (event) => {
setSelectedValue(event);
if (event.includes("advanced")) {
return setShowModalAdvanced(true);
}
setShowModalAdvanced(false);
if (event.includes("show")) {
setShowNode((prev) => !prev);
updateNodeInternals(data.id);
}
if (event.includes("disabled")) {
return;
}
if (event.includes("ungroup")) {
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance);
switch (event) {
case "advanced":
setShowModalAdvanced(true);
break;
case "show":
takeSnapshot();
setShowNode(data.showNode ?? true ? false : true);
updateNodeInternals(data.id);
break;
case "Download":
downloadNode(createFlowComponent(cloneDeep(data), version));
break;
case "Share":
if (hasApiKey || hasStore) setShowconfirmShare(true);
break;
case "SaveAll":
saveComponent(cloneDeep(data), false);
break;
case "disabled":
break;
case "ungroup":
takeSnapshot();
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance, getNodeId);
break;
case "override":
setShowOverrideModal(true);
break;
}
};
const isSaved = flows.some((flow) =>
Object.values(flow).includes(data.node?.display_name!)
);
return (
<>
<div className="w-26 h-10">
@ -145,43 +182,28 @@ export default function NodeToolbarComponent({
</a>
</ShadTooltip>
{isMinimal || isGroup ? (
<Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top">
<SelectTrigger>
<div id="advancedIcon">
<div
className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent
name="MoreHorizontal"
className="relative left-2 h-4 w-4"
/>
</div>
</div>
</SelectTrigger>
</ShadTooltip>
<SelectContent>
<SelectItem
value={
getRandomKeyByssmm() +
(nodeLength == 0 ? "disabled" : "advanced")
}
>
<Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top">
<SelectTrigger>
<div>
<div
id="editAdvancedBtn"
className={
"flex " +
(nodeLength == 0
? "text-muted-foreground"
: "text-primary")
}
data-testid="more-options-modal"
className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
>
<IconComponent
name="MoreHorizontal"
className="relative left-2 h-4 w-4"
/>
</div>
</div>
</SelectTrigger>
</ShadTooltip>
<SelectContent>
{nodeLength > 0 && (
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
<div className="flex" data-testid="edit-button-modal">
<IconComponent
name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4"
@ -189,62 +211,115 @@ export default function NodeToolbarComponent({
Edit{" "}
</div>{" "}
</SelectItem>
{isMinimal && (
<SelectItem value={getRandomKeyByssmm() + "show"}>
<div className="flex" id="editAdvanced">
<IconComponent
name={showNode ? "Minimize2" : "Maximize2"}
className="relative top-0.5 mr-2 h-4 w-4"
/>
{showNode ? "Minimize" : "Expand"}
</div>
</SelectItem>
)}
{isGroup && (
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
<div className="flex">
<IconComponent
name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
) : (
<ShadTooltip content="Edit" side="top">
<div id="editAdvancedIcon">
<button
disabled={nodeLength === 0}
onClick={() => setShowModalAdvanced(true)}
className={classNames(
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</button>
</div>
</ShadTooltip>
)}
)}
{showModalAdvanced && (
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
open={showModalAdvanced}
onClose={(modal) => {
setShowModalAdvanced(modal);
}}
>
{isSaved ? (
<SelectItem value={"override"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
) : (
<SelectItem value={"SaveAll"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
)}
{hasStore && (
<SelectItem
disabled={!hasApiKey || !validApiKey}
value={"Share"}
>
<div className="flex">
<IconComponent
name="Share2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Share{" "}
</div>{" "}
</SelectItem>
)}
<SelectItem value={"Download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
{isMinimal && (
<SelectItem value={"show"}>
<div className="flex">
<IconComponent
name={showNode ? "Minimize2" : "Maximize2"}
className="relative top-0.5 mr-2 h-4 w-4"
/>
{showNode ? "Minimize" : "Expand"}
</div>
</SelectItem>
)}
{isGroup && (
<SelectItem value="ungroup">
<div className="flex">
<IconComponent
name="Combine"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
<ConfirmationModal
asChild
open={showOverrideModal}
title={`Replace`}
cancelText="Create New"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={(index, user) => {
saveComponent(cloneDeep(data), true);
}}
onClose={setShowOverrideModal}
onCancel={() => saveComponent(cloneDeep(data), false)}
>
<ConfirmationModal.Content>
<span>
It seems {data.node?.display_name} already exists. Do you want
to replace it with the current or create a new one?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</EditNodeModal>
)}
</ConfirmationModal.Trigger>
</ConfirmationModal>
<EditNodeModal
data={data}
nodeLength={nodeLength}
open={showModalAdvanced}
setOpen={setShowModalAdvanced}
/>
<ShareModal
open={showconfirmShare}
setOpen={setShowconfirmShare}
is_component={true}
component={flowComponent!}
/>
</span>
</div>
</>

View file

@ -1,12 +1,11 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import Header from "../../components/headerComponent";
import { FlowsContext } from "../../contexts/flowsContext";
import { getVersion } from "../../controllers/API";
import Page from "./components/PageComponent";
export default function FlowPage(): JSX.Element {
const { flows, tabId, setTabId } = useContext(FlowsContext);
const { flows, tabId, setTabId, version } = useContext(FlowsContext);
const { id } = useParams();
// Set flow tab id
@ -14,14 +13,6 @@ export default function FlowPage(): JSX.Element {
setTabId(id!);
}, [id]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<>
<Header />

View file

@ -0,0 +1,202 @@
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import PaginatorComponent from "../../../../components/PaginatorComponent";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { FlowType } from "../../../../types/flow";
export default function ComponentsComponent({
is_component = true,
}: {
is_component?: boolean;
}) {
const { flows, removeFlow, uploadFlow, addFlow, isLoading } =
useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(1);
const [loadingScreen, setLoadingScreen] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (isLoading) return;
const all = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
setData(all.slice(start, end));
}, [flows, pageIndex, pageSize]);
const [data, setData] = useState<FlowType[]>([]);
const name = is_component ? "Component" : "Flow";
const onFileDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow({
newProject: true,
file: e.dataTransfer.files.item(0)!,
isComponent: is_component,
})
.then(() => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
function resetFilter() {
setPageIndex(1);
setPageSize(10);
}
useEffect(() => {
setTimeout(() => {
setLoadingScreen(false);
}, 600);
}, []);
return (
<CardsWrapComponent
onFileDrop={onFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!loadingScreen && data.length === 0 ? (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1">
<span>New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{loadingScreen === false && data?.length > 0 ? (
data?.map((item, idx) => (
<CollectionCardComponent
onDelete={() => {
removeFlow(item.id);
setSuccessData({
title: `${
item.is_component ? "Component" : "Flow"
} deleted successfully!`,
});
resetFilter();
}}
key={idx}
data={item}
disabled={isLoading}
button={
!is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
/>
))
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!loadingScreen && data.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={
flows.filter((f) => (f.is_component ?? false) === is_component)
.length
}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
);
}

View file

@ -1,180 +1,111 @@
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Group, ToyBrick } from "lucide-react";
import { useContext, useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import DropdownButton from "../../components/DropdownButtonComponent";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import PageLayout from "../../components/pageLayout";
import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
export default function HomePage(): JSX.Element {
const {
flows,
setTabId,
downloadFlows,
uploadFlows,
addFlow,
removeFlow,
uploadFlow,
isLoading,
} = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext);
const { setTabId, downloadFlows, uploadFlows, addFlow, uploadFlow } =
useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const location = useLocation();
const pathname = location.pathname;
const is_component = pathname === "/components";
const dropdownOptions = [
{
name: "Import from JSON",
onBtnClick: () =>
uploadFlow(true).then((id) => {
navigate("/flow/" + id);
}),
onBtnClick: () => {
uploadFlow({
newProject: true,
isComponent: is_component,
})
.then((id) => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
if (!is_component) navigate("/flow/" + id);
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
},
},
];
const sidebarNavItems = [
{
title: "Flows",
href: "/flows",
icon: <Group className="w-5 stroke-[1.5]" />,
},
{
title: "Components",
href: "/components",
icon: <ToyBrick className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]" />,
},
];
// Set a null id
useEffect(() => {
setTabId("");
}, []);
}, [pathname]);
const navigate = useNavigate();
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = () => {
setIsDragging(false);
};
const fileDrop = (e) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow(true, e.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
// Personal flows display
return (
<>
<Header />
<div className="main-page-panel">
<div className="main-page-nav-arrangement">
<span className="main-page-nav-title">
<IconComponent name="Home" className="w-6" />
{USER_PROJECTS_HEADER}
</span>
<div className="button-div-style">
<Button
variant="primary"
onClick={() => {
downloadFlows();
}}
>
<IconComponent name="Download" className="main-page-nav-button" />
Download Collection
</Button>
<Button
variant="primary"
onClick={() => {
uploadFlows();
}}
>
<IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection
</Button>
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
options={dropdownOptions}
/>
</div>
<PageLayout
title={USER_PROJECTS_HEADER}
description="Manage your personal projects. Download or upload your collection."
button={
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
downloadFlows();
}}
>
<IconComponent name="Download" className="main-page-nav-button" />
Download Collection
</Button>
<Button
variant="primary"
onClick={() => {
uploadFlows();
}}
>
<IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection
</Button>
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
options={dropdownOptions}
/>
</div>
<span className="main-page-description-text">
Manage your personal projects. Download or upload your collection.
</span>
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={fileDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<IconComponent
name="ArrowUpToLine"
className="h-12 w-12 stroke-1"
/>
Drop your flow here
</>
) : (
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
)}
</div>
)}
}
>
<div className="flex h-full w-full space-y-8 lg:flex-row lg:space-x-8 lg:space-y-0">
<aside className="flex h-full flex-col space-y-6 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div className="h-full w-full flex-1">
<Outlet />
</div>
</div>
</>
</PageLayout>
);
}

View file

@ -0,0 +1,412 @@
import { uniqueId } from "lodash";
import { useContext, useEffect, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CollectionCardComponent from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import PageLayout from "../../components/pageLayout";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { Link, useParams } from "react-router-dom";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Badge } from "../../components/ui/badge";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils";
export default function StorePage(): JSX.Element {
const { validApiKey, setValidApiKey, hasApiKey, loadingApiKey } =
useContext(StoreContext);
const { apiKey } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
const { setTabId } = useContext(FlowsContext);
const [loading, setLoading] = useState(true);
const [loadingTags, setLoadingTags] = useState(true);
const { id } = useParams();
const [filteredCategories, setFilterCategories] = useState<any[]>([]);
const [inputText, setInputText] = useState<string>("");
const [searchData, setSearchData] = useState<storeComponent[]>([]);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const [pageSize, setPageSize] = useState(12);
const [pageIndex, setPageIndex] = useState(1);
const [pageOrder, setPageOrder] = useState("Popular");
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [tabActive, setTabActive] = useState("All");
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
useEffect(() => {
handleGetTags();
}, []);
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
setErrorData({
title: "API Key Error",
list: [
"You don't have an API Key. Please add one to use the Langflow Store.",
],
});
setLoading(false);
} else if (!validApiKey) {
setErrorData({
title: "API Key Error",
list: [
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.",
],
});
}
}
}, [loadingApiKey, validApiKey, hasApiKey]);
useEffect(() => {
handleGetComponents();
}, [
tabActive,
pageOrder,
pageIndex,
pageSize,
filteredCategories,
selectFilter,
validApiKey,
hasApiKey,
apiKey,
searchNow,
loadingApiKey,
id,
]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags()
.then((res) => {
setTags(res);
setLoadingTags(false);
})
.catch((err) => {
console.log(err);
setLoadingTags(false);
});
}
function handleGetComponents() {
if (!hasApiKey || loadingApiKey) return;
setLoading(true);
getStoreComponents({
component_id: id,
page: pageIndex,
limit: pageSize,
is_component:
tabActive === "All" ? null : tabActive === "Flows" ? false : true,
sort: pageOrder === "Popular" ? "-count(downloads)" : "name",
tags: filteredCategories,
liked: selectFilter === "likedbyme" && validApiKey ? true : null,
isPrivate: null,
search: inputText === "" ? null : inputText,
filterByUser: selectFilter === "createdbyme" && validApiKey ? true : null,
})
.then((res) => {
if (!res?.authorized && validApiKey === true) {
setValidApiKey(false);
setSelectFilter("all");
} else {
if (res?.authorized) {
setValidApiKey(true);
}
setLoading(false);
setSearchData(res?.results ?? []);
setTotalRowsCount(
filteredCategories?.length === 0
? Number(res?.count ?? 0)
: res?.results?.length ?? 0
);
}
})
.catch((err) => {
if (err.response.status === 403 || err.response.status === 401) {
setValidApiKey(false);
} else {
setSearchData([]);
setTotalRowsCount(0);
setLoading(false);
setErrorData({
title: "Error getting components.",
list: [err["response"]["data"]["detail"]],
});
}
});
}
// Set a null id
useEffect(() => {
setTabId("");
}, []);
function resetPagination() {
setPageIndex(1);
setPageSize(12);
}
return (
<PageLayout
title="Langflow Store"
description="Search flows and components from the community."
button={
<>
{StoreApiKeyModal && (
<StoreApiKeyModal disabled={loading}>
<Button
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : ""
)}
variant="primary"
>
<IconComponent name="Key" className="mr-2 w-4" />
API Key
</Button>
</StoreApiKeyModal>
)}
</>
}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4 p-0">
<div className="flex items-end gap-4">
<div className="relative h-12 w-[40%]">
<Input
disabled={loading}
placeholder="Search Flows and Components"
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setInputText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchNow(uniqueId());
}
}}
value={inputText}
/>
<button
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
onClick={() => {
setSearchNow(uniqueId());
}}
>
<IconComponent
name={loading ? "Loader2" : "Search"}
className={loading ? " animate-spin cursor-not-allowed" : ""}
/>
</button>
</div>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
disabled={loading}
onClick={() => {
setTabActive("All");
}}
className={
(tabActive === "All"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
All
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Flows");
}}
className={
(tabActive === "Flows"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Flows
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Components");
}}
className={
(tabActive === "Components"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Components
</button>
<ShadTooltip content="Coming Soon">
<button className="cursor-not-allowed p-3 text-muted-foreground">
Bundles
</button>
</ShadTooltip>
</div>
</div>
<div className="flex items-center gap-2">
<Select
disabled={loading}
onValueChange={setSelectFilter}
value={selectFilter}
>
<SelectTrigger className="mr-4 w-[160px] flex-shrink-0">
<SelectValue placeholder="Filter Values" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="createdbyme"
>
Created By Me
</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="likedbyme"
>
Liked By Me
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{id === undefined ? (
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={loading}
selectedTags={filteredCategories}
setSelectedTags={setFilterCategories}
/>
) : (
<Badge
key="id"
variant="outline"
size="sq"
className="gap-2 bg-beta-foreground text-background hover:bg-beta-foreground"
>
<Link to={"/store"} className="cursor-pointer">
<IconComponent name="X" className="h-4 w-4" />
</Link>
{id}
</Badge>
)}
</div>
<div className="flex items-end justify-between">
<span className="px-0.5 text-sm text-muted-foreground">
{(!loading || searchData.length !== 0) && (
<>
{totalRowsCount} {totalRowsCount !== 1 ? "results" : "result"}
</>
)}
</span>
<Select
disabled={loading}
onValueChange={(e) => {
setPageOrder(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Popular" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Popular">Popular</SelectItem>
{/* <SelectItem value="Recent">Most Recent</SelectItem> */}
<SelectItem value="Alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{!loading || searchData.length !== 0 ? (
searchData.map((item) => {
return (
<>
<CollectionCardComponent
key={item.id}
data={item}
authorized={validApiKey}
disabled={loading}
/>
</>
);
})
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
{!loading && searchData?.length === 0 && (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex h-full w-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
{selectFilter != "all" ? (
<>
You haven't{" "}
{selectFilter === "createdbyme" ? "created" : "liked"}{" "}
anything with the selected filters yet.
</>
) : (
<>
There are no{" "}
{tabActive == "Flows" ? "Flows" : "Components"} with the
selected filters.
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
{!loading && searchData.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
totalRowsCount={totalRowsCount}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</PageLayout>
);
}

View file

@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import { FlowsContext } from "../../contexts/flowsContext";
import { getVersion } from "../../controllers/API";
import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() {
@ -13,14 +12,6 @@ export default function ViewPage() {
setTabId(id!);
}, [id]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<div className="flow-page-positioning">
{flows.length > 0 &&

View file

@ -1,21 +1,31 @@
import { Route, Routes } from "react-router-dom";
import { useEffect } from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import { ProtectedAdminRoute } from "./components/authAdminGuard";
import { ProtectedRoute } from "./components/authGuard";
import { ProtectedLoginRoute } from "./components/authLoginGuard";
import { CatchAllRoute } from "./components/catchAllRoutes";
import { StoreGuard } from "./components/storeGuard";
import AdminPage from "./pages/AdminPage";
import LoginAdminPage from "./pages/AdminPage/LoginPage";
import ApiKeysPage from "./pages/ApiKeysPage";
import CommunityPage from "./pages/CommunityPage";
import FlowPage from "./pages/FlowPage";
import HomePage from "./pages/MainPage";
import ComponentsComponent from "./pages/MainPage/components/components";
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
import StorePage from "./pages/StorePage";
import ViewPage from "./pages/ViewPage";
import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage";
import SignUp from "./pages/signUpPage";
const Router = () => {
const navigate = useNavigate();
useEffect(() => {
// Redirect from root to /flows
if (window.location.pathname === "/") {
navigate("/flows");
}
}, [navigate]);
return (
<Routes>
<Route
@ -25,15 +35,37 @@ const Router = () => {
<HomePage />
</ProtectedRoute>
}
/>
>
<Route
path="flows"
element={<ComponentsComponent key="flows" is_component={false} />}
/>
<Route
path="components"
element={<ComponentsComponent key="components" />}
/>
</Route>
<Route
path="/community"
path="/store"
element={
<ProtectedRoute>
<CommunityPage />
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route
path="/store/:id/"
element={
<ProtectedRoute>
<StoreGuard>
<StorePage />
</StoreGuard>
</ProtectedRoute>
}
/>
<Route path="/flow/:id/">
<Route
path=""

View file

@ -66,7 +66,7 @@
@apply fixed right-4;
}
.side-bar-arrangement {
@apply flex h-full w-52 flex-col overflow-hidden border-r scrollbar-hide;
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
}
.side-bar-search-div-placement {
@apply relative mx-auto mb-2 mt-2 flex items-center;
@ -114,7 +114,7 @@
@apply pointer-events-none;
}
.extra-side-bar-buttons {
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
}
.extra-side-bar-buttons:hover {
@apply hover:bg-muted;
@ -123,7 +123,7 @@
@apply flex gap-2;
}
.primary-input {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left text-sm shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50;
}
.skeleton-card {
@ -205,19 +205,19 @@
}
.loading-page-panel {
@apply flex h-full w-full items-center justify-center bg-muted;
@apply flex h-full w-full items-center justify-center bg-background;
}
.main-page-panel {
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
@apply flex-max-width h-full flex-col overflow-auto bg-background px-16;
}
.admin-page-panel {
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
@apply flex-max-width h-full flex-col overflow-auto bg-background px-16;
}
.main-page-nav-arrangement {
@apply flex-max-width justify-between px-6 py-12 pb-2;
@apply flex-max-width justify-between py-8 pb-2;
}
.main-page-nav-title {
@ -229,23 +229,23 @@
}
.main-page-description-text {
@apply flex w-[60%] px-6 pb-14 text-muted-foreground;
@apply flex w-[60%] pb-4 text-muted-foreground;
}
.admin-page-description-text {
@apply flex w-[80%] px-6 pb-8 text-muted-foreground;
@apply flex w-[80%] pb-8 text-muted-foreground;
}
.main-page-flows-display {
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
@apply grid w-full gap-4 pb-7 md:grid-cols-2 lg:grid-cols-3;
}
.community-page-arrangement {
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
@apply flex-max-width h-full flex-col justify-between overflow-auto bg-background px-16;
}
.community-page-nav-arrangement {
@apply flex-max-width justify-between px-6 py-12 pb-2;
@apply flex-max-width justify-between py-8 pb-2;
}
.community-page-nav-title {
@ -257,7 +257,7 @@
}
.community-page-description-text {
@apply flex w-[70%] px-6 pb-8 text-muted-foreground;
@apply flex w-[70%] pb-8 text-muted-foreground;
}
.community-pages-flows-panel {
@ -311,10 +311,10 @@
@apply hover:text-accent-foreground hover:transition-all;
}
.generic-node-desc {
@apply h-full w-full text-foreground;
@apply h-full w-full text-foreground;
}
.generic-node-desc-text {
@apply w-full px-5 pb-3 text-sm text-muted-foreground;
@apply w-full px-5 mb-4 text-sm text-muted-foreground;
}
.alert-icon {
@ -353,34 +353,6 @@
@apply alert-font-size text-success-foreground word-break-break-word;
}
.card-component-title-display {
@apply round-button-div flex-max-width;
}
.card-component-image {
@apply flex h-7 w-7 items-center justify-center rounded-full text-2xl;
}
.card-component-title-size {
@apply w-full flex-1 truncate-doubleline word-break-break-word;
}
.card-component-delete-button {
@apply flex self-start;
}
.card-component-delete-icon {
@apply h-4 w-4 text-primary opacity-0 transition-all group-hover:opacity-100;
}
.card-component-desc {
@apply pb-2 pt-2;
}
.card-component-desc-text {
@apply truncate-doubleline;
}
.card-component-footer-arrangement {
@apply flex-max-width items-end justify-between gap-2;
}
.card-component-footer {
@apply flex flex-wrap gap-2;
}
.unused-side-bar-aside {
@apply flex flex-shrink-0 flex-col overflow-hidden border-r transition-all duration-500;
}
@ -495,7 +467,7 @@
@apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium;
}
.header-menu-bar-display {
@apply flex max-w-[200px] cursor-pointer items-center gap-2;
@apply flex max-w-[120px] cursor-pointer items-center gap-2 lg:max-w-[200px];
}
.header-menu-flow-name {
@apply flex-1 truncate;
@ -505,13 +477,13 @@
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-border bg-background;
@apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted;
}
.header-start-display {
@apply flex w-[30%] items-center justify-start gap-2;
@apply flex items-center justify-start gap-2;
}
.header-end-division {
@apply flex w-[30%] justify-end px-2;
@apply flex justify-end px-2;
}
.header-end-display {
@apply ml-auto mr-2 flex items-center gap-5;
@ -535,7 +507,7 @@
@apply hover:bg-accent hover:text-accent-foreground;
}
.header-github-display {
@apply -mr-px ml-2 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
@apply -mr-px ml-1 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
}
.header-notifications-box {
@apply fixed left-0 top-0 h-screen w-screen;
@ -1061,4 +1033,55 @@
.input-invalid {
@apply border-destructive focus:border-destructive focus:ring-destructive;
}
.fade-container {
@apply relative overflow-hidden;
}
.fade-container::before,
.fade-container::after {
@apply pointer-events-none absolute bottom-0 top-0 bg-gradient-to-r to-transparent from-background;
content: "";
width: 50px;
opacity: 0;
transition: opacity 0.3s;
}
.fade-container::after {
@apply right-0;
transform: rotate(180deg);
}
.fade-container.fade-left::before,
.fade-container.fade-right::after {
opacity: 1;
}
.fade-container-dark {
@apply relative overflow-hidden;
}
.fade-container-dark::before,
.fade-container-dark::after {
@apply pointer-events-none absolute bottom-0 top-0;
content: "";
width: 50px;
opacity: 0;
transition: opacity 0.3s;
background: linear-gradient(to right, black, transparent);
}
.fade-container-dark:after {
@apply right-0;
transform: rotate(180deg);
}
.fade-container-dark.fade-left::before,
.fade-container-dark.fade-right::after {
opacity: 1;
}
.scroll-container {
@apply flex overflow-x-scroll scrollbar-hide;
}
}

View file

@ -47,6 +47,9 @@
--chat-bot-icon: #afe6ef;
--chat-user-icon: #aface9;
--component-icon: #d8598a;
--flow-icon: #2f67d0;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923de;
--build-trigger: #dc735b;
@ -107,6 +110,9 @@
--medium-indigo: #6366f1;
--low-indigo: #e0e7ff;
--component-icon: #c35f85;
--flow-icon: #2467e4;
/* Colors that are shared in dark and light mode */
--blur-shared: #151923d2;
--build-trigger: #dc735b;

View file

@ -23,6 +23,7 @@ export type APIClassType = {
beta?: boolean;
documentation: string;
error?: string;
official?: boolean;
flow?: FlowType;
[key: string]:
| Array<string>
@ -120,3 +121,10 @@ export type Users = {
create_at: Date;
updated_at: Date;
};
export type Component = {
name: string;
description: string;
data: Object;
tags: [string];
};

View file

@ -36,10 +36,10 @@ export type DropDownComponentType = {
editNode?: boolean;
apiModal?: boolean;
numberOfOptions?: number;
id?: string;
};
export type ParameterComponentType = {
data: NodeDataType;
setData: (value: NodeDataType) => void;
title: string;
id: sourceHandleType | targetHandleType;
color: string;
@ -54,6 +54,7 @@ export type ParameterComponentType = {
proxy?: { field: string; id: string };
showNode?: boolean;
index?: string;
onCloseModal?: (close: boolean) => void;
};
export type InputListComponentType = {
value: string[];
@ -68,8 +69,7 @@ export type KeyPairListComponentType = {
disabled: boolean;
editNode?: boolean;
duplicateKey?: boolean;
advanced?: boolean | null;
dataValue?: any;
editNodeModal?: boolean;
};
export type DictComponentType = {
@ -77,6 +77,7 @@ export type DictComponentType = {
onChange: (value) => void;
disabled: boolean;
editNode?: boolean;
id?: string;
};
export type TextAreaComponentType = {
@ -94,7 +95,7 @@ export type TextAreaComponentType = {
export type PromptAreaComponentType = {
field_name?: string;
nodeClass?: APIClassType;
setNodeClass?: (value: APIClassType) => void;
setNodeClass?: (value: APIClassType, code?: string) => void;
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
@ -109,7 +110,7 @@ export type CodeAreaComponentType = {
value: string;
editNode?: boolean;
nodeClass?: APIClassType;
setNodeClass?: (value: APIClassType) => void;
setNodeClass?: (value: APIClassType, code?: string) => void;
dynamic?: boolean;
id?: string;
readonly?: boolean;
@ -119,7 +120,6 @@ export type FileComponentType = {
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
suffixes: Array<string>;
fileTypes: Array<string>;
onFileChange: (value: string) => void;
editNode?: boolean;
@ -216,18 +216,17 @@ export type IconComponentProps = {
className?: string;
iconColor?: string;
onClick?: () => void;
stroke?: string;
id?: string;
};
export type InputProps = {
name: string | null;
description: string | null;
maxLength?: number;
flows: Array<{ id: string; name: string; description: string }>;
tabId: string;
invalidName?: boolean;
setName: (name: string) => void;
setDescription: (description: string) => void;
setInvalidName?: (invalidName: boolean) => void;
invalidNameList?: string[];
};
export type TooltipProps = {
@ -246,8 +245,15 @@ export type LoadingComponentProps = {
remSize: number;
};
export type ContentProps = { children: ReactNode };
export type ContentProps = {
children: ReactNode;
tolltipContent?: ReactNode;
side?: "top" | "right" | "bottom" | "left";
};
export type HeaderProps = { children: ReactNode; description: string };
export type TriggerProps = {
children: ReactNode;
};
export interface languageMap {
[key: string]: string | undefined;
@ -261,7 +267,7 @@ export type signUpInputStateType = {
export type inputHandlerEventType = {
target: {
value: string | boolean;
value: string;
name: string;
};
};
@ -271,21 +277,37 @@ export type PaginatorComponentType = {
rowsCount?: number[];
totalRowsCount: number;
paginate: (pageIndex: number, pageSize: number) => void;
storeComponent?: boolean;
};
export type ConfirmationModalType = {
onCancel?: () => void;
title: string;
titleHeader: string;
titleHeader?: string;
asChild?: boolean;
modalContent: string;
modalContentTitle: string;
destructive?: boolean;
modalContentTitle?: string;
cancelText: string;
confirmationText: string;
children: ReactElement;
children: [
React.ReactElement<ContentProps>,
React.ReactElement<TriggerProps>
];
icon: string;
data: any;
index: number;
data?: any;
index?: number;
onConfirm: (index, data) => void;
open?: boolean;
onClose?: (close: boolean) => void;
size?:
| "x-small"
| "smaller"
| "small"
| "medium"
| "large"
| "large-h-full"
| "small-h-full"
| "medium-h-full";
};
export type UserManagementType = {
@ -332,14 +354,20 @@ export type ApiKeyType = {
onCloseModal: () => void;
};
export type ApiKeyInputType = {
apikeyname: string;
export type StoreApiKeyType = {
children: ReactElement;
disabled?: boolean;
};
export type groupedObjType = {
family: string;
type: string;
};
export type nodeGroupedObjType = {
displayName: string;
node: string[] | string;
};
type test = {
[char: string]: string;
};
@ -439,7 +467,6 @@ export type fileCardPropsType = {
export type nodeToolbarPropsType = {
data: NodeDataType;
deleteNode: (idx: string) => void;
setData: (newState: NodeDataType) => void;
position: XYPosition;
setShowNode: (boolean: any) => void;
numberOfHandles: boolean[] | [];
@ -473,7 +500,7 @@ export type codeAreaModalPropsType = {
setValue: (value: string) => void;
value: string;
nodeClass: APIClassType | undefined;
setNodeClass: (Class: APIClassType) => void | undefined;
setNodeClass: (Class: APIClassType, code?: string) => void | undefined;
children: ReactNode;
dynamic?: boolean;
readonly?: boolean;
@ -499,7 +526,7 @@ export type genericModalPropsType = {
modalTitle: string;
type: number;
nodeClass?: APIClassType;
setNodeClass?: (Class: APIClassType) => void;
setNodeClass?: (Class: APIClassType, code?: string) => void;
children: ReactNode;
id?: string;
readonly?: boolean;
@ -526,8 +553,7 @@ export type groupDataType = {
};
export type cardComponentPropsType = {
flow: FlowType;
id: string;
data: FlowType;
onDelete?: () => void;
button?: JSX.Element;
};

View file

@ -14,4 +14,7 @@ export type AuthContextType = {
authenticationErrorCount: number;
autoLogin: boolean;
setAutoLogin: (autoLogin: boolean) => void;
apiKey: string | null;
setApiKey: (apiKey: string | null) => void;
storeApiKey: (apiKey: string) => void;
};

View file

@ -0,0 +1,9 @@
export type storeContextType = {
setHasStore: (store: boolean) => void;
hasStore: boolean;
setHasApiKey: (key: boolean) => void;
hasApiKey: boolean;
setValidApiKey: (key: boolean) => void;
validApiKey: boolean;
loadingApiKey: boolean;
};

View file

@ -1,6 +1,12 @@
import { FlowType } from "../flow";
export type sidebarNavigationItemType = {
name: string;
href: string;
icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
current: boolean;
};
export type localStorageUserType = {
components: { [key: string]: FlowType };
};

View file

@ -7,6 +7,11 @@ export type FlowType = {
data: ReactFlowJsonObject | null;
description: string;
style?: FlowStyleType;
is_component: boolean;
parent?: string;
date_created?: string;
updated_at?: string;
last_tested_version?: string;
};
export type NodeType = {
id: string;
@ -17,6 +22,7 @@ export type NodeType = {
};
export type NodeDataType = {
showNode?: boolean;
type: string;
node?: APIClassType;
id: string;

View file

@ -0,0 +1,20 @@
export type storeComponent = {
id: string;
is_component: boolean;
tags?: { id: string; name: string }[];
metadata?: any;
downloads_count?: number;
name: string;
description: string;
liked_by_count?: number;
liked_by_user?: boolean;
user_created?: { username: string };
last_tested_version?: string;
private?: boolean;
};
export type StoreComponentResponse = {
count: number;
authorized: boolean;
results: storeComponent[];
};

View file

@ -1,5 +1,6 @@
import { XYPosition } from "reactflow";
import { tweakType } from "../components";
import { FlowType } from "../flow";
import { FlowType, NodeDataType } from "../flow";
export type FlowsContextType = {
saveFlow: (flow: FlowType, silent?: boolean) => Promise<void>;
@ -10,7 +11,9 @@ export type FlowsContextType = {
removeFlow: (id: string) => void;
addFlow: (
newProject: boolean,
flow?: FlowType
flow?: FlowType,
override?: boolean,
position?: XYPosition
) => Promise<String | undefined>;
updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string;
@ -23,7 +26,18 @@ export type FlowsContextType = {
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
saveCurrentFlow: () => void;
uploadFlow: ({
newProject,
file,
isComponent,
position,
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
position?: XYPosition;
}) => Promise<String | never>;
hardReset: () => void;
getNodeId: (nodeType: string) => string;
tabsState: FlowsState;
@ -36,6 +50,14 @@ export type FlowsContextType = {
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
setTweak: (tweak: tweakType) => tweakType | void;
getTweak: tweakType;
saveComponent: (
component: NodeDataType,
override: boolean
) => Promise<String | undefined>;
deleteComponent: (key: string) => void;
version: string;
nodesOnFlow: string;
setNodesOnFlow: (nodes: string) => void;
};
export type FlowsState = {

View file

@ -44,6 +44,8 @@ export type alertContextType = {
removeFromNotificationList: (index: string) => void;
loading: boolean;
setLoading: (newState: boolean) => void;
modalContextOpen: boolean | null;
setModalContextOpen: (newState: boolean) => void;
};
export type darkContextType = {

View file

@ -8,7 +8,11 @@ import {
ReactFlowJsonObject,
XYPosition,
} from "reactflow";
import { specialCharsRegex } from "../constants/constants";
import ShortUniqueId from "short-unique-id";
import {
LANGFLOW_SUPPORTED_TYPES,
specialCharsRegex,
} from "../constants/constants";
import { APITemplateType, TemplateVariableType } from "../types/api";
import {
FlowType,
@ -96,8 +100,7 @@ export function isValidConnection(
(t) =>
targetHandleObject.inputTypes?.some((n) => n === t) ||
t === targetHandleObject.type
) ||
targetHandleObject.type === "str"
)
) {
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
if (!targetNode) {
@ -162,46 +165,51 @@ export function updateIds(
) {
let idsMap = {};
newFlow.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
let newId = getNodeId(node.data.node?.flow ? "GroupNode" : node.data.type);
idsMap[node.id] = newId;
node.id = newId;
node.data.id = newId;
// Add the new node to the list of nodes in state
});
if (newFlow.nodes)
newFlow.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
let newId = getNodeId(
node.data.node?.flow ? "GroupNode" : node.data.type
);
idsMap[node.id] = newId;
node.id = newId;
node.data.id = newId;
// Add the new node to the list of nodes in state
});
newFlow.edges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: edge.source,
if (newFlow.edges)
newFlow.edges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: edge.source,
});
if (edge.data?.sourceHandle?.id) {
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: edge.target,
});
if (edge.data?.targetHandle?.id) {
edge.data.targetHandle.id = edge.target;
}
edge.id =
"reactflow__edge-" +
edge.source +
edge.sourceHandle +
"-" +
edge.target +
edge.targetHandle;
});
if (edge.data?.sourceHandle?.id) {
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: edge.target,
});
if (edge.data?.targetHandle?.id) {
edge.data.targetHandle.id = edge.target;
}
edge.id =
"reactflow__edge-" +
edge.source +
edge.sourceHandle +
"-" +
edge.target +
edge.targetHandle;
});
return idsMap;
}
export function buildTweaks(flow: FlowType) {
@ -518,6 +526,7 @@ export function generateFlow(
const newFlow: FlowType = {
data: newFlowData,
is_component: false,
name: name,
description: "",
//generating local id instead of using the id from the server, can change in the future
@ -627,15 +636,7 @@ function updateGroupNodeTemplate(template: APITemplateType) {
let type = template[key].type;
let input_types = template[key].input_types;
if (
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
LANGFLOW_SUPPORTED_TYPES.has(type) &&
!template[key].required &&
!input_types
) {
@ -695,7 +696,6 @@ function isHandleConnected(
/*
this function receives a flow and a handleId and check if there is a connection with this handle
*/
scapedJSONStringfy({ type: field.type, fieldName: key, id: nodeId });
if (field.proxy) {
if (
edges.some(
@ -899,17 +899,43 @@ export function ungroupNode(
BaseFlow.edges = edges;
}
function updateProxyIdsOnTemplate(
template: APITemplateType,
idsMap: { [key: string]: string }
) {
Object.keys(template).forEach((key) => {
if (template[key].proxy && idsMap[template[key].proxy!.id]) {
template[key].proxy!.id = idsMap[template[key].proxy!.id];
}
});
}
function updateEdgesIds(edges: Edge[], idsMap: { [key: string]: string }) {
edges.forEach((edge) => {
let targetHandle: targetHandleType = edge.data.targetHandle;
if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
}
edge.data.targetHandle = targetHandle;
edge.targetHandle = scapedJSONStringfy(targetHandle);
});
}
export function expandGroupNode(
groupNode: NodeDataType,
ReactFlowInstance: ReactFlowInstance
ReactFlowInstance: ReactFlowInstance,
getNodeId: (type: string) => string
) {
const { template, flow } = _.cloneDeep(groupNode.node!);
const idsMap = updateIds(flow!.data!, getNodeId);
updateProxyIdsOnTemplate(template, idsMap);
let flowEdges = ReactFlowInstance.getEdges();
updateEdgesIds(flowEdges, idsMap);
const gNodes: NodeType[] = flow?.data?.nodes!;
const gEdges = flow!.data!.edges;
console.log(gEdges);
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
ReactFlowInstance.getEdges().forEach((edge) => {
flowEdges.forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
@ -995,7 +1021,6 @@ export function expandGroupNode(
...gEdges,
...updatedEdges,
];
console.log(edges);
ReactFlowInstance.setNodes(nodes);
ReactFlowInstance.setEdges(edges);
}
@ -1018,7 +1043,6 @@ export function getGroupStatus(
let status = { valid: true, params: "Built sucessfully ✨" };
const { nodes } = flow.data!;
const ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => console.log(ssData[id]));
ids.forEach((id) => {
if (!ssData[id]) {
status = ssData[id];
@ -1030,3 +1054,58 @@ export function getGroupStatus(
});
return status;
}
export function createFlowComponent(
nodeData: NodeDataType,
version: string
): FlowType {
nodeData.node!.official = false;
const flowNode: FlowType = {
data: {
edges: [],
nodes: [
{
data: nodeData,
id: nodeData.id,
position: { x: 0, y: 0 },
type: "genericNode",
},
],
viewport: { x: 1, y: 1, zoom: 1 },
},
description: nodeData.node?.description || "",
name: nodeData.node?.display_name || nodeData.type || "",
id: nodeData.id || "",
is_component: true,
last_tested_version: version,
};
return flowNode;
}
export function downloadNode(NodeFLow: FlowType) {
const element = document.createElement("a");
const file = new Blob([JSON.stringify(NodeFLow)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `${NodeFLow.name}.json`;
element.click();
}
export function updateComponentNameAndType(
data: any,
component: NodeDataType
) {}
export function removeFileNameFromComponents(flow: FlowType) {
flow.data!.nodes.forEach((node: NodeType) => {
Object.keys(node.data.node!.template).forEach((field) => {
if (node.data.node?.template[field].type === "file") {
node.data.node!.template[field].value = "";
}
});
if (node.data.node?.flow) {
removeFileNameFromComponents(node.data.node.flow);
}
});
}

View file

@ -0,0 +1,23 @@
import { cloneDeep } from "lodash";
import { FlowType } from "../types/flow";
export default function cloneFLowWithParent(
flow: FlowType,
parent: string,
is_component: boolean
) {
let childFLow = cloneDeep(flow);
childFLow.parent = parent;
childFLow.id = "";
childFLow.is_component = is_component;
return childFLow;
}
export function getTagsIds(
tags: string[],
tagListId: { name: string; id: string }[]
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
}

View file

@ -2,6 +2,8 @@ import {
ArrowUpToLine,
Bell,
BookMarked,
BookmarkPlus,
Boxes,
Check,
CheckCircle2,
ChevronDown,
@ -14,6 +16,7 @@ import {
Circle,
Clipboard,
Code2,
Combine,
Compass,
Copy,
Cpu,
@ -31,11 +34,14 @@ import {
FileText,
FileUp,
Fingerprint,
FolderPlus,
Gift,
GitBranchPlus,
GitFork,
GithubIcon,
Group,
Hammer,
Heart,
HelpCircle,
Home,
Info,
@ -44,7 +50,9 @@ import {
Layers,
Lightbulb,
Link,
Loader2,
Lock,
LogIn,
LucideSend,
Maximize2,
Menu,
@ -55,20 +63,27 @@ import {
Minus,
MoonIcon,
MoreHorizontal,
Network,
Paperclip,
Pencil,
Plus,
Redo,
RefreshCcw,
Rocket,
Save,
SaveAll,
Scissors,
Search,
Settings2,
Share2,
Shield,
Sparkles,
Square,
Store,
SunIcon,
TerminalIcon,
TerminalSquare,
ToyBrick,
Trash2,
Undo,
Ungroup,
@ -81,6 +96,7 @@ import {
Users2,
Variable,
Wand2,
Workflow,
Wrench,
X,
XCircle,
@ -97,7 +113,11 @@ import { EvernoteIcon } from "../icons/Evernote";
import { FBIcon } from "../icons/FacebookMessenger";
import { GitBookIcon } from "../icons/GitBook";
import { GoogleIcon } from "../icons/Google";
import { GradientSparkles } from "../icons/GradientSparkles";
import {
GradientInfinity,
GradientSave,
GradientUngroup,
} from "../icons/GradientSparkles";
import { HuggingFaceIcon } from "../icons/HuggingFace";
import { IFixIcon } from "../icons/IFixIt";
import { MetaIcon } from "../icons/Meta";
@ -154,9 +174,12 @@ export const nodeColors: { [char: string]: string } = {
prompts: "#4367BF",
llms: "#6344BE",
chains: "#FE7500",
Document: "#7AAE42",
list: "#9AAE42",
agents: "#903BBE",
tools: "#FF3434",
memories: "#F5B85A",
saved_components: "#a5B85A",
advanced: "#000000",
chat: "#198BF6",
thought: "#272541",
@ -181,6 +204,7 @@ export const nodeNames: { [char: string]: string } = {
agents: "Agents",
tools: "Tools",
memories: "Memories",
saved_components: "Saved",
advanced: "Advanced",
chat: "Chat",
embeddings: "Embeddings",
@ -233,6 +257,7 @@ export const nodeIconsLucide: iconsType = {
ChatVertexAI: VertexAIIcon,
VertexAIEmbeddings: VertexAIIcon,
agents: Rocket,
Workflow,
User,
WikipediaAPIWrapper: SvgWikipedia,
chains: Link,
@ -243,6 +268,7 @@ export const nodeIconsLucide: iconsType = {
advanced: Laptop2,
chat: MessageCircle,
embeddings: Fingerprint,
saved_components: GradientSave,
documentloaders: Paperclip,
vectorstores: Layers,
toolkits: Hammer,
@ -255,9 +281,12 @@ export const nodeIconsLucide: iconsType = {
unknown: HelpCircle,
WikipediaQueryRun: SvgWikipedia,
WolframAlphaQueryRun: SvgWolfram,
custom_components: GradientSparkles,
custom_components: GradientInfinity,
group_components: GradientUngroup,
custom: Edit,
Trash2,
Boxes,
Network,
X,
XCircle,
Info,
@ -285,6 +314,7 @@ export const nodeIconsLucide: iconsType = {
Clipboard,
Code2,
Variable,
Store,
Download,
Eraser,
Lock,
@ -293,6 +323,7 @@ export const nodeIconsLucide: iconsType = {
DownloadCloud,
File,
FileText,
FolderPlus,
GitFork,
GithubIcon,
FileDown,
@ -317,6 +348,7 @@ export const nodeIconsLucide: iconsType = {
Key,
Unplug,
Group,
LogIn,
ChevronUp,
Ungroup,
BookMarked,
@ -324,4 +356,15 @@ export const nodeIconsLucide: iconsType = {
Square,
Minimize2,
Maximize2,
SaveAll,
Share2,
GitBranchPlus,
Loader2,
BookmarkPlus,
Heart,
Link,
ToyBrick,
RefreshCcw,
Combine,
TerminalIcon,
};

View file

@ -9,6 +9,7 @@ import {
import {
IVarHighlightType,
groupedObjType,
nodeGroupedObjType,
tweakType,
} from "../types/components";
import { FlowType, NodeType } from "../types/flow";
@ -107,13 +108,15 @@ export function groupByFamily(
const baseClassesSet = new Set(baseClasses.split("\n"));
let arrOfPossibleInputs: Array<{
category: string;
nodes: string[];
nodes: nodeGroupedObjType[];
full: boolean;
display_name?: string;
}> = [];
let arrOfPossibleOutputs: Array<{
category: string;
nodes: string[];
nodes: nodeGroupedObjType[];
full: boolean;
display_name?: string;
}> = [];
let checkedNodes = new Map();
const excludeTypes = new Set([
@ -126,18 +129,23 @@ export function groupByFamily(
"int",
]);
const checkBaseClass = (template: TemplateVariableType) =>
template.type &&
template.show &&
((!excludeTypes.has(template.type) && baseClassesSet.has(template.type)) ||
(template.input_types &&
template.input_types.some((inputType) =>
baseClassesSet.has(inputType)
)));
const checkBaseClass = (template: TemplateVariableType) => {
return (
template.type &&
template.show &&
((!excludeTypes.has(template.type) &&
baseClassesSet.has(template.type)) ||
(template.input_types &&
template.input_types.some((inputType) => {
baseClassesSet.has(inputType);
})))
);
};
if (flow) {
for (const node of flow) {
const nodeData = node.data;
const foundNode = checkedNodes.get(nodeData.type);
checkedNodes.set(nodeData.type, {
hasBaseClassInTemplate:
@ -148,16 +156,18 @@ export function groupByFamily(
nodeData.node!.base_classes.some((baseClass) =>
baseClassesSet.has(baseClass)
),
displayName: nodeData.node?.display_name,
});
}
}
for (const [d, nodes] of Object.entries(data)) {
let tempInputs: string[] = [],
tempOutputs: string[] = [];
let tempInputs: nodeGroupedObjType[] = [],
tempOutputs: nodeGroupedObjType[] = [];
for (const [n, node] of Object.entries(nodes!)) {
let foundNode = checkedNodes.get(n);
if (!foundNode) {
foundNode = {
hasBaseClassInTemplate: Object.values(node!.template).some(
@ -166,15 +176,18 @@ export function groupByFamily(
hasBaseClassInBaseClasses: node!.base_classes.some((baseClass) =>
baseClassesSet.has(baseClass)
),
displayName: node?.display_name,
};
checkedNodes.set(n, foundNode);
}
if (foundNode.hasBaseClassInTemplate) tempInputs.push(n);
if (foundNode.hasBaseClassInBaseClasses) tempOutputs.push(n);
if (foundNode.hasBaseClassInTemplate)
tempInputs.push({ node: n, displayName: foundNode.displayName });
if (foundNode.hasBaseClassInBaseClasses)
tempOutputs.push({ node: n, displayName: foundNode.displayName });
}
const totalNodes = Object.keys(nodes!).length;
if (tempInputs.length)
arrOfPossibleInputs.push({
category: d,
@ -192,11 +205,15 @@ export function groupByFamily(
return left
? arrOfPossibleOutputs.map((output) => ({
family: output.category,
type: output.full ? "" : output.nodes.join(", "),
type: output.full
? ""
: output.nodes.map((item) => item.node).join(", "),
display_name: "",
}))
: arrOfPossibleInputs.map((input) => ({
family: input.category,
type: input.full ? "" : input.nodes.join(", "),
type: input.full ? "" : input.nodes.map((item) => item.node).join(", "),
display_name: input.nodes.map((item) => item.displayName).join(", "),
}));
}
@ -556,6 +573,77 @@ export function tabsArray(codes: string[], method: number) {
];
}
export function checkLocalStorageKey(key: string): boolean {
return localStorage.getItem(key) !== null;
}
export function IncrementObjectKey(
object: object,
key: string
): { newKey: string; increment: number } {
let count = 1;
const type = removeCountFromString(key);
let newKey = type + " " + `(${count})`;
while (object[newKey]) {
count++;
newKey = type + " " + `(${count})`;
}
return { newKey, increment: count };
}
export function removeCountFromString(input: string): string {
// Define a regex pattern to match the count in parentheses
const pattern = /\s*\(\w+\)\s*$/;
// Use the `replace` method to remove the matched pattern
const result = input.replace(pattern, "");
return result.trim(); // Trim any leading/trailing spaces
}
export function createRandomKey(key: string, uid: string): string {
return removeCountFromString(key) + ` (${uid})`;
}
export function sensitiveSort(a: string, b: string): number {
// Extract the name and number from each string using regular expressions
const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex);
const matchB = b.match(regex);
if (matchA && matchB) {
// Compare the names alphabetically
const nameA = matchA[1];
const nameB = matchB[1];
if (nameA !== nameB) {
return nameA.localeCompare(nameB);
}
// If the names are the same, compare the numbers numerically
const numberA = parseInt(matchA[2]);
const numberB = parseInt(matchB[2]);
return numberA - numberB;
} else {
// Handle cases where one or both strings do not match the expected pattern
// Simple strings are treated as pure alphabetical comparisons
return a.localeCompare(b);
}
}
// this function is used to get the set of keys from an object
export function getSetFromObject(obj: object, key?: string): Set<string> {
const set = new Set<string>();
if (key) {
for (const objKey in obj) {
set.add(obj[objKey][key]);
}
} else {
for (const key in obj) {
set.add(key);
}
}
return set;
}
export function getFieldTitle(
template: APITemplateType,
templateField: string

View file

@ -29,6 +29,8 @@ module.exports = {
},
extend: {
colors: {
"component-icon": "var(--component-icon)",
"flow-icon": "var(--flow-icon)",
"low-indigo": "var(--low-indigo)",
"chat-send": "var(--chat-send)",
connection: "var(--connection)",

View file

@ -13,7 +13,7 @@ test("CodeAreaModalComponent", async ({ page }) => {
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sidePythonFunctionTool"]')
.getByTestId("toolsPythonFunctionTool")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
@ -31,11 +31,8 @@ test("CodeAreaModalComponent", async ({ page }) => {
await page.locator('//*[@id="checkAndSaveBtn"]').click();
await page
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy();
@ -111,11 +108,8 @@ test("CodeAreaModalComponent", async ({ page }) => {
if (elementCount === 0) {
expect(true).toBeTruthy();
await page
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();

View file

@ -0,0 +1,104 @@
import { expect, test } from "@playwright/test";
test("dropDownComponent", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.waitForTimeout(2000);
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("amazon");
await page.waitForTimeout(2000);
await page
.getByTestId("llmsAmazon Bedrock")
.first()
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.getByTestId("dropdown-2-display").click();
await page.getByTestId("ai21.j2-grande-instruct-0-option").click();
let value = await page.getByTestId("dropdown-2-display").innerText();
if (value !== "ai21.j2-grande-instruct") {
expect(false).toBeTruthy();
}
await page.getByTestId("dropdown-2-display").click();
await page.getByTestId("ai21.j2-jumbo-instruct-1-option").click();
value = await page.getByTestId("dropdown-2-display").innerText();
if (value !== "ai21.j2-jumbo-instruct") {
expect(false).toBeTruthy();
}
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
value = await page.getByTestId("dropdown-edit-1-display").innerText();
if (value !== "ai21.j2-jumbo-instruct") {
expect(false).toBeTruthy();
}
// showcredentials_profile_name
await page.locator('//*[@id="showcredentials_profile_name"]').click();
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
).toBeFalsy();
// showmodel_id
await page.locator('//*[@id="showmodel_id"]').click();
expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy();
// showcredentials_profile_name
await page.locator('//*[@id="showcredentials_profile_name"]').click();
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
).toBeTruthy();
// showmodel_id
await page.locator('//*[@id="showmodel_id"]').click();
expect(
await page.locator('//*[@id="showmodel_id"]').isChecked()
).toBeTruthy();
// showcredentials_profile_name
await page.locator('//*[@id="showcredentials_profile_name"]').click();
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
).toBeFalsy();
// showmodel_id
await page.locator('//*[@id="showmodel_id"]').click();
expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy();
// showcredentials_profile_name
await page.locator('//*[@id="showcredentials_profile_name"]').click();
expect(
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
).toBeTruthy();
// showmodel_id
await page.locator('//*[@id="showmodel_id"]').click();
expect(
await page.locator('//*[@id="showmodel_id"]').isChecked()
).toBeTruthy();
await page.getByTestId("dropdown-edit-1-display").click();
await page.getByTestId("ai21.j2-ultra-v1-5-option").click();
value = await page.getByTestId("dropdown-edit-1-display").innerText();
if (value !== "ai21.j2-ultra-v1") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="saveChangesBtn"]').click();
value = await page.getByTestId("dropdown-2-display").innerText();
if (value !== "ai21.j2-ultra-v1") {
expect(false).toBeTruthy();
}
});

View file

@ -13,7 +13,7 @@ test("FloatComponent", async ({ page }) => {
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideLlamaCpp"]')
.locator('//*[@id="llmsLlamaCpp"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
@ -36,12 +36,8 @@ test("FloatComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="editAdvancedIcon"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.locator('//*[@id="showcache"]').click();
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy();
@ -380,12 +376,8 @@ test("FloatComponent", async ({ page }) => {
if (elementCount === 0) {
expect(true).toBeTruthy();
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="editAdvancedIcon"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
// showtemperature
await page.locator('//*[@id="showtemperature"]').click();

View file

@ -14,7 +14,8 @@ test.describe("Flow Page tests", () => {
.filter({ hasText: /^Custom Component$/ })
.nth(4)
.dragTo(page.locator(".react-flow__pane"));
await page.locator("div:nth-child(4) > .extra-side-bar-buttons").click();
await page.locator(".success-alert").click();
// await page.getByTestId("icon-ExternalLink").click();
// await page.locator('//*[@id="checkAndSaveBtn"]').click();
});
});

View file

@ -13,7 +13,8 @@ test("IntComponent", async ({ page }) => {
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideGET Request"]')
.getByTestId("utilitiesGET Request")
.first()
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
@ -30,7 +31,7 @@ test("IntComponent", async ({ page }) => {
}
await page.locator('//*[@id="int-input-2"]').click();
await page.locator('//*[@id="int-input-2"]').fill("-3");
await page.locator('//*[@id="int-input-2"]').fill("0");
value = await page.locator('//*[@id="int-input-2"]').inputValue();
@ -38,11 +39,8 @@ test("IntComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
value = await page.locator('//*[@id="int-input-1"]').inputValue();
@ -77,11 +75,8 @@ test("IntComponent", async ({ page }) => {
if (elementCount === 0) {
expect(true).toBeTruthy();
await page
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click();
await page.locator('//*[@id="showtimeout"]').click();
expect(

Some files were not shown because too many files have changed in this diff Show more