Merge branch 'feature/store' into bug/undo-copy and fixed edit node modal

This commit is contained in:
Lucas Oliveira 2023-11-23 14:39:53 -03:00
commit 807ea46464
295 changed files with 17396 additions and 7418 deletions

File diff suppressed because one or more lines are too long

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

@ -30,6 +30,9 @@ terminate_process_by_port() {
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
# install playwright if there is not installed yet
npx playwright install
# Navigate to the project root directory (where the Makefile is located)
cd ../../
@ -56,7 +59,7 @@ cd ../../
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
sleep 10
sleep 25
# Navigate back to the test directory
cd src/frontend

View file

@ -16,8 +16,8 @@ import {
FETCH_ERROR_MESSAGE,
} from "./constants/constants";
import { alertContext } from "./contexts/alertContext";
import { FlowsContext } from "./contexts/flowsContext";
import { locationContext } from "./contexts/locationContext";
import { TabsContext } from "./contexts/tabsContext";
import { typesContext } from "./contexts/typesContext";
import Router from "./routes";
@ -30,7 +30,7 @@ export default function App() {
setShowSideBar(true);
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext);
const { hardReset } = useContext(FlowsContext);
const {
errorData,
@ -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,10 @@ import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs";
import {
convertObjToArray,
convertValuesToNumbers,
@ -55,13 +54,14 @@ export default function ParameterComponent({
info = "",
proxy,
showNode,
index = "",
}: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0);
const { setTabsState, tabId, flows } = useContext(TabsContext);
const { setTabsState, tabId, flows } = useContext(FlowsContext);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -83,7 +83,11 @@ export default function ParameterComponent({
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);
@ -168,21 +172,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>
@ -351,9 +374,11 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + index}
/>
) : (
<InputComponent
id={"input-" + index}
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
@ -364,6 +389,7 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? (
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + index}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
setEnabled={(isEnabled) => {
@ -406,6 +432,7 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + index}
/>
</div>
) : left === true && type === "file" ? (
@ -427,16 +454,13 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + index}
/>
</div>
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
readonly={data.node?.flow ? true : false}
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
@ -447,6 +471,7 @@ export default function ParameterComponent({
onChange={(e) => {
handleOnNewValue(e);
}}
id={"prompt-input-" + index}
/>
</div>
) : left === true && type === "NestedDict" ? (

View file

@ -7,7 +7,7 @@ import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { undoRedoContext } from "../../contexts/undoRedoContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
@ -19,7 +19,7 @@ import {
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils";
import { classNames, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
@ -33,7 +33,7 @@ export default function GenericNode({
xPos: number;
yPos: number;
}): JSX.Element {
const { updateFlow, flows, tabId } = useContext(TabsContext);
const { updateFlow, flows, tabId } = useContext(FlowsContext);
const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
@ -230,6 +230,7 @@ export default function GenericNode({
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
index={idx.toString()}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
@ -248,15 +249,10 @@ export default function GenericNode({
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
title={getFieldTitle(
data.node?.template!,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
@ -421,7 +417,7 @@ export default function GenericNode({
/>
) : (
<div
className="generic-node-desc-text"
className="generic-node-desc-text break-all"
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
@ -433,11 +429,21 @@ export default function GenericNode({
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => {
if (a.toLowerCase() === "code") {
return -1;
} else if (b.toLowerCase() === "code") {
return 1;
} else {
return a.localeCompare(b);
}
})
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
index={idx.toString()}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
@ -456,15 +462,10 @@ export default function GenericNode({
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
title={getFieldTitle(
data.node?.template!,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
@ -501,29 +502,31 @@ export default function GenericNode({
>
{" "}
</div>
<ParameterComponent
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
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={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

@ -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,7 +36,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
{isMaxLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
{invalidName && (
{invalidNameList?.includes(name ?? "") && (
<span className="edit-flow-span">Name already in use</span>
)}
</div>

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

@ -1,8 +1,16 @@
import { useContext } from "react";
import { TabsContext } from "../../contexts/tabsContext";
import { cardComponentPropsType } from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
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(TabsContext);
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-indigo-500"
: "h-7 w-7 flex-shrink-0 text-emerald-800"
)}
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 && (
<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 ? (
<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

@ -7,9 +7,9 @@ import { typesContext } from "../../../contexts/typesContext";
import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow";
import { TabsContext } from "../../../contexts/tabsContext";
import { FlowsContext } from "../../../contexts/flowsContext";
import { parsedDataType } from "../../../types/components";
import { TabsState } from "../../../types/tabs";
import { FlowsState } from "../../../types/tabs";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
@ -26,7 +26,7 @@ export default function BuildTrigger({
}): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext);
const { setTabsState, saveFlow } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
@ -76,7 +76,11 @@ 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 response = await postBuildInit(flow);
const id = saveFlow({ ...flow, data: reactFlowInstance!.toObject() }, true);
const response = await postBuildInit({
...flow,
data: reactFlowInstance!.toObject(),
});
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = [];
@ -99,7 +103,7 @@ export default function BuildTrigger({
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
return {
...old,
[flowId]: {

View file

@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
import ChatTrigger from "./chatTrigger";
import * as _ from "lodash";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow";
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType): JSX.Element {
const [open, setOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -12,6 +12,7 @@ export default function CodeAreaComponent({
nodeClass,
dynamic,
setNodeClass,
id = "",
readonly = false,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
@ -43,6 +44,7 @@ export default function CodeAreaComponent({
>
<div className="flex w-full items-center">
<span
id={id}
className={
editNode
? "input-edit-node input-dialog"

View file

@ -22,7 +22,6 @@ export default function DictComponent({
}, [value]);
const ref = useRef(value);
debugger;
return (
<div
className={classNames(

View file

@ -3,7 +3,7 @@ import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => {
({ name, className, iconColor, stroke }: IconComponentProps, ref) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
return (
<TargetIcon
@ -11,6 +11,7 @@ const ForwardedIconComponent = forwardRef(
className={className}
style={iconColor ? { color: iconColor } : {}}
ref={ref}
stroke={stroke ? stroke : "currentColor"}
/>
);
}

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import {
DropdownMenu,
DropdownMenuContent,
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
const { addFlow } = useContext(TabsContext);
const { addFlow } = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext);
const { undo, redo } = useContext(undoRedoContext);
const [openSettings, setOpenSettings] = useState(false);

View file

@ -6,7 +6,9 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
import { StoreContext } from "../../contexts/storeContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -22,17 +24,18 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element {
const { flows, tabId } = useContext(TabsContext);
const { flows, tabId } = useContext(FlowsContext);
const { dark, setDark } = useContext(darkContext);
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

@ -17,6 +17,7 @@ export default function InputComponent({
editNode = false,
placeholder = "Type something...",
className,
id = "",
blurOnEnter = false,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
@ -33,6 +34,7 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
@ -61,6 +63,7 @@ export default function InputComponent({
</Form.Control>
) : (
<Input
id={id}
ref={refInput}
type="text"
onBlur={onBlur}

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { uploadFile } from "../../controllers/API";
import { FileComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
@ -17,7 +17,7 @@ export default function InputFileComponent({
const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext);
const { tabId } = useContext(TabsContext);
const { tabId } = useContext(FlowsContext);
// Clear component state
useEffect(() => {

View file

@ -8,6 +8,7 @@ export default function IntComponent({
onChange,
disabled,
editNode = false,
id = "",
}: FloatComponentType): JSX.Element {
const min = 0;
@ -21,6 +22,7 @@ export default function IntComponent({
return (
<div className="w-full">
<Input
id={id}
onKeyDown={(event) => {
if (
event.key !== "Backspace" &&
@ -34,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

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

@ -14,6 +14,7 @@ export default function PromptAreaComponent({
onChange,
disabled,
editNode = false,
id = "",
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
@ -23,7 +24,8 @@ export default function PromptAreaComponent({
}, [disabled]);
useEffect(() => {
if (value !== "" && !editNode && !readonly) {
//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);
@ -36,6 +38,7 @@ export default function PromptAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
id={id}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}
@ -49,6 +52,7 @@ export default function PromptAreaComponent({
>
<div className="flex w-full items-center">
<span
id={id}
className={
editNode
? "input-edit-node input-dialog"

View file

@ -0,0 +1,63 @@
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;
}[];
secondaryItems?: {
href: string;
title: string;
}[];
}
export default function SidebarNav({
className,
items,
secondaryItems,
...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-background hover:bg-background"
: "hover:bg-transparent hover:underline",
"justify-start"
)}
>
{item.title}
</Link>
))}
{/* {secondaryItems && (
<>
<div className="py-2">
<Separator />
</div>
<div className="flex justify-center">
<button className="flex h-8 w-fit items-center justify-between rounded-md border border-ring/60 px-4 py-2 text-sm text-primary hover:bg-muted">
<IconComponent name="FolderPlus" className="mr-2 h-4 w-4 " />
New Folder
</button>
</div>
</>
)} */}
</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,101 @@
import { useEffect, useRef, useState } from "react";
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 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 gap-2">
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={disabled ? "cursor-not-allowed" : ""}
onClick={() => {
updateTags(tag.name);
}}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
);
}

View file

@ -10,6 +10,7 @@ export default function TextAreaComponent({
onChange,
disabled,
editNode = false,
id = "",
}: TextAreaComponentType): JSX.Element {
// Clear text area
useEffect(() => {
@ -20,25 +21,30 @@ export default function TextAreaComponent({
return (
<div className="flex w-full items-center">
<Input
<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">
<Input
id={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
name="ExternalLink"
@ -48,8 +54,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

@ -23,6 +23,7 @@ const buttonVariants = cva(
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

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

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

@ -1,5 +1,5 @@
import { AxiosError } from "axios";
import _ from "lodash";
import _, { cloneDeep } from "lodash";
import {
ReactNode,
createContext,
@ -10,10 +10,10 @@ import {
} from "react";
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
import ShortUniqueId from "short-unique-id";
import { skipNodeUpdate } from "../constants/constants";
import {
deleteFlowFromDatabase,
downloadFlowsFromDatabase,
getVersion,
readFlowsFromDatabase,
saveFlowToDatabase,
updateFlowInDatabase,
@ -28,24 +28,29 @@ import {
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import { FlowsContextType, FlowsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
createFlowComponent,
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";
const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = {
const FlowsContextInitialValue: FlowsContextType = {
tabId: "",
setTabId: (index: string) => {},
isLoading: true,
@ -65,7 +70,7 @@ const TabsContextInitialValue: TabsContextType = {
lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {},
tabsState: {},
setTabsState: (state: TabsState) => {},
setTabsState: (state: FlowsState) => {},
getNodeId: (nodeType: string) => "",
setTweak: (tweak: any) => {},
getTweak: [],
@ -73,29 +78,32 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => {},
saveComponent: async (component: NodeDataType) => "",
deleteComponent: (key: string) => {},
version: "",
};
export const TabsContext = createContext<TabsContextType>(
TabsContextInitialValue
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 [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 +121,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 +135,41 @@ 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) => {
prev["saved_components"] = cloneDeep(savedComponents);
return prev;
});
}
@ -170,28 +194,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"];
}
@ -260,9 +262,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({
title: "Warning: Critical data, JSON file may include API keys.",
});
}
function downloadFlows() {
@ -290,46 +289,69 @@ 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,
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
}): Promise<String | never> {
return new Promise(async (resolve, reject) => {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (
(fileData.is_component === undefined && 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);
});
resolve("");
} else {
id = await addFlow(newProject, fileData);
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);
console.log(isComponent, fileData);
if (fileData.is_component === undefined) {
reject("Your file doesn't have the is_component property.");
} else if (
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() {
@ -360,12 +382,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));
}
}
/**
@ -395,127 +418,82 @@ export function TabsProvider({ children }: { children: ReactNode }) {
: reactFlowInstance!.project({ x: position.x, y: position.y });
selectionInstance.nodes.forEach((node: NodeType) => {
nodes = updateNodeId(
node,
idsMap,
nodes,
insidePosition,
minimumX,
minimumY
);
});
// Generate a unique node ID
let newId = getNodeId(node.data.type);
idsMap[node.id] = newId;
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position: {
x: insidePosition.x + node.position!.x - minimumX,
y: insidePosition.y + node.position!.y - minimumY,
},
data: {
..._.cloneDeep(node.data),
id: newId,
},
};
// Add the new node to the list of nodes in state
nodes = nodes
.map((node) => ({ ...node, selected: false }))
.concat({ ...newNode, selected: false });
});
reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge: Edge) => {
edges = updateEdgeId(edge, idsMap, edges);
});
let source = idsMap[edge.source];
let target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id =
"reactflow__edge-" +
source +
sourceHandle +
"-" +
target +
targetHandle;
edges = addEdge(
{
source,
target,
sourceHandle,
targetHandle,
id,
data: cloneDeep(edge.data),
style: { stroke: "#555" },
className:
targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ",
animated: targetHandleObject.type === "Text",
selected: false,
},
edges.map((edge) => ({ ...edge, selected: false }))
);
});
reactFlowInstance!.setEdges(edges);
}
const updateNodeId = (
node: NodeType,
idsMap,
nodes,
insidePosition,
minimumX,
minimumY
) => {
// Generate a unique node ID
let newId = getNodeId(node.data.type);
idsMap[node.id] = newId;
let data = {
..._.cloneDeep(node.data),
id: newId,
};
if (data.node?.flow) {
let internNodes = [];
data.node.flow.data!.nodes.map((node) => {
internNodes = updateNodeId(
node,
idsMap,
internNodes,
insidePosition,
minimumX,
minimumY
);
});
data.node.flow.data!.nodes = internNodes;
let internEdges = [];
data.node.flow.data!.edges.map((edge) => {
internEdges = updateEdgeId(edge, idsMap, internEdges);
});
data.node.flow.data!.edges = internEdges;
}
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
position: {
x: insidePosition.x + node.position!.x - minimumX,
y: insidePosition.y + node.position!.y - minimumY,
},
data,
};
// Add the new node to the list of nodes in state
nodes = nodes
.map((node) => ({ ...node, selected: false }))
.concat({ ...newNode, selected: false });
return nodes;
};
const updateEdgeId = (edge: Edge, idsMap, edges) => {
let source = idsMap[edge.source];
let target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id =
"reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle;
edges = addEdge(
{
data: edge.data,
source,
target,
sourceHandle,
targetHandle,
id,
style: { stroke: "#555" },
className:
targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ",
animated: targetHandleObject.type === "Text",
selected: false,
},
edges.map((edge) => ({ ...edge, selected: false }))
);
return edges;
};
const addFlow = async (
newProject: Boolean,
flow?: FlowType
@ -526,12 +504,12 @@ 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.
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.
@ -558,7 +536,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
let data = flow?.data ? flow.data : null;
if (data) {
processFlowEdges(flow);
processFlowNodes(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
// updateNodes(data.nodes, data.edges);
@ -569,46 +548,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 = (
@ -619,12 +569,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);
};
/**
@ -645,6 +600,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
async function saveFlow(newFlow: FlowType, silent?: boolean) {
console.log(newFlow);
if (newFlow?.data?.nodes?.length === 0) return;
try {
// updates flow in db
const updatedFlow = await updateFlowInDatabase(newFlow);
@ -657,10 +615,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
newFlows[index] = {
...newFlows[index],
description: updatedFlow.description,
data: updatedFlow.data,
name: updatedFlow.name,
};
}
return newFlows;
});
//update tabs state
@ -682,11 +644,35 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
}
function saveComponent(component: NodeDataType) {
component.node!.official = false;
return addFlow(true, createFlowComponent(component, version));
}
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 (
<TabsContext.Provider
<FlowsContext.Provider
value={{
version,
saveFlow,
isBuilt,
setIsBuilt,
@ -711,9 +697,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
getTweak,
setTweak,
isLoading,
saveComponent,
deleteComponent,
}}
>
{children}
</TabsContext.Provider>
</FlowsContext.Provider>
);
}

View file

@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext";
import { FlowsProvider } from "./flowsContext";
import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
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(true);
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,
@ -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);
});

View file

@ -13,7 +13,7 @@ import {
undoRedoContextType,
} from "../types/typesContext";
import { isWrappedWithClass } from "../utils/utils";
import { TabsContext } from "./tabsContext";
import { FlowsContext } from "./flowsContext";
const initialValue = {
undo: () => {},
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
export function UndoRedoProvider({ children }) {
const { tabId, flows } = useContext(TabsContext);
const { tabId, flows } = useContext(FlowsContext);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));

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) {
@ -522,3 +526,280 @@ 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}`);
};

View file

@ -0,0 +1,31 @@
const SvgAWS = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
id="Layer_1"
x={0}
y={0}
style={{
enableBackground: "new 0 0 304 182",
}}
viewBox="0 0 304 182"
{...props}
>
<style>{".st1{fill-rule:evenodd;clip-rule:evenodd;fill:#f90}"}</style>
<path
d="M86.4 66.4c0 3.7.4 6.7 1.1 8.9.8 2.2 1.8 4.6 3.2 7.2.5.8.7 1.6.7 2.3 0 1-.6 2-1.9 3L83.2 92c-.9.6-1.8.9-2.6.9-1 0-2-.5-3-1.4-1.4-1.5-2.6-3.1-3.6-4.7-1-1.7-2-3.6-3.1-5.9-7.8 9.2-17.6 13.8-29.4 13.8-8.4 0-15.1-2.4-20-7.2-4.9-4.8-7.4-11.2-7.4-19.2 0-8.5 3-15.4 9.1-20.6 6.1-5.2 14.2-7.8 24.5-7.8 3.4 0 6.9.3 10.6.8 3.7.5 7.5 1.3 11.5 2.2v-7.3c0-7.6-1.6-12.9-4.7-16-3.2-3.1-8.6-4.6-16.3-4.6-3.5 0-7.1.4-10.8 1.3-3.7.9-7.3 2-10.8 3.4-1.6.7-2.8 1.1-3.5 1.3-.7.2-1.2.3-1.6.3-1.4 0-2.1-1-2.1-3.1v-4.9c0-1.6.2-2.8.7-3.5.5-.7 1.4-1.4 2.8-2.1 3.5-1.8 7.7-3.3 12.6-4.5C41 1.9 46.2 1.3 51.7 1.3c11.9 0 20.6 2.7 26.2 8.1 5.5 5.4 8.3 13.6 8.3 24.6v32.4zM45.8 81.6c3.3 0 6.7-.6 10.3-1.8 3.6-1.2 6.8-3.4 9.5-6.4 1.6-1.9 2.8-4 3.4-6.4.6-2.4 1-5.3 1-8.7v-4.2c-2.9-.7-6-1.3-9.2-1.7-3.2-.4-6.3-.6-9.4-.6-6.7 0-11.6 1.3-14.9 4-3.3 2.7-4.9 6.5-4.9 11.5 0 4.7 1.2 8.2 3.7 10.6 2.4 2.5 5.9 3.7 10.5 3.7zm80.3 10.8c-1.8 0-3-.3-3.8-1-.8-.6-1.5-2-2.1-3.9L96.7 10.2c-.6-2-.9-3.3-.9-4 0-1.6.8-2.5 2.4-2.5h9.8c1.9 0 3.2.3 3.9 1 .8.6 1.4 2 2 3.9l16.8 66.2 15.6-66.2c.5-2 1.1-3.3 1.9-3.9.8-.6 2.2-1 4-1h8c1.9 0 3.2.3 4 1 .8.6 1.5 2 1.9 3.9l15.8 67 17.3-67c.6-2 1.3-3.3 2-3.9.8-.6 2.1-1 3.9-1h9.3c1.6 0 2.5.8 2.5 2.5 0 .5-.1 1-.2 1.6-.1.6-.3 1.4-.7 2.5l-24.1 77.3c-.6 2-1.3 3.3-2.1 3.9-.8.6-2.1 1-3.8 1h-8.6c-1.9 0-3.2-.3-4-1-.8-.7-1.5-2-1.9-4L156 23l-15.4 64.4c-.5 2-1.1 3.3-1.9 4-.8.7-2.2 1-4 1h-8.6zm128.5 2.7c-5.2 0-10.4-.6-15.4-1.8-5-1.2-8.9-2.5-11.5-4-1.6-.9-2.7-1.9-3.1-2.8-.4-.9-.6-1.9-.6-2.8v-5.1c0-2.1.8-3.1 2.3-3.1.6 0 1.2.1 1.8.3.6.2 1.5.6 2.5 1 3.4 1.5 7.1 2.7 11 3.5 4 .8 7.9 1.2 11.9 1.2 6.3 0 11.2-1.1 14.6-3.3 3.4-2.2 5.2-5.4 5.2-9.5 0-2.8-.9-5.1-2.7-7-1.8-1.9-5.2-3.6-10.1-5.2L246 52c-7.3-2.3-12.7-5.7-16-10.2-3.3-4.4-5-9.3-5-14.5 0-4.2.9-7.9 2.7-11.1 1.8-3.2 4.2-6 7.2-8.2 3-2.3 6.4-4 10.4-5.2 4-1.2 8.2-1.7 12.6-1.7 2.2 0 4.5.1 6.7.4 2.3.3 4.4.7 6.5 1.1 2 .5 3.9 1 5.7 1.6 1.8.6 3.2 1.2 4.2 1.8 1.4.8 2.4 1.6 3 2.5.6.8.9 1.9.9 3.3v4.7c0 2.1-.8 3.2-2.3 3.2-.8 0-2.1-.4-3.8-1.2-5.7-2.6-12.1-3.9-19.2-3.9-5.7 0-10.2.9-13.3 2.8-3.1 1.9-4.7 4.8-4.7 8.9 0 2.8 1 5.2 3 7.1 2 1.9 5.7 3.8 11 5.5l14.2 4.5c7.2 2.3 12.4 5.5 15.5 9.6 3.1 4.1 4.6 8.8 4.6 14 0 4.3-.9 8.2-2.6 11.6-1.8 3.4-4.2 6.4-7.3 8.8-3.1 2.5-6.8 4.3-11.1 5.6-4.5 1.4-9.2 2.1-14.3 2.1z"
style={{
fill: "#252f3e",
}}
/>
<path
d="M273.5 143.7c-32.9 24.3-80.7 37.2-121.8 37.2-57.6 0-109.5-21.3-148.7-56.7-3.1-2.8-.3-6.6 3.4-4.4 42.4 24.6 94.7 39.5 148.8 39.5 36.5 0 76.6-7.6 113.5-23.2 5.5-2.5 10.2 3.6 4.8 7.6z"
className="st1"
/>
<path
d="M287.2 128.1c-4.2-5.4-27.8-2.6-38.5-1.3-3.2.4-3.7-2.4-.8-4.5 18.8-13.2 49.7-9.4 53.3-5 3.6 4.5-1 35.4-18.6 50.2-2.7 2.3-5.3 1.1-4.1-1.9 4-9.9 12.9-32.2 8.7-37.5z"
className="st1"
/>
</svg>
);
export default SvgAWS;

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
<style type="text/css">
.st0{fill:#252F3E;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
</style>
<g>
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2
c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8
c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8
c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4
c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1
c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6
c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7
c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6
C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4
c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1
h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5
c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1
c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8
c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3
c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5
c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1
c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1
c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2
c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1
c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6
c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
<g>
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4
c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5
c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,8 @@
import React, { forwardRef } from "react";
import SvgAWS from "./AWS";
export const AWSIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgAWS ref={ref} {...props} />;
}
);

View file

@ -1,7 +1,7 @@
import { InfinityIcon } from "lucide-react";
import { forwardRef } from "react";
export const GradientSparkles = forwardRef<
export const GradientInfinity = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {

View file

@ -15,7 +15,7 @@ import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index";
@ -45,7 +45,7 @@ const ApiModal = forwardRef(
const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]);
const { setTweak, getTweak, tabsState } = useContext(TabsContext);
const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
const pythonApiCode = getPythonApiCode(
flow,
autoLogin,

View file

@ -1,14 +1,30 @@
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,
@ -17,13 +33,26 @@ export default function ConfirmationModal({
data,
index,
onConfirm,
size,
open,
onClose,
}: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false);
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 size={size ?? "x-small"} open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger asChild={asChild}>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<span className="pr-2">{title}</span>
<Icon
@ -39,14 +68,14 @@ export default function ConfirmationModal({
<br></br>
</>
)}
<span>{modalContent}</span>
{ContentChild}
</BaseModal.Content>
<BaseModal.Footer>
<Button
className="ml-3"
className="ml-3 mt-5"
onClick={() => {
setOpen(false);
setModalOpen(false);
onConfirm(index, data);
}}
>
@ -54,9 +83,10 @@ export default function ConfirmationModal({
</Button>
<Button
className="mt-5"
variant="outline"
onClick={() => {
setOpen(false);
setModalOpen(false);
}}
>
{cancelText}
@ -65,3 +95,7 @@ export default function ConfirmationModal({
</BaseModal>
);
}
ConfirmationModal.Content = Content;
ConfirmationModal.Trigger = Trigger;
export default ConfirmationModal;

View file

@ -0,0 +1,60 @@
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>{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">Cancel</Button>
<Button
type="submit"
variant="destructive"
onClick={() => {
onConfirm();
}}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -1,12 +1,6 @@
import { cloneDeep } from "lodash";
import {
ReactNode,
forwardRef,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { forwardRef, useContext, useEffect, useRef, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
@ -32,10 +26,10 @@ import {
TableRow,
} from "../../components/ui/table";
import { limitScrollFieldsModal } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs";
import { FlowsState } from "../../types/tabs";
import {
convertObjToArray,
convertValuesToNumbers,
@ -49,23 +43,21 @@ const EditNodeModal = forwardRef(
{
data,
nodeLength,
children,
open,
onClose,
setOpen,
}: {
data: NodeDataType;
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(TabsContext);
const { setTabsState, tabId } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
let disabled =
reactFlowInstance
@ -73,39 +65,48 @@ const EditNodeModal = forwardRef(
.some((edge) => edge.targetHandle === data.id) ?? false;
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(() => {
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
}, [modalOpen]);
if (open) {
setMyData(data); // reset data to what it is on node when opening modal
}
}, [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">
@ -138,28 +139,28 @@ 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]
myData.node?.template[templateParam].show &&
(myData.node.template[templateParam]
.type === "str" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "bool" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "float" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "code" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "prompt" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "file" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "int" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "dict" ||
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.type === "NestedDict")
)
.map((templateParam, index) => (
@ -167,46 +168,46 @@ const EditNodeModal = forwardRef(
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
content={
myData.current.node?.template[templateParam]
myData.node?.template[templateParam]
.proxy
? myData.current.node?.template[
? myData.node?.template[
templateParam
].proxy?.id
: null
}
>
<span>
{myData.current.node?.template[templateParam]
{myData.node?.template[templateParam]
.display_name
? myData.current.node.template[
? myData.node.template[
templateParam
].display_name
: myData.current.node?.template[
: myData.node?.template[
templateParam
].name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam]
{myData.node?.template[templateParam]
.type === "str" &&
!myData.current.node.template[templateParam]
!myData.node.template[templateParam]
.options ? (
<div className="mx-auto">
{myData.current.node.template[templateParam]
{myData.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.current.node.template[
!myData.node.template[
templateParam
].value ||
myData.current.node.template[
myData.node.template[
templateParam
].value === ""
? [""]
: myData.current.node.template[
: myData.node.template[
templateParam
].value
}
@ -214,14 +215,15 @@ const EditNodeModal = forwardRef(
handleOnNewValue(value, templateParam);
}}
/>
) : myData.current.node.template[
) : myData.node.template[
templateParam
].multiline ? (
<TextAreaComponent
id={"textarea-edit-" + index}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
@ -231,15 +233,16 @@ const EditNodeModal = forwardRef(
/>
) : (
<InputComponent
id={"input-" + index}
editNode={true}
disabled={disabled}
password={
myData.current.node.template[
myData.node.template[
templateParam
].password ?? false
}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
@ -249,57 +252,55 @@ const EditNodeModal = forwardRef(
/>
)}
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
myData.node!.template[
templateParam
].value.toString() === "{}"
? {
yourkey: "value",
}
: myData.current.node!.template[
: myData.node!.template[
templateParam
].value
}
onChange={(newValue) => {
myData.current.node!.template[
myData.node!.template[
templateParam
].value = newValue;
handleOnNewValue(newValue, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.current.node!.template[templateParam]
myData.node!.template[templateParam]
.value?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
dataValue={dataValue}
advanced={adv}
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
myData.node!.template[
templateParam
].value?.length === 0 ||
!myData.current.node!.template[
!myData.node!.template[
templateParam
].value
? [{ "": "" }]
: convertObjToArray(
myData.current.node!.template[
myData.node!.template[
templateParam
].value
)
@ -308,7 +309,7 @@ const EditNodeModal = forwardRef(
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.current.node!.template[
myData.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
@ -321,14 +322,15 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled}
enabled={
myData.current.node.template[
myData.node.template[
templateParam
].value
}
@ -341,14 +343,14 @@ const EditNodeModal = forwardRef(
size="small"
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
@ -357,16 +359,16 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "str" &&
myData.current.node.template[templateParam]
myData.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.current.node.template[
myData.node.template[
templateParam
].options
}
@ -374,20 +376,21 @@ const EditNodeModal = forwardRef(
handleOnNewValue(value, templateParam)
}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? "Choose an option"
}
></Dropdown>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
id={"int-input-" + index}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
@ -396,14 +399,14 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
@ -411,12 +414,12 @@ const EditNodeModal = forwardRef(
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.current.node.template[
myData.node.template[
templateParam
].fileTypes
}
suffixes={
myData.current.node.template[
myData.node.template[
templateParam
].suffixes
}
@ -427,42 +430,38 @@ const EditNodeModal = forwardRef(
}}
></InputFileComponent>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
myData.node?.flow ? true : false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.current.node}
nodeClass={myData.node}
setNodeClass={(nodeClass) => {
myData.current.node = nodeClass;
myData.node = nodeClass;
}}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"prompt-area-edit" + index}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
myData.node?.flow &&
myData.node.template[
templateParam
].dynamic
? true
@ -479,16 +478,17 @@ const EditNodeModal = forwardRef(
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
myData.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"code-area-edit" + index}
/>
</div>
) : myData.current.node?.template[templateParam]
) : myData.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
@ -500,11 +500,11 @@ const EditNodeModal = forwardRef(
<ToggleShadComponent
id={
"show" +
myData.current.node?.template[templateParam]
myData.node?.template[templateParam]
.name
}
enabled={
!myData.current.node?.template[
!myData.node?.template[
templateParam
].advanced
}
@ -531,10 +531,9 @@ 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: TabsState) => {
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
@ -543,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;
@ -104,6 +105,10 @@ function BaseModal({
minWidth = "min-w-[40vw]";
height = "h-[27vh]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
height = "h-[40vh]";

View file

@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
import AceEditor from "react-ace";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext";
@ -144,6 +145,11 @@ export default function CodeAreaModal({
/>
</BaseModal.Header>
<BaseModal.Content>
<Input
value={code}
className="absolute left-[500%] top-[500%]"
id="codeValue"
/>
<div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full">
<AceEditor
@ -183,10 +189,11 @@ export default function CodeAreaModal({
</div>
<div className="flex h-fit w-full justify-end">
<Button
disabled={readonly}
className="mt-3"
onClick={handleClick}
type="submit"
id="checkAndSaveBtn"
disabled={readonly}
>
Check & Save
</Button>

View file

@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(TabsContext);
const [checked, setChecked] = useState(false);
const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -22,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>
@ -36,14 +40,13 @@ const ExportModal = forwardRef(
<EditFlowSettings
name={name}
description={description}
flows={flows}
tabId={tabId}
setName={setName}
setDescription={setDescription}
/>
<div className="mt-3 flex items-center space-x-2">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(event: boolean) => {
setChecked(event);
}}
@ -52,20 +55,42 @@ const ExportModal = forwardRef(
Save with my API keys
</label>
</div>
<span className=" text-xs text-destructive ">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<Button
onClick={() => {
if (checked)
if (checked) {
downloadFlow(
flows.find((flow) => flow.id === tabId)!,
{
id: tabId,
data: reactFlowInstance?.toObject()!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!,
description
);
else
setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
});
} else
downloadFlow(
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
removeApiKeys({
id: tabId,
data: reactFlowInstance?.toObject()!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!,
description
);

View file

@ -3,15 +3,17 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { readFlowsFromDatabase } from "../../controllers/API";
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(TabsContext);
const { flows, tabId, updateFlow, saveFlow } = useContext(FlowsContext);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -19,7 +21,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 +29,18 @@ export default function FlowSettingsModal({
saveFlow(savedFlow!);
setOpen(false);
}
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
readFlowsFromDatabase().then((flows) => {
flows.forEach((flow: FlowType) => {
tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== flow!.name));
});
}, []);
return (
<BaseModal open={open} setOpen={setOpen} size="smaller">
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
@ -36,19 +49,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

@ -24,9 +24,9 @@ import {
import { Textarea } from "../../components/ui/textarea";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API";
import { TabsState } from "../../types/tabs";
import { FlowsState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils";
export default function FormModal({
@ -38,7 +38,7 @@ export default function FormModal({
setOpen: (open: boolean) => void;
flow: FlowType;
}): JSX.Element {
const { tabsState, setTabsState } = useContext(TabsContext);
const { tabsState, setTabsState } = useContext(FlowsContext);
const [chatValue, setChatValue] = useState(() => {
try {
const { formKeysData } = tabsState[flow.id];
@ -401,7 +401,7 @@ export default function FormModal({
chatKey: chatKey!,
});
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
if (!chatKey) return old;
let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![chatKey] = "";
@ -522,7 +522,7 @@ export default function FormModal({
}
onChange={(e) => {
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old);
newTabsState[
id.current
@ -634,7 +634,7 @@ export default function FormModal({
setChatValue={(value) => {
setChatValue(value);
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![
chatKey!

View file

@ -30,6 +30,7 @@ export default function GenericModal({
nodeClass,
setNodeClass,
children,
id = "",
readonly = false,
}: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText);
@ -211,6 +212,7 @@ export default function GenericModal({
>
{type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea
id={"modal-" + id}
ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue}
@ -286,7 +288,7 @@ export default function GenericModal({
className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm"
>
<div className="relative bottom-[1px]">
<span>
<span id={"badge" + index.toString()}>
{word.replace(/[{}]/g, "").length > 59
? word.replace(/[{}]/g, "").slice(0, 56) +
"..."
@ -306,6 +308,7 @@ export default function GenericModal({
)}
</div>
<Button
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {
switch (myModalType) {

View file

@ -0,0 +1,192 @@
import { ReactNode, useContext, useEffect, 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 {
getStoreComponents,
getStoreTags,
saveFlowStore,
} from "../../controllers/API";
import { FlowType } from "../../types/flow";
import { removeApiKeys } from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import BaseModal from "../baseModal";
export default function ShareModal({
component,
is_component,
children,
open,
setOpen,
}: {
children?: ReactNode;
is_component: boolean;
component: FlowType;
open?: boolean;
setOpen?: (open: boolean) => void;
}): JSX.Element {
const { version, addFlow } = useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const [name, setName] = useState(component?.name ?? "");
const [description, setDescription] = useState(component?.description ?? "");
const [internalOpen, internalSetOpen] = useState(children ? false : true);
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<string[]>([]);
useEffect(() => {
if (open || internalOpen) {
handleGetTags();
handleGetNames();
}
}, [open, internalOpen]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags().then((res) => {
setTags(res);
setLoadingTags(false);
});
}
function handleGetNames() {
const unavaliableNames: Array<string> = [];
getStoreComponents({ fields: ["name"], filterByUser: true }).then((res) => {
res?.results?.forEach((element: any) => {
unavaliableNames.push(element.name);
});
console.log(unavaliableNames);
setUnavaliableNames(unavaliableNames);
});
}
useEffect(() => {
setName(component?.name ?? "");
setDescription(component?.description ?? "");
}, [component]);
const handleShareComponent = () => {
const saveFlow: 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,
});
saveFlowStore(saveFlow, getTagsIds(selectedTags, tags), sharePublic).then(
() => {
if (is_component) {
addFlow(true, saveFlow);
}
setSuccessData({
title: `${nameComponent} shared successfully`,
});
},
(err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
);
};
return (
<BaseModal
size="smaller-h-full"
open={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}
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);
}}
/>
<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);
}}
/>
<label htmlFor="terms" className="export-modal-save-api text-sm ">
Save with my API keys
</label>
</div>
<span className=" text-xs text-destructive ">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<Button
disabled={unavaliableNames.includes(name)}
onClick={() => {
handleShareComponent();
if (setOpen) setOpen(false);
else internalSetOpen(false);
}}
type="button"
>
{is_component ? "Save and " : ""}Share {!is_component ? "Flow" : ""}
</Button>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -22,7 +22,7 @@ import {
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import {
addUser,
deleteUser,
@ -44,7 +44,7 @@ export default function AdminPage() {
const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext);
const { setTabId } = useContext(FlowsContext);
// set null id
useEffect(() => {
@ -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 { TabsContext } from "../../contexts/tabsContext";
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(TabsContext);
// 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

@ -8,7 +8,7 @@ export default function DisclosureComponent({
openDisc,
}: DisclosureComponentType): JSX.Element {
return (
<Disclosure as="div" key={title}>
<Disclosure as="div" defaultOpen={openDisc} key={title}>
{({ open }) => (
<>
<div>
@ -35,9 +35,7 @@ export default function DisclosureComponent({
</div>
</Disclosure.Button>
</div>
<Disclosure.Panel as="div" static={openDisc}>
{children}
</Disclosure.Panel>
<Disclosure.Panel as="div">{children}</Disclosure.Panel>
</>
)}
</Disclosure>

View file

@ -28,13 +28,13 @@ import ReactFlow, {
import GenericNode from "../../../../CustomNodes/GenericNode";
import Chat from "../../../../components/chatComponent";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { locationContext } from "../../../../contexts/locationContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs";
import { FlowsState } from "../../../../types/tabs";
import {
generateFlow,
generateNodeFromFlow,
@ -70,7 +70,7 @@ export default function Page({
saveFlow,
setTabsState,
tabId,
} = useContext(TabsContext);
} = useContext(FlowsContext);
const {
types,
reactFlowInstance,
@ -187,7 +187,15 @@ export default function Page({
let updatedSeconds = prevSeconds + 1;
if (updatedSeconds % 30 === 0) {
saveFlow(flow, true);
saveFlow(
{
...flow!,
data: reactFlowInstance
? reactFlowInstance!.toObject()
: flow!.data,
},
true
);
updatedSeconds = 0;
}
@ -208,7 +216,7 @@ export default function Page({
return newX;
});
//@ts-ignore
setTabsState((prev: TabsState) => {
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
@ -225,7 +233,7 @@ export default function Page({
(change: NodeChange[]) => {
onNodesChange(change);
//@ts-ignore
setTabsState((prev: TabsState) => {
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
@ -310,7 +318,6 @@ export default function Page({
event.dataTransfer.getData("nodedata")
);
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created
const position = reactFlowInstance!.project({
x: event.clientX - reactflowBounds!.left,
@ -351,7 +358,16 @@ 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)!);
uploadFlow({
newProject: false,
isComponent: false,
file: event.dataTransfer.files.item(0)!,
}).catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
@ -367,7 +383,10 @@ export default function Page({
useEffect(() => {
return () => {
if (tabsState && tabsState[flow.id]?.isPending) {
saveFlow(flow);
saveFlow({
...flow!,
data: reactFlowInstance ? reactFlowInstance!.toObject() : flow!.data,
});
}
};
}, []);

View file

@ -1,29 +1,35 @@
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 { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
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 } =
useContext(TabsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
useContext(FlowsContext);
const { setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const isPending = tabsState[tabId]?.isPending;
@ -52,8 +58,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 +84,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("");
@ -91,6 +100,47 @@ export default function ExtraSidebar(): JSX.Element {
}
}, [getFilterEdge]);
useEffect(() => {
handleSearchInput(search);
}, [data]);
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]);
useEffect(() => {
if (getFilterEdge?.length > 0) {
setFilterData((_) => {
@ -122,11 +172,37 @@ export default function ExtraSidebar(): JSX.Element {
}
}
});
setSearch("search");
setSearch("");
return ret;
});
}
}, [getFilterEdge]);
}, [getFilterEdge, data]);
const ModalMemo = useMemo(
() => (
<ShareModal is_component={false} component={flow!}>
<ShadTooltip content="Share" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="Share2" className="side-bar-button-size" />
</div>
</ShadTooltip>
</ShareModal>
),
[]
);
const ExportMemo = useMemo(
() => (
<ExportModal>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</div>
</ShadTooltip>
</ExportModal>
),
[]
);
return (
<div className="side-bar-arrangement">
@ -136,25 +212,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 && (
@ -178,24 +250,34 @@ export default function ExtraSidebar(): JSX.Element {
</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"
<div>
<button
disabled={flow?.data?.nodes.length === 0}
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
"extra-side-bar-buttons " +
(isPending && flow!.data!.nodes?.length > 0
? ""
: "button-disable")
}
/>
</button>
onClick={(event) => {
saveFlow({ ...flow!, data: reactFlowInstance!.toObject() });
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending && flow!.data!.nodes?.length > 0
? " "
: " extra-side-bar-save-disable")
}
/>
</button>
</div>
</ShadTooltip>
</div>
<div className="side-bar-button">{ModalMemo}</div>
</div>
<Separator />
<div className="side-bar-search-div-placement">
@ -227,8 +309,12 @@ export default function ExtraSidebar(): JSX.Element {
.map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<DisclosureComponent
openDisc={search.length == 0 ? false : true}
key={index}
openDisc={
getFilterEdge.length !== 0 || search.length !== 0
? true
: false
}
key={index + search + JSON.stringify(getFilterEdge)}
button={{
title: nodeNames[SBSectionName] ?? nodeNames.unknown,
Icon:
@ -237,57 +323,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={SBItemName}
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,130 @@
import { DragEventHandler, useContext, useRef } 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 = useRef(false);
const { getNodeId, deleteComponent, version } = useContext(FlowsContext);
const { autoLogin, userData } = useContext(AuthContext);
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) => (open.current = change)}
open={open.current}
>
<div
onContextMenuCapture={(e) => {
e.preventDefault();
open.current = 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
id={sectionName + display_name}
className="side-bar-components-div-form"
>
<span className="side-bar-components-text">{display_name}</span>
<div>
<SelectTrigger>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
</SelectTrigger>
<SelectContent>
<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";
@ -8,14 +9,18 @@ import {
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select-custom";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
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,
@ -37,10 +42,13 @@ 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);
function canMinimize() {
let countHandles: number = 0;
@ -53,27 +61,42 @@ export default function NodeToolbarComponent({
const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false;
const { paste } = useContext(TabsContext);
const { paste, saveComponent, version } = useContext(FlowsContext);
const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState("");
const [flowComponent, setFlowComponent] = useState<FlowType>();
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [data]);
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":
setShowNode((prev) => !prev);
updateNodeInternals(data.id);
break;
case "Download":
downloadNode(createFlowComponent(cloneDeep(data), version));
break;
case "Share":
setShowconfirmShare(true);
break;
case "SaveAll":
saveComponent(cloneDeep(data));
break;
case "disabled":
break;
case "ungroup":
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance, getNodeId);
break;
}
};
@ -144,43 +167,31 @@ 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" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<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">
<IconComponent
name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4"
@ -188,61 +199,71 @@ 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}
nodeLength={nodeLength}
open={showModalAdvanced}
onClose={(modal) => {
setShowModalAdvanced(modal);
}}
>
<></>
</EditNodeModal>
)}
<SelectItem value={"SaveAll"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
<SelectItem 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="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
<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 { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API";
import { FlowsContext } from "../../contexts/flowsContext";
import Page from "./components/PageComponent";
export default function FlowPage(): JSX.Element {
const { flows, tabId, setTabId } = useContext(TabsContext);
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 />
@ -29,7 +20,7 @@ export default function FlowPage(): JSX.Element {
{flows.length > 0 &&
tabId !== "" &&
flows.findIndex((flow) => flow.id === tabId) !== -1 && (
<Page flow={flows.find((flow) => flow.id === tabId)!} />
<Page key={id} flow={flows.find((flow) => flow.id === tabId)!} />
)}
<a
target={"_blank"}

View file

@ -0,0 +1,158 @@
import { useContext, useEffect, useState } from "react";
import { 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, isLoading } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(1);
const [allData, setAllData] = useState(flows);
const navigate = useNavigate();
useEffect(() => {
setAllData(
flows
.filter((f) => f.is_component === 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()
);
}
})
);
}, [flows]);
useEffect(() => {
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
setData(allData.slice(start, end));
}, [pageIndex, pageSize, allData]);
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"],
});
}
}
};
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">
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{!isLoading || data?.length > 0 ? (
data?.map((item, idx) => (
<CollectionCardComponent
onDelete={() => {
removeFlow(item.id);
}}
key={idx}
data={item}
disabled={isLoading}
button={
!is_component ? (
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
onClick={() => {
navigate("/flow/" + item.id);
}}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
) : (
<></>
)
}
/>
))
) : !isLoading && data?.length === 0 ? (
<>You haven't created {name}s yet.</>
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
</div>
{!isLoading && allData.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={allData.length}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
);
}

View file

@ -1,180 +1,108 @@
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
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 { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
export default function HomePage(): JSX.Element {
const {
flows,
setTabId,
downloadFlows,
uploadFlows,
addFlow,
removeFlow,
uploadFlow,
isLoading,
} = useContext(TabsContext);
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",
},
{
title: "Components",
href: "/components",
},
];
// 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

@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { resetPassword, updateUser } from "../../controllers/API";
import {
inputHandlerEventType,
@ -17,7 +17,7 @@ import {
} from "../../types/components";
import { gradients } from "../../utils/styleUtils";
export default function ProfileSettingsPage(): JSX.Element {
const { setTabId } = useContext(TabsContext);
const { setTabId } = useContext(FlowsContext);
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE

View file

@ -0,0 +1,395 @@
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("");
}, []);
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={() => {
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={() => {
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 value="createdbyme">Created By Me</SelectItem>
<SelectItem 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,11 +1,10 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API";
import { FlowsContext } from "../../contexts/flowsContext";
import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() {
const { flows, tabId, setTabId } = useContext(TabsContext);
const { flows, tabId, setTabId } = useContext(FlowsContext);
const { id } = useParams();
// Set flow tab id
@ -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;
@ -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 justify-between h-full flex-col 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 {
@ -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,32 @@
.input-invalid {
@apply border-destructive focus:border-destructive focus:ring-destructive;
}
.fade-container {
@apply relative overflow-hidden;
}
.fade-container::before,
.fade-container::after {
@apply absolute top-0 bottom-0 pointer-events-none;
content: '';
width: 50px;
opacity: 0;
transition: opacity 0.3s;
background: linear-gradient(to right, white, transparent);
}
.fade-container::after {
@apply right-0;
transform: rotate(180deg);
}
.fade-container.fade-left::before,
.fade-container.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: #4338ca;
--flow-icon: #0f766e;
/* 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: #352e9e;
--flow-icon: #115e59;
/* 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>
@ -47,6 +48,8 @@ export type TemplateVariableType = {
dynamic?: boolean;
proxy?: { id: string; field: string };
input_types?: Array<string>;
display_name?: string;
name?: string;
[key: string]: any;
};
export type sendAllProps = {
@ -118,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

@ -19,6 +19,7 @@ export type InputComponentType = {
showPass?: boolean;
placeholder?: string;
className?: string;
id?: string;
blurOnEnter?: boolean;
};
export type ToggleComponentType = {
@ -51,6 +52,7 @@ export type ParameterComponentType = {
info?: string;
proxy?: { field: string; id: string };
showNode?: boolean;
index?: string;
};
export type InputListComponentType = {
value: string[];
@ -65,8 +67,6 @@ export type KeyPairListComponentType = {
disabled: boolean;
editNode?: boolean;
duplicateKey?: boolean;
advanced?: boolean | null;
dataValue?: any;
};
export type DictComponentType = {
@ -84,6 +84,7 @@ export type TextAreaComponentType = {
onChange: (value: string[] | string) => void;
value: string;
editNode?: boolean;
id?: string;
readonly?: boolean;
};
@ -96,6 +97,7 @@ export type PromptAreaComponentType = {
value: string;
readonly?: boolean;
editNode?: boolean;
id?: string;
};
export type CodeAreaComponentType = {
@ -106,6 +108,7 @@ export type CodeAreaComponentType = {
nodeClass?: APIClassType;
setNodeClass?: (value: APIClassType) => void;
dynamic?: boolean;
id?: string;
readonly?: boolean;
};
@ -137,6 +140,7 @@ export type FloatComponentType = {
disabled?: boolean;
onChange: (value: string) => void;
editNode?: boolean;
id?: string;
};
export type TooltipComponentType = {
@ -209,18 +213,16 @@ export type IconComponentProps = {
className?: string;
iconColor?: string;
onClick?: () => void;
stroke?: 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 = {
@ -239,8 +241,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;
@ -254,7 +263,7 @@ export type signUpInputStateType = {
export type inputHandlerEventType = {
target: {
value: string | boolean;
value: string;
name: string;
};
};
@ -264,21 +273,35 @@ export type PaginatorComponentType = {
rowsCount?: number[];
totalRowsCount: number;
paginate: (pageIndex: number, pageSize: number) => void;
storeComponent?: boolean;
};
export type ConfirmationModalType = {
title: string;
titleHeader: string;
asChild?: boolean;
modalContent: string;
modalContentTitle: string;
cancelText: string;
confirmationText: string;
children: ReactElement;
children: [
React.ReactElement<ContentProps>,
React.ReactElement<TriggerProps>
];
icon: string;
data: any;
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 = {
@ -325,14 +348,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;
};
@ -493,6 +522,7 @@ export type genericModalPropsType = {
nodeClass?: APIClassType;
setNodeClass?: (Class: APIClassType) => void;
children: ReactNode;
id?: string;
readonly?: boolean;
};
@ -517,8 +547,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;

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,7 +1,7 @@
import { tweakType } from "../components";
import { FlowType } from "../flow";
import { FlowType, NodeDataType } from "../flow";
export type TabsContextType = {
export type FlowsContextType = {
saveFlow: (flow: FlowType, silent?: boolean) => Promise<void>;
tabId: string;
isLoading: boolean;
@ -23,11 +23,19 @@ export type TabsContextType = {
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
uploadFlow: ({
newProject,
file,
isComponent,
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
}) => Promise<String | never>;
hardReset: () => void;
getNodeId: (nodeType: string) => string;
tabsState: TabsState;
setTabsState: (state: TabsState) => void;
tabsState: FlowsState;
setTabsState: (state: FlowsState) => void;
paste: (
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
@ -36,9 +44,12 @@ export type TabsContextType = {
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
setTweak: (tweak: tweakType) => tweakType | void;
getTweak: tweakType;
saveComponent: (component: NodeDataType) => Promise<String | undefined>;
deleteComponent: (key: string) => void;
version: string;
};
export type TabsState = {
export type FlowsState = {
[key: string]: {
isPending: boolean;
formKeysData: {

View file

@ -25,8 +25,7 @@ import {
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { toNormalCase, toTitleCase } from "./utils";
const uid = new ShortUniqueId({ length: 5 });
import { getFieldTitle, toTitleCase } from "./utils";
export function cleanEdges({
flow: { edges, nodes },
@ -164,46 +163,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) {
@ -240,11 +244,7 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
node.id
)
) {
errors.push(
`${type} is missing ${
template.display_name || toNormalCase(template[t].name)
}.`
);
errors.push(`${type} is missing ${getFieldTitle(template, t)}.`);
} else if (
template[t].type === "dict" &&
template[t].required &&
@ -255,15 +255,14 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
) {
if (hasDuplicateKeys(template[t].value))
errors.push(
`${type} (${
template.display_name || template[t].name
}) contains duplicate keys with the same values.`
`${type} (${getFieldTitle(
template,
t
)}) contains duplicate keys with the same values.`
);
if (hasEmptyKey(template[t].value))
errors.push(
`${type} (${
template.display_name || template[t].name
}) field must not be empty.`
`${type} (${getFieldTitle(template, t)}) field must not be empty.`
);
}
return errors;
@ -436,8 +435,8 @@ export function convertValuesToNumbers(arr) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
if (/\s/g.test(value)) {
value = value.trim();
if (/^\d+$/.test(value)) {
value = value?.toString().trim();
}
newObj[key] =
value === "" || isNaN(value) ? value.toString() : Number(value);
@ -512,7 +511,7 @@ export function generateFlow(
name: string
): generateFlowType {
const newFlowData = reactFlowInstance.toObject();
const uid = new ShortUniqueId({ length: 5 });
/* remove edges that are not connected to selected nodes on both ends
in future we can save this edges to when ungrouping reconect to the old nodes
*/
@ -525,6 +524,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
@ -702,7 +702,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(
@ -875,7 +874,7 @@ export function ungroupNode(
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let display_name: string;
let display_name: string | undefined;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
@ -906,16 +905,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;
//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;
@ -964,7 +990,7 @@ export function expandGroupNode(
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let proxy: { id: string; field: string } | undefined;
let display_name: string;
let display_name: string | undefined;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
@ -980,7 +1006,8 @@ export function expandGroupNode(
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
gNodes[nodeIndex].selected = false;
// keep the nodes selected after ungrouping
// gNodes[nodeIndex].selected = false;
if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else {
@ -1033,3 +1060,45 @@ 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
) {}

View file

@ -0,0 +1,25 @@
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 }[]
) {
console.log(tags);
console.log(tagListId);
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,
@ -31,11 +33,14 @@ import {
FileText,
FileUp,
Fingerprint,
FolderPlus,
Gift,
GitBranchPlus,
GitFork,
GithubIcon,
Group,
Hammer,
Heart,
HelpCircle,
Home,
Info,
@ -44,7 +49,9 @@ import {
Layers,
Lightbulb,
Link,
Loader2,
Lock,
LogIn,
LucideSend,
Maximize2,
Menu,
@ -55,20 +62,25 @@ import {
Minus,
MoonIcon,
MoreHorizontal,
Network,
Paperclip,
Pencil,
Plus,
Redo,
Rocket,
Save,
SaveAll,
Scissors,
Search,
Settings2,
Share2,
Shield,
Sparkles,
Square,
Store,
SunIcon,
TerminalSquare,
ToyBrick,
Trash2,
Undo,
Ungroup,
@ -81,12 +93,14 @@ import {
Users2,
Variable,
Wand2,
Workflow,
Wrench,
X,
XCircle,
Zap,
} from "lucide-react";
import { FaApple, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS";
import { AirbyteIcon } from "../icons/Airbyte";
import { AnthropicIcon } from "../icons/Anthropic";
import { BingIcon } from "../icons/Bing";
@ -96,7 +110,7 @@ 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 } from "../icons/GradientSparkles";
import { HuggingFaceIcon } from "../icons/HuggingFace";
import { IFixIcon } from "../icons/IFixIt";
import { MetaIcon } from "../icons/Meta";
@ -156,6 +170,7 @@ export const nodeColors: { [char: string]: string } = {
agents: "#903BBE",
tools: "#FF3434",
memories: "#F5B85A",
saved_components: "#a5B85A",
advanced: "#000000",
chat: "#198BF6",
thought: "#272541",
@ -180,6 +195,7 @@ export const nodeNames: { [char: string]: string } = {
agents: "Agents",
tools: "Tools",
memories: "Memories",
saved_components: "Saved Components",
advanced: "Advanced",
chat: "Chat",
embeddings: "Embeddings",
@ -199,6 +215,7 @@ export const nodeIconsLucide: iconsType = {
ArrowUpToLine: ArrowUpToLine,
Chroma: ChromaIcon,
AirbyteJSONLoader: AirbyteIcon,
AmazonBedrockEmbeddings: AWSIcon,
Anthropic: AnthropicIcon,
ChatAnthropic: AnthropicIcon,
BingSearchAPIWrapper: BingIcon,
@ -231,6 +248,7 @@ export const nodeIconsLucide: iconsType = {
ChatVertexAI: VertexAIIcon,
VertexAIEmbeddings: VertexAIIcon,
agents: Rocket,
Workflow,
User,
WikipediaAPIWrapper: SvgWikipedia,
chains: Link,
@ -241,6 +259,7 @@ export const nodeIconsLucide: iconsType = {
advanced: Laptop2,
chat: MessageCircle,
embeddings: Fingerprint,
saved_components: Save,
documentloaders: Paperclip,
vectorstores: Layers,
toolkits: Hammer,
@ -253,9 +272,11 @@ export const nodeIconsLucide: iconsType = {
unknown: HelpCircle,
WikipediaQueryRun: SvgWikipedia,
WolframAlphaQueryRun: SvgWolfram,
custom_components: GradientSparkles,
custom_components: GradientInfinity,
custom: Edit,
Trash2,
Boxes,
Network,
X,
XCircle,
Info,
@ -283,6 +304,7 @@ export const nodeIconsLucide: iconsType = {
Clipboard,
Code2,
Variable,
Store,
Download,
Eraser,
Lock,
@ -291,6 +313,7 @@ export const nodeIconsLucide: iconsType = {
DownloadCloud,
File,
FileText,
FolderPlus,
GitFork,
GithubIcon,
FileDown,
@ -315,6 +338,7 @@ export const nodeIconsLucide: iconsType = {
Key,
Unplug,
Group,
LogIn,
ChevronUp,
Ungroup,
BookMarked,
@ -322,4 +346,12 @@ export const nodeIconsLucide: iconsType = {
Square,
Minimize2,
Maximize2,
SaveAll,
Share2,
GitBranchPlus,
Loader2,
BookmarkPlus,
Heart,
Link,
ToyBrick,
};

View file

@ -1,14 +1,19 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "../flow_constants";
import { APIDataType, TemplateVariableType } from "../types/api";
import {
APIDataType,
APITemplateType,
TemplateVariableType,
} from "../types/api";
import {
IVarHighlightType,
groupedObjType,
nodeGroupedObjType,
tweakType,
} from "../types/components";
import { FlowType, NodeType } from "../types/flow";
import { TabsState } from "../types/tabs";
import { FlowsState } from "../types/tabs";
import { buildTweaks } from "./reactflowUtils";
export function classNames(...classes: Array<string>): string {
@ -53,7 +58,8 @@ export function normalCaseToSnakeCase(str: string): string {
.join("_");
}
export function toTitleCase(str: string): string {
export function toTitleCase(str: string | undefined): string {
if (!str) return "";
let result = str
.split("_")
.map((word, index) => {
@ -102,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([
@ -121,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:
@ -143,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(
@ -161,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,
@ -187,15 +205,19 @@ 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(", "),
}));
}
export function buildInputs(tabsState: TabsState, id: string): string {
export function buildInputs(tabsState: FlowsState, id: string): string {
return tabsState &&
tabsState[id] &&
tabsState[id].formKeysData &&
@ -273,10 +295,10 @@ export function buildTweakObject(tweak: tweakType) {
/**
* Function to get Chat Input Field
* @param {FlowType} flow - The current flow.
* @param {TabsState} tabsState - The current tabs state.
* @param {FlowsState} tabsState - The current tabs state.
* @returns {string} - The chat input field
*/
export function getChatInputField(flow: FlowType, tabsState?: TabsState) {
export function getChatInputField(flow: FlowType, tabsState?: FlowsState) {
let chat_input_field = "text";
if (
@ -301,7 +323,7 @@ export function getPythonApiCode(
flow: FlowType,
isAuth: boolean,
tweak?: any[],
tabsState?: TabsState
tabsState?: FlowsState
): string {
const flowId = flow.id;
@ -365,7 +387,7 @@ export function getCurlCode(
flow: FlowType,
isAuth: boolean,
tweak?: any[],
tabsState?: TabsState
tabsState?: FlowsState
): string {
const flowId = flow.id;
const tweaks = buildTweaks(flow);
@ -393,7 +415,7 @@ export function getCurlCode(
export function getPythonCode(
flow: FlowType,
tweak?: any[],
tabsState?: TabsState
tabsState?: FlowsState
): string {
const flowName = flow.name;
const tweaks = buildTweaks(flow);
@ -418,7 +440,7 @@ flow(inputs)`;
export function getWidgetCode(
flow: FlowType,
isAuth: boolean,
tabsState?: TabsState
tabsState?: FlowsState
): string {
const flowId = flow.id;
const flowName = flow.name;
@ -550,3 +572,85 @@ 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
): string {
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name
? toTitleCase(template[templateField].name!)
: toTitleCase(templateField);
}

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

@ -0,0 +1,140 @@
import { expect, test } from "@playwright/test";
test("CodeAreaModalComponent", 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("pythonfunctiontool");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sidePythonFunctionTool"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="code-input-0"]').click();
let value = await page.locator('//*[@id="codeValue"]').inputValue();
if (
value !=
'def python_function(text: str) -> str: """This is a default python function that returns the input text""" return text'
) {
expect(false).toBeTruthy();
}
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.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showdescription"]').click();
expect(
await page.locator('//*[@id="showdescription"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showname"]').click();
expect(await page.locator('//*[@id="showname"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showreturn_direct"]').click();
expect(
await page.locator('//*[@id="showreturn_direct"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showdescription"]').click();
expect(
await page.locator('//*[@id="showdescription"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showname"]').click();
expect(await page.locator('//*[@id="showname"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showreturn_direct"]').click();
expect(
await page.locator('//*[@id="showreturn_direct"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showdescription"]').click();
expect(
await page.locator('//*[@id="showdescription"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showname"]').click();
expect(await page.locator('//*[@id="showname"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showreturn_direct"]').click();
expect(
await page.locator('//*[@id="showreturn_direct"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showdescription"]').click();
expect(
await page.locator('//*[@id="showdescription"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showname"]').click();
expect(await page.locator('//*[@id="showname"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showreturn_direct"]').click();
expect(
await page.locator('//*[@id="showreturn_direct"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="code-input-0"]');
const elementCount = await plusButtonLocator.count();
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.locator('//*[@id="showcode"]').click();
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="code-area-edit0"]').click();
let value = await page.locator('//*[@id="codeValue"]').inputValue();
if (
value !=
'def python_function(text: str) -> str: """This is a default python function that returns the input text""" return text'
) {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="checkAndSaveBtn"]').click();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.locator('//*[@id="code-input-0"]').click();
}
});

View file

@ -1,153 +1,5 @@
import { expect, test } from "@playwright/test";
test("KeypairListComponent", 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("csv");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideCSVLoader"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page.locator('//*[@id="keypair100"]').fill("testtesttesttesttesttest");
const plusButtonLocatorNode = page.locator('//*[@id="plusbtn0"]');
const elementCountNode = await plusButtonLocatorNode.count();
if (elementCountNode > 0) {
await plusButtonLocatorNode.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page.locator('//*[@id="keypair101"]').fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page.locator('//*[@id="keypair102"]').fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
expect(true).toBeTruthy();
} else {
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="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showmetadata"]').click();
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
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="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showmetadata"]').click();
expect(
await page.locator('//*[@id="showmetadata"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page
.locator('//*[@id="keypair100"]')
.fill("testtesttesttesttesttest");
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
if (elementCount > 0) {
await plusButtonLocator.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page
.locator('//*[@id="keypair101"]')
.fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page
.locator('//*[@id="keypair102"]')
.fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
await page.locator('//*[@id="saveChangesBtn"]').click();
const key1 = await page.locator('//*[@id="keypair0"]').inputValue();
const value1 = await page.locator('//*[@id="keypair100"]').inputValue();
const key2 = await page.locator('//*[@id="keypair1"]').inputValue();
const value2 = await page.locator('//*[@id="keypair101"]').inputValue();
if (
key1 === "testtesttesttest" &&
value1 === "testtesttesttesttesttest" &&
key2 === "testtesttesttest2" &&
value2 === "testtesttesttesttesttest2"
) {
expect(true).toBeTruthy();
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
});
test("FloatComponent", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.waitForTimeout(2000);
@ -161,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();
@ -189,7 +41,7 @@ test("FloatComponent", async ({ page }) => {
'//*[@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.locator('//*[@id="showcache"]').click();
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy();

View file

@ -0,0 +1,118 @@
import { expect, test } from "@playwright/test";
test("IntComponent", 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("getrequest");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideGET Request"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="int-input-2"]').click();
await page
.locator('//*[@id="int-input-2"]')
.fill("123456789123456789123456789");
let value = await page.locator('//*[@id="int-input-2"]').inputValue();
if (value != "123456789123456789123456789") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="int-input-2"]').click();
await page.locator('//*[@id="int-input-2"]').fill("-3");
value = await page.locator('//*[@id="int-input-2"]').inputValue();
if (value != "0") {
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();
value = await page.locator('//*[@id="int-input-1"]').inputValue();
if (value != "0") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="int-input-1"]').click();
await page
.locator('//*[@id="int-input-1"]')
.fill("123456789123456789123456789");
await page.locator('//*[@id="showheaders"]').click();
expect(await page.locator('//*[@id="showheaders"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showtimeout"]').click();
expect(await page.locator('//*[@id="showtimeout"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showurl"]').click();
expect(await page.locator('//*[@id="showurl"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showheaders"]').click();
expect(await page.locator('//*[@id="showheaders"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showurl"]').click();
expect(await page.locator('//*[@id="showurl"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="int-input-2"]');
const elementCount = await plusButtonLocator.count();
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.locator('//*[@id="showtimeout"]').click();
expect(
await page.locator('//*[@id="showtimeout"]').isChecked()
).toBeTruthy();
const valueEditNode = await page
.locator('//*[@id="int-input-1"]')
.inputValue();
if (valueEditNode != "123456789123456789123456789") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.locator('//*[@id="int-input-2"]').click();
await page.locator('//*[@id="int-input-2"]').fill("3");
let value = await page.locator('//*[@id="int-input-2"]').inputValue();
if (value != "3") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="int-input-2"]').click();
await page.locator('//*[@id="int-input-2"]').fill("-3");
value = await page.locator('//*[@id="int-input-2"]').inputValue();
if (value != "0") {
expect(false).toBeTruthy();
}
}
});

View file

@ -0,0 +1,159 @@
import { expect, test } from "@playwright/test";
test("KeypairListComponent", 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("csv");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideCSVLoader"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page
.locator('//*[@id="keypair100"]')
.fill("test test test test test test");
const valueWithSpace = await page
.locator('//*[@id="keypair100"]')
.inputValue();
if (valueWithSpace !== "test test test test test test") {
expect(false).toBeTruthy();
}
const plusButtonLocatorNode = page.locator('//*[@id="plusbtn0"]');
const elementCountNode = await plusButtonLocatorNode.count();
if (elementCountNode > 0) {
await plusButtonLocatorNode.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page.locator('//*[@id="keypair101"]').fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page.locator('//*[@id="keypair102"]').fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
expect(true).toBeTruthy();
} else {
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="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showmetadata"]').click();
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
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="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showmetadata"]').click();
expect(
await page.locator('//*[@id="showmetadata"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page
.locator('//*[@id="keypair100"]')
.fill("test test test test test test");
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
if (elementCount > 0) {
await plusButtonLocator.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page
.locator('//*[@id="keypair101"]')
.fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page
.locator('//*[@id="keypair102"]')
.fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
await page.locator('//*[@id="saveChangesBtn"]').click();
const key1 = await page.locator('//*[@id="keypair0"]').inputValue();
const value1 = await page.locator('//*[@id="keypair100"]').inputValue();
const key2 = await page.locator('//*[@id="keypair1"]').inputValue();
const value2 = await page.locator('//*[@id="keypair101"]').inputValue();
if (
key1 === "testtesttesttest" &&
value1 === "test test test test test test" &&
key2 === "testtesttesttest2" &&
value2 === "testtesttesttesttesttest2"
) {
expect(true).toBeTruthy();
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
});

View file

@ -0,0 +1,174 @@
import { expect, test } from "@playwright/test";
test("PromptTemplateComponent", 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("promptTemplate");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sidePromptTemplate"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="prompt-input-4"]').click();
await page
.locator('//*[@id="modal-prompt-input-4"]')
.fill("{prompt} example {prompt1}");
let value = await page
.locator('//*[@id="modal-prompt-input-4"]')
.inputValue();
if (value != "{prompt} example {prompt1}") {
expect(false).toBeTruthy();
}
let valueBadgeOne = await page.locator('//*[@id="badge0"]').innerText();
if (valueBadgeOne != "prompt") {
expect(false).toBeTruthy();
}
let valueBadgeTwo = await page.locator('//*[@id="badge1"]').innerText();
if (valueBadgeTwo != "prompt1") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="genericModalBtnSave"]').click();
await page.locator('//*[@id="textarea-7"]').click();
await page.locator('//*[@id="textarea-7"]').fill("prompt_value_!@#!@#");
value = await page.locator('//*[@id="textarea-7"]').inputValue();
if (value != "prompt_value_!@#!@#") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="textarea-8"]').click();
await page
.locator('//*[@id="textarea-8"]')
.fill("prompt_name_test_123123!@#!@#");
value = await page.locator('//*[@id="textarea-8"]').inputValue();
if (value != "prompt_name_test_123123!@#!@#") {
expect(false).toBeTruthy();
}
value = await page.locator('//*[@id="prompt-input-4"]').innerText();
if (value != "{prompt} example {prompt1}") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="editAdvancedIcon"]').click();
value = await page.locator('//*[@id="textarea-edit-1"]').inputValue();
if (value != "prompt_value_!@#!@#") {
expect(false).toBeTruthy();
}
value = await page.locator('//*[@id="textarea-edit-2"]').inputValue();
if (value != "prompt_name_test_123123!@#!@#") {
expect(false).toBeTruthy();
}
value = await page.locator('//*[@id="prompt-area-edit0"]').innerText();
if (value != "{prompt} example {prompt1}") {
expect(false).toBeTruthy();
}
await page
.locator('//*[@id="textarea-edit-2"]')
.fill("prompt_edit_test_12312312321!@#$");
await page
.locator('//*[@id="textarea-edit-1"]')
.fill("prompt_edit_test_44444444444!@#$");
await page.locator('//*[@id="showtemplate"]').click();
expect(await page.locator('//*[@id="showtemplate"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showprompt"]').click();
expect(await page.locator('//*[@id="showprompt"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showprompt1"]').click();
expect(await page.locator('//*[@id="showprompt1"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showtemplate"]').click();
expect(
await page.locator('//*[@id="showtemplate"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showprompt"]').click();
expect(await page.locator('//*[@id="showprompt"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showprompt1"]').click();
expect(await page.locator('//*[@id="showprompt1"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showtemplate"]').click();
expect(await page.locator('//*[@id="showtemplate"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showprompt"]').click();
expect(await page.locator('//*[@id="showprompt"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showprompt1"]').click();
expect(await page.locator('//*[@id="showprompt1"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showtemplate"]').click();
expect(
await page.locator('//*[@id="showtemplate"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showprompt"]').click();
expect(await page.locator('//*[@id="showprompt"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="textarea-8"]');
const elementCount = await plusButtonLocator.count();
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]'
)
.click();
await page.locator('//*[@id="editAdvancedIcon"]').click();
await page.locator('//*[@id="showprompt1"]').click();
expect(
await page.locator('//*[@id="showprompt1"]').isChecked()
).toBeTruthy();
value = await page.locator('//*[@id="textarea-edit-1"]').inputValue();
if (value != "prompt_edit_test_44444444444!@#$") {
expect(false).toBeTruthy();
}
value = await page.locator('//*[@id="textarea-edit-2"]').inputValue();
if (value != "prompt_edit_test_12312312321!@#$") {
expect(false).toBeTruthy();
}
value = await page.locator('//*[@id="prompt-area-edit0"]').innerText();
if (value != "{prompt} example {prompt1}") {
expect(false).toBeTruthy();
}
}
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,419 @@
{
"description": "Use this Tool on every query",
"name": "Getting Started: Simple python function applied to each output",
"data": {
"nodes": [
{
"width": 384,
"height": 631,
"id": "ChatOpenAI-tRw3A",
"type": "genericNode",
"position": {
"x": 543.1816229116944,
"y": 942.891611351432
},
"data": {
"type": "ChatOpenAI",
"node": {
"template": {
"lc_kwargs": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "lc_kwargs",
"advanced": true,
"type": "code",
"list": false
},
"verbose": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"value": false,
"password": false,
"name": "verbose",
"advanced": false,
"type": "bool",
"list": false
},
"callbacks": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "callbacks",
"advanced": false,
"type": "langchain.callbacks.base.BaseCallbackHandler",
"list": true
},
"client": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "client",
"advanced": false,
"type": "Any",
"list": false
},
"model_name": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"value": "gpt-3.5-turbo",
"password": false,
"options": [
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k-0613",
"gpt-3.5-turbo-16k",
"gpt-4-0613",
"gpt-4-32k-0613",
"gpt-4",
"gpt-4-32k"
],
"name": "model_name",
"advanced": false,
"type": "str",
"list": true
},
"temperature": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"value": "0.2",
"password": false,
"name": "temperature",
"advanced": false,
"type": "float",
"list": false
},
"model_kwargs": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"password": false,
"name": "model_kwargs",
"advanced": true,
"type": "code",
"list": false
},
"openai_api_key": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"value": "",
"password": true,
"name": "openai_api_key",
"display_name": "OpenAI API Key",
"advanced": false,
"type": "str",
"list": false
},
"openai_api_base": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"password": false,
"name": "openai_api_base",
"display_name": "OpenAI API Base",
"advanced": false,
"type": "str",
"list": false
},
"openai_organization": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "openai_organization",
"display_name": "OpenAI Organization",
"advanced": false,
"type": "str",
"list": false
},
"openai_proxy": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "openai_proxy",
"display_name": "OpenAI Proxy",
"advanced": false,
"type": "str",
"list": false
},
"request_timeout": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"password": false,
"name": "request_timeout",
"advanced": false,
"type": "float",
"list": false
},
"max_retries": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"value": 6,
"password": false,
"name": "max_retries",
"advanced": false,
"type": "int",
"list": false
},
"streaming": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"value": false,
"password": false,
"name": "streaming",
"advanced": false,
"type": "bool",
"list": false
},
"n": {
"required": false,
"placeholder": "",
"show": false,
"multiline": false,
"value": 1,
"password": false,
"name": "n",
"advanced": false,
"type": "int",
"list": false
},
"max_tokens": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"password": true,
"name": "max_tokens",
"advanced": false,
"type": "int",
"list": false
},
"_type": "ChatOpenAI"
},
"description": "Wrapper around OpenAI Chat large language models.",
"base_classes": [
"Serializable",
"BaseChatModel",
"ChatOpenAI",
"BaseLanguageModel"
],
"display_name": "ChatOpenAI"
},
"id": "ChatOpenAI-tRw3A",
"value": null
},
"selected": false,
"dragging": false,
"positionAbsolute": {
"x": 543.1816229116944,
"y": 942.891611351432
}
},
{
"width": 384,
"height": 387,
"id": "AgentInitializer-KcVTt",
"type": "genericNode",
"position": {
"x": 1036.6064439140812,
"y": 645.1919693466587
},
"data": {
"type": "AgentInitializer",
"node": {
"template": {
"agent": {
"required": true,
"placeholder": "",
"show": true,
"multiline": false,
"value": "zero-shot-react-description",
"password": false,
"options": [
"zero-shot-react-description",
"react-docstore",
"self-ask-with-search",
"conversational-react-description",
"openai-functions"
],
"name": "agent",
"advanced": false,
"type": "str",
"list": true
},
"memory": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"password": false,
"name": "memory",
"advanced": false,
"type": "BaseChatMemory",
"list": false
},
"tools": {
"required": false,
"placeholder": "",
"show": true,
"multiline": false,
"password": false,
"name": "tools",
"advanced": false,
"type": "Tool",
"list": true
},
"llm": {
"required": true,
"placeholder": "",
"show": true,
"multiline": false,
"password": false,
"name": "llm",
"display_name": "LLM",
"advanced": false,
"type": "BaseLanguageModel",
"list": false
},
"_type": "initialize_agent"
},
"description": "Construct a zero shot agent from an LLM and tools.",
"base_classes": ["AgentExecutor", "function"],
"display_name": "AgentInitializer"
},
"id": "AgentInitializer-KcVTt",
"value": null
},
"selected": false,
"positionAbsolute": {
"x": 1036.6064439140812,
"y": 645.1919693466587
}
},
{
"width": 384,
"height": 437,
"id": "PythonFunctionTool-FwZVF",
"type": "genericNode",
"position": {
"x": 553.050119331742,
"y": 412.9533535948685
},
"data": {
"type": "PythonFunctionTool",
"node": {
"template": {
"name": {
"required": true,
"placeholder": "",
"show": true,
"multiline": false,
"value": "PythonFunction",
"password": false,
"name": "name",
"advanced": false,
"type": "str",
"list": false
},
"description": {
"required": true,
"placeholder": "",
"show": true,
"multiline": true,
"value": "Returns the Text you send. This is a testing tool.",
"password": false,
"name": "description",
"advanced": false,
"type": "str",
"list": false
},
"code": {
"required": true,
"placeholder": "",
"show": true,
"multiline": true,
"value": "\ndef python_function(text: str) -> str:\n \"\"\"This is a default python function that returns the input text\"\"\"\n return text\n",
"password": false,
"name": "code",
"advanced": false,
"type": "code",
"list": false
},
"_type": "PythonFunctionTool"
},
"description": "Python function to be executed.",
"base_classes": ["Tool"],
"display_name": "PythonFunctionTool"
},
"id": "PythonFunctionTool-FwZVF",
"value": null
},
"selected": false,
"dragging": false,
"positionAbsolute": {
"x": 553.050119331742,
"y": 412.9533535948685
}
}
],
"edges": [
{
"source": "ChatOpenAI-tRw3A",
"sourceHandle": "ChatOpenAI|ChatOpenAI-tRw3A|Serializable|BaseChatModel|ChatOpenAI|BaseLanguageModel",
"target": "AgentInitializer-KcVTt",
"targetHandle": "BaseLanguageModel|llm|AgentInitializer-KcVTt",
"style": {
"stroke": "inherit"
},
"className": "stroke-gray-900 dark:stroke-gray-200",
"animated": false,
"id": "reactflow__edge-ChatOpenAI-tRw3AChatOpenAI|ChatOpenAI-tRw3A|Serializable|BaseChatModel|ChatOpenAI|BaseLanguageModel-AgentInitializer-KcVTtBaseLanguageModel|llm|AgentInitializer-KcVTt",
"selected": false
},
{
"source": "PythonFunctionTool-FwZVF",
"sourceHandle": "PythonFunctionTool|PythonFunctionTool-FwZVF|Tool",
"target": "AgentInitializer-KcVTt",
"targetHandle": "Tool|tools|AgentInitializer-KcVTt",
"style": {
"stroke": "inherit"
},
"className": "stroke-gray-900 dark:stroke-gray-200",
"animated": false,
"id": "reactflow__edge-PythonFunctionTool-FwZVFPythonFunctionTool|PythonFunctionTool-FwZVF|Tool-AgentInitializer-KcVTtTool|tools|AgentInitializer-KcVTt",
"selected": false
}
],
"viewport": {
"x": 4.748095479939138,
"y": -155.65184647754464,
"zoom": 0.6079953565987085
}
},
"id": "15030b3c-570d-4658-8473-58138077e9b0"
}

View file

@ -0,0 +1,87 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test.describe("drag and drop test", () => {
/// <reference lib="dom"/>
test("drop collection", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http:localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
// Read your file into a buffer.
const jsonContent = readFileSync(
"tests/onlyFront/assets/collection.json",
"utf-8"
);
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data], "collection.json", {
type: "application/json",
});
dt.items.add(file);
return dt;
}, jsonContent);
// Now dispatch
await page.dispatchEvent('//*[@id="root"]/div/div[2]/div[2]', "drop", {
dataTransfer,
});
expect(
await page
.locator(".main-page-flows-display")
.evaluate((el) => el.children)
).toBeTruthy();
});
test("drop flow", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http:localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
// Read your file into a buffer.
const jsonContent = readFileSync(
"tests/onlyFront/assets/flow.json",
"utf-8"
);
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data], "flow.json", {
type: "application/json",
});
dt.items.add(file);
return dt;
}, jsonContent);
// Now dispatch
await page.dispatchEvent('//*[@id="root"]/div/div[2]/div[2]', "drop", {
dataTransfer,
});
expect(
await page
.locator(".main-page-flows-display")
.evaluate((el) => el.children)
).toBeTruthy();
});
});

View file

@ -1,14 +1,49 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test.describe("Group component tests", () => {
test("group test", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "Community Examples" }).click();
await page
.locator(
"div:nth-child(7) > div:nth-child(2) > .card-component-footer-arrangement > .inline-flex"
)
.click();
test.describe("group node test", () => {
/// <reference lib="dom"/>
test("group and ungroup updating values", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http:localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
// Read your file into a buffer.
const jsonContent = readFileSync(
"tests/onlyFront/assets/flow.json",
"utf-8"
);
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data], "flow.json", {
type: "application/json",
});
dt.items.add(file);
return dt;
}, jsonContent);
// Now dispatch
await page.dispatchEvent('//*[@id="root"]/div/div[2]/div[2]', "drop", {
dataTransfer,
});
expect(
await page
.locator(".main-page-flows-display")
.evaluate((el) => el.children)
).toBeTruthy();
await page.getByRole("button", { name: "Edit Flow" }).click();
//inside the flow
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[1]/div/div[1]/div"
@ -84,16 +119,11 @@ test.describe("Group component tests", () => {
)
.click();
await page.getByLabel("Ungroup").click();
await expect(
page.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[3]/div/div[2]/div[4]/div/div[2]/div/input"
)
).toHaveValue("fieldValue");
await expect(page.locator('//*[@id="input-2"]')).toHaveValue("fieldValue");
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[2]/div/div[2]/div[5]/div/div[2]/div/button/span[1]"
)
.getByTestId(/.*rf__node-AgentInitializer.*/)
.getByRole("button", { name: "openai-functions" })
.textContent()
).toBe("openai-functions");
});

View file

@ -0,0 +1,172 @@
import { expect, test } from "@playwright/test";
test("InputComponent", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
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("Chroma");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideChroma"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator("#input-8").click();
await page
.locator("#input-8")
.fill("collection_name_test_123123123!@#$&*(&%$@");
let value = await page.locator("#input-8").inputValue();
if (value != "collection_name_test_123123123!@#$&*(&%$@") {
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.locator('//*[@id="showchroma_server_cors_allow_origins"]').click();
expect(
await page
.locator('//*[@id="showchroma_server_cors_allow_origins"]')
.isChecked()
).toBeTruthy();
await page.locator('//*[@id="showchroma_server_grpc_port"]').click();
expect(
await page.locator('//*[@id="showchroma_server_grpc_port"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showchroma_server_host"]').click();
expect(
await page.locator('//*[@id="showchroma_server_host"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showchroma_server_http_port"]').click();
expect(
await page.locator('//*[@id="showchroma_server_http_port"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showchroma_server_ssl_enabled"]').click();
expect(
await page.locator('//*[@id="showchroma_server_ssl_enabled"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showcollection_name"]').click();
expect(
await page.locator('//*[@id="showcollection_name"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showpersist"]').click();
expect(await page.locator('//*[@id="showpersist"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="showpersist_directory"]').click();
expect(
await page.locator('//*[@id="showpersist_directory"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showsearch_kwargs"]').click();
expect(
await page.locator('//*[@id="showsearch_kwargs"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showchroma_server_cors_allow_origins"]').click();
expect(
await page
.locator('//*[@id="showchroma_server_cors_allow_origins"]')
.isChecked()
).toBeFalsy();
await page.locator('//*[@id="showchroma_server_grpc_port"]').click();
expect(
await page.locator('//*[@id="showchroma_server_grpc_port"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showchroma_server_host"]').click();
expect(
await page.locator('//*[@id="showchroma_server_host"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showchroma_server_http_port"]').click();
expect(
await page.locator('//*[@id="showchroma_server_http_port"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showchroma_server_ssl_enabled"]').click();
expect(
await page.locator('//*[@id="showchroma_server_ssl_enabled"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showpersist"]').click();
expect(await page.locator('//*[@id="showpersist"]').isChecked()).toBeTruthy();
await page.locator('//*[@id="showpersist_directory"]').click();
expect(
await page.locator('//*[@id="showpersist_directory"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showsearch_kwargs"]').click();
expect(
await page.locator('//*[@id="showsearch_kwargs"]').isChecked()
).toBeFalsy();
let valueEditNode = await page.locator('//*[@id="input-5"]').inputValue();
if (valueEditNode != "collection_name_test_123123123!@#$&*(&%$@") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="input-5"]').click();
await page
.locator('//*[@id="input-5"]')
.fill("NEW_collection_name_test_123123123!@#$&*(&%$@");
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator("#input-8");
const elementCount = await plusButtonLocator.count();
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.locator('//*[@id="showcollection_name"]').click();
expect(
await page.locator('//*[@id="showcollection_name"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
let value = await page.locator("#input-8").inputValue();
if (value != "NEW_collection_name_test_123123123!@#$&*(&%$@") {
expect(false).toBeTruthy();
}
}
});

View file

@ -0,0 +1,254 @@
import { Page, expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test.describe("save component tests", () => {
async function saveComponent(page: Page, pattern: RegExp, n: number) {
for (let i = 0; i < n; i++) {
await page.getByTestId(pattern).click();
//more node options
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Save").click();
}
}
/// <reference lib="dom"/>
test("save group component tests", async ({ page }) => {
//make front work withoput backend
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http:localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
// Read your file into a buffer.
const jsonContent = readFileSync(
"tests/onlyFront/assets/flow.json",
"utf-8"
);
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
// Convert the buffer to a hex array
const file = new File([data], "flow.json", {
type: "application/json",
});
dt.items.add(file);
return dt;
}, jsonContent);
// Now dispatch
await page.dispatchEvent('//*[@id="root"]/div/div[2]/div[2]', "drop", {
dataTransfer,
});
expect(
await page
.locator(".main-page-flows-display")
.evaluate((el) => el.children)
).toBeTruthy();
await page.getByRole("button", { name: "Edit Flow" }).click();
//inside the flow
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[1]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[2]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[3]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page.getByRole("button", { name: "Group" }).click();
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.isVisible()
).toBeTruthy();
await page.getByPlaceholder("Type something...").first().click();
await page.getByPlaceholder("Type something...").first().fill("save");
await page.locator(".react-flow__pane").click();
await page
.locator(".side-bar-buttons-arrangement > div:nth-child(3)")
.click();
//more option click
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Save").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("save");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="custom_componentssave"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.waitForTimeout(2000);
expect(
(await page.getByTestId(/.*rf__node-AgentInitializer.*/).all()).length
).toBe(2);
await page.locator(".isolate > button").first().click();
expect(
(await page.getByTestId(/.*rf__node-AgentInitializer.*/).all()).length
).toBe(1);
await page.getByTestId(/.*rf__node-AgentInitializer.*/).click();
await page.getByTestId(/.*rf__node-AgentInitializer.*/).press("Backspace");
await page
.locator('//*[@id="custom_componentssave"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.getByTestId(/.*rf__node-AgentInitializer.*/).click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Ungroup").click();
expect((await page.getByTestId(/.*rf__node-.*/).all()).length).toBe(3);
expect(
(await page.getByTestId(/.*rf__edge-reactflow.*/).all()).length
).toBe(2);
});
test("save default component with custom values", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http://localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("Chroma");
await page
.locator('//*[@id="vectorstoresChroma"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.locator("#input-8").click();
await page.locator("#input-8").fill("test");
await saveComponent(page, /.*rf__node-Chroma.*/, 1);
await page.getByTestId(/.*rf__node-Chroma.*/).press("Backspace");
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("");
await page.getByPlaceholder("Search").fill("Chroma");
await page
.locator('//*[@id="custom_componentsChroma"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
expect(await page.locator("#input-8").inputValue()).toBe("test");
});
test("save same component multiple times", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http://localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("Chroma");
await page
.locator('//*[@id="vectorstoresChroma"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await saveComponent(page, /.*rf__node-Chroma.*/, 3);
await page.getByTestId(/.*rf__node-Chroma.*/).press("Backspace");
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("");
await page.getByPlaceholder("Search").fill("Chroma");
expect(
await page.locator('//*[@id="custom_componentsChroma"]').isVisible()
).toBeTruthy();
expect(
await page.locator('[id="custom_componentsChroma\\ \\(1\\)"]').isVisible()
).toBeTruthy();
expect(
await page.locator('[id="custom_componentsChroma\\ \\(2\\)"]').isVisible()
).toBeTruthy();
await page
.locator('[id="custom_componentsChroma\\ \\(2\\)"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
expect(
(await page.getByTestId(/.*rf__node-Chroma.*/).allInnerTexts()).includes(
"Chroma (2)"
)
).toBeTruthy();
});
test("save default component and delete it", async ({ page }) => {
await page.routeFromHAR("harFiles/langflow.har", {
url: "**/api/v1/**",
update: false,
});
await page.route("**/api/v1/flows/", async (route) => {
const json = {
id: "e9ac1bdc-429b-475d-ac03-d26f9a2a3210",
};
await route.fulfill({ json, status: 201 });
});
await page.goto("http://localhost:3000/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("Chroma");
await page
.locator('//*[@id="vectorstoresChroma"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await saveComponent(page, /.*rf__node-Chroma.*/, 1);
await page.getByTestId(/.*rf__node-Chroma.*/).press("Backspace");
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("");
await page.getByPlaceholder("Search").fill("Chroma");
await page.locator("#custom_componentsChroma").getByRole("combobox").click({
button: "right",
});
await page.getByLabel("Delete").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill(" ");
await page.getByPlaceholder("Search").fill("Chroma");
expect(
await page.locator("#custom_componentsChroma").isVisible()
).toBeFalsy();
});
});

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