Merge dev

This commit is contained in:
igorrCarvalho 2024-05-29 12:35:09 -03:00
commit 0eb6f6b212
386 changed files with 27625 additions and 32139 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,138 +1,138 @@
{
"name": "langflow",
"version": "0.1.2",
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.3.4",
"@million/lint": "^0.0.73",
"react-hotkeys-hook": "^4.5.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"ace-builds": "^1.24.1",
"ag-grid-community": "^31.2.1",
"ag-grid-react": "^31.2.1",
"ansi-to-html": "^0.7.2",
"axios": "^1.5.0",
"base64-js": "^1.5.1",
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
"dompurify": "^3.0.5",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",
"file-saver": "^2.0.5",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"million": "^3.0.6",
"moment": "^2.29.4",
"openseadragon": "^4.1.1",
"playwright": "^1.42.0",
"react": "^18.2.21",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.51.4",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-pdf": "^7.7.1",
"react-router-dom": "^6.15.0",
"react-syntax-highlighter": "^15.5.0",
"react18-json-view": "^0.2.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"shadcn-ui": "^0.2.3",
"short-unique-id": "^4.4.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.0",
"vite-plugin-svgr": "^3.2.0",
"web-vitals": "^2.1.4",
"zod": "^3.23.7",
"zustand": "^4.4.7"
},
"scripts": {
"dev:docker": "vite --host 0.0.0.0",
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "npx prettier --write \"{docs,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"simple-git-hooks": {
"pre-commit": "npx pretty-quick --staged"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^16.18.46",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"daisyui": "^4.0.4",
"eslint": "^8.57.0",
"eslint-plugin-node": "^11.1.0",
"postcss": "^8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.3.0",
"pretty-quick": "^3.1.3",
"simple-git-hooks": "^2.11.1",
"tailwindcss": "^3.3.3",
"tailwindcss-dotted-background": "^1.1.0",
"typescript": "^5.2.2",
"ua-parser-js": "^1.0.37",
"vite": "^4.5.2"
}
"name": "langflow",
"version": "0.1.2",
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.3.4",
"@million/lint": "^0.0.73",
"react-hotkeys-hook": "^4.5.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"ace-builds": "^1.24.1",
"ag-grid-community": "^31.2.1",
"ag-grid-react": "^31.2.1",
"ansi-to-html": "^0.7.2",
"axios": "^1.5.0",
"base64-js": "^1.5.1",
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
"dompurify": "^3.0.5",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",
"file-saver": "^2.0.5",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"million": "^3.0.6",
"moment": "^2.29.4",
"openseadragon": "^4.1.1",
"playwright": "^1.42.0",
"react": "^18.2.21",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.51.4",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-pdf": "^7.7.1",
"react-router-dom": "^6.15.0",
"react-syntax-highlighter": "^15.5.0",
"react18-json-view": "^0.2.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"shadcn-ui": "^0.2.3",
"short-unique-id": "^4.4.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.0",
"vite-plugin-svgr": "^3.2.0",
"web-vitals": "^2.1.4",
"zod": "^3.23.7",
"zustand": "^4.4.7"
},
"scripts": {
"dev:docker": "vite --host 0.0.0.0",
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "npx prettier --write \"{docs,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"simple-git-hooks": {
"pre-commit": "npx pretty-quick --staged"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^16.18.46",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"daisyui": "^4.0.4",
"eslint": "^8.57.0",
"eslint-plugin-node": "^11.1.0",
"postcss": "^8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.3.0",
"pretty-quick": "^3.1.3",
"simple-git-hooks": "^2.11.1",
"tailwindcss": "^3.3.3",
"tailwindcss-dotted-background": "^1.1.0",
"typescript": "^5.2.2",
"ua-parser-js": "^1.0.37",
"vite": "^4.5.2"
}
}

View file

@ -15,15 +15,18 @@ import {
} from "./constants/constants";
import { AuthContext } from "./contexts/authContext";
import { autoLogin, getGlobalVariables, getHealth } from "./controllers/API";
import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path";
import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useFolderStore } from "./stores/foldersStore";
import { useGlobalVariablesStore } from "./stores/globalVariables";
import { useGlobalVariablesStore } from "./stores/globalVariablesStore/globalVariables";
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
useTrackLastVisitedPath();
const removeFromTempNotificationList = useAlertStore(
(state) => state.removeFromTempNotificationList,
);
@ -104,7 +107,6 @@ export default function App() {
const fetchAllData = async () => {
setTimeout(async () => {
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
getFoldersApi();
}, 1000);
};
@ -112,6 +114,7 @@ export default function App() {
return new Promise<void>(async (resolve, reject) => {
if (isAuthenticated) {
try {
await getFoldersApi();
await getTypes();
await refreshFlows();
const res = await getGlobalVariables();
@ -208,7 +211,7 @@ export default function App() {
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" && (
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.title}
@ -216,26 +219,12 @@ export default function App() {
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
alert.type === "success" && (
<SuccessAlert
alert.type === "notice" && (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
@ -244,6 +233,20 @@ export default function App() {
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
</div>
</div>
);

View file

@ -31,14 +31,14 @@ export default function ImageViewer({ image }) {
const fullPageButton = document.getElementById("full-page-button");
zoomInButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(1.2)
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(0.8)
viewer.viewport.zoomBy(0.8),
);
homeButton!.addEventListener("click", () => viewer.viewport.goHome());
fullPageButton!.addEventListener("click", () =>
viewer.setFullScreen(true)
viewer.setFullScreen(true),
);
// Optionally, you can set additional viewer options here
@ -47,16 +47,16 @@ export default function ImageViewer({ image }) {
return () => {
viewer.destroy();
zoomInButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(1.2)
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(0.8)
viewer.viewport.zoomBy(0.8),
);
homeButton!.removeEventListener("click", () =>
viewer.viewport.goHome()
viewer.viewport.goHome(),
);
fullPageButton!.removeEventListener("click", () =>
viewer.setFullScreen(true)
viewer.setFullScreen(true),
);
};
}

View file

@ -2,16 +2,16 @@ import { useState } from "react";
import { registerGlobalVariable } from "../../controllers/API";
import BaseModal from "../../modals/baseModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
import { useTypesStore } from "../../stores/typesStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { sortByName } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import sortByName from "./utils/sort-by-name";
//TODO IMPLEMENT FORM LOGIC

View file

@ -0,0 +1,3 @@
export default function sortByName(stringList: string[]): string[] {
return stringList.sort((a, b) => a.localeCompare(b));
}

View file

@ -13,7 +13,7 @@ import { storeComponent } from "../../types/store";
import cloneFLowWithParent, {
getInputsAndOutputs,
} from "../../utils/storeUtils";
import { cn, convertTestName } from "../../utils/utils";
import { cn } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Button } from "../ui/button";
@ -27,6 +27,7 @@ import {
import { Checkbox } from "../ui/checkbox";
import { FormControl, FormField } from "../ui/form";
import Loading from "../ui/loading";
import { convertTestName } from "./utils/convert-test-name";
import DragCardComponent from "./components/dragCardComponent";
export default function CollectionCardComponent({

View file

@ -0,0 +1,3 @@
export function convertTestName(name: string): string {
return name.replace(/ /g, "-").toLowerCase();
}

View file

@ -8,7 +8,6 @@ import {
} from "../../../ui/dropdown-menu";
import { useNavigate } from "react-router-dom";
import { Node } from "reactflow";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
@ -22,11 +21,7 @@ import IconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({
removeFunction,
}: {
removeFunction: (nodes: Node[]) => void;
}): JSX.Element => {
export const MenuBar = ({}: {}): JSX.Element => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -36,7 +31,6 @@ export const MenuBar = ({
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const [openLogs, setOpenLogs] = useState(false);
const nodes = useFlowStore((state) => state.nodes);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
@ -72,14 +66,6 @@ export const MenuBar = ({
return currentFlow ? (
<div className="round-button-div">
<button
onClick={() => {
removeFunction(nodes);
navigate("/");
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>

View file

@ -3,14 +3,17 @@ import { FaDiscord, FaGithub } from "react-icons/fa";
import { RiTwitterXFill } from "react-icons/ri";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import {
LOCATIONS_TO_RETURN,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { Node } from "reactflow";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useLocationStore } from "../../stores/locationStore";
import { useStoreStore } from "../../stores/storeStore";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
@ -29,6 +32,7 @@ import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element {
const notificationCenter = useAlertStore((state) => state.notificationCenter);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const navigate = useNavigate();
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
@ -40,20 +44,56 @@ export default function Header(): JSX.Element {
const setDark = useDarkStore((state) => state.setDark);
const stars = useDarkStore((state) => state.stars);
async function checkForChanges(nodes: Node[]): Promise<void> {
const routeHistory = useLocationStore((state) => state.routeHistory);
async function checkForChanges(): Promise<void> {
if (nodes.length === 0) {
await removeFlow(id!);
}
}
const redirectToLastLocation = () => {
const lastFlowVisitedIndex = routeHistory
.reverse()
.findIndex(
(path) => path.includes("/flow/") && path !== location.pathname,
);
const lastFlowVisited = routeHistory[lastFlowVisitedIndex];
lastFlowVisited && !location.pathname.includes("/flow")
? navigate(lastFlowVisited)
: navigate("/all");
};
const visitedFlowPathBefore = () => {
const lastThreeVisitedPaths = routeHistory.slice(-3);
return lastThreeVisitedPaths.some((path) => path.includes("/flow/"));
};
const showArrowReturnIcon =
LOCATIONS_TO_RETURN.some((path) => location.pathname.includes(path)) &&
visitedFlowPathBefore();
return (
<div className="header-arrangement">
<div className="header-start-display lg:w-[30%]">
<Link to="/" onClick={() => checkForChanges(nodes)}>
<Link to="/all" className="cursor-pointer" onClick={checkForChanges}>
<span className="ml-4 text-2xl">⛓️</span>
</Link>
<MenuBar removeFunction={checkForChanges} />
{showArrowReturnIcon && (
<button
onClick={() => {
checkForChanges();
redirectToLastLocation();
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
)}
<MenuBar />
</div>
<div className="round-button-div">
<Link to="/">
<Button
@ -65,9 +105,7 @@ export default function Header(): JSX.Element {
: "secondary"
}
size="sm"
onClick={() => {
checkForChanges(nodes);
}}
onClick={checkForChanges}
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
@ -80,9 +118,7 @@ export default function Header(): JSX.Element {
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
onClick={() => {
checkForChanges(nodes);
}}
onClick={checkForChanges}
data-testid="button-store"
>
<IconComponent name="Store" className="h-4 w-4" />

View file

@ -33,6 +33,8 @@ export default function InputComponent({
optionButton,
objectOptions,
isObjectOption = false,
name,
onChangeFolderName,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
@ -53,6 +55,7 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
name={name}
id={"form-" + id}
ref={refInput}
onBlur={onInputLostFocus}
@ -72,6 +75,9 @@ export default function InputComponent({
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
if (onChangeFolderName) {
return onChangeFolderName(e);
}
onChange && onChange(e.target.value);
}}
onCopy={(e) => {

View file

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { deleteGlobalVariable } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
import { InputGlobalComponentType } from "../../types/components";
import { cn } from "../../utils/utils";
import AddNewVariableButton from "../addNewVariableButtonComponent/addNewVariableButton";
@ -34,6 +34,7 @@ export default function InputGlobalComponent({
useEffect(() => {
if (data.node?.template[name])
if (
globalVariablesEntries &&
!globalVariablesEntries.includes(data.node?.template[name].value) &&
data.node?.template[name].load_from_db
) {
@ -138,6 +139,7 @@ export default function InputGlobalComponent({
)}
selectedOption={
data?.node?.template[name].load_from_db &&
globalVariablesEntries &&
globalVariablesEntries.includes(data?.node?.template[name].value ?? "")
? data?.node?.template[name].value
: ""

View file

@ -11,10 +11,7 @@ type SideBarButtonsComponentProps = {
pathname: string;
handleOpenNewFolderModal?: () => void;
};
const SideBarButtonsComponent = ({
items,
handleOpenNewFolderModal,
}: SideBarButtonsComponentProps) => {
const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
return (
<>
{items.map((item) => (
@ -26,7 +23,6 @@ const SideBarButtonsComponent = ({
buttonVariants({ variant: "ghost" }),
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent",
)}
onClick={handleOpenNewFolderModal}
>
{item.title}
</div>

View file

@ -1,16 +1,18 @@
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
import { addFolder, updateFolder } from "../../../../pages/MainPage/services";
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { handleKeyDown } from "../../../../utils/reactflowUtils";
import { cn } from "../../../../utils/utils";
import DropdownButton from "../../../dropdownButtonComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../genericIconComponent";
import { Button, buttonVariants } from "../../../ui/button";
import { Input } from "../../../ui/input";
import useFileDrop from "../../hooks/use-on-file-drop";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
import useAlertStore from "../../../../stores/alertStore";
type SideBarFoldersButtonsComponentProps = {
folders: FolderType[];
@ -18,22 +20,27 @@ type SideBarFoldersButtonsComponentProps = {
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
handleAddFolder?: () => void;
};
const SideBarFoldersButtonsComponent = ({
folders,
pathname,
handleAddFolder,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
}: SideBarFoldersButtonsComponentProps) => {
const refInput = useRef<HTMLInputElement>(null);
const setFolders = useFolderStore((state) => state.setFolders);
const folders = useFolderStore((state) => state.folders);
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })),
);
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
const urlWithoutPath = pathname.split("/").length < 4;
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const folderIdDragging = useFolderStore((state) => state.folderIdDragging);
const checkPathName = (itemId: string) => {
if (urlWithoutPath && itemId === myCollectionId) {
@ -62,20 +69,44 @@ const SideBarFoldersButtonsComponent = ({
handleDownloadFolderFn(id);
};
function addNewFolder() {
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
},
);
}
function handleEditFolderName(e, name): void {
const {
target: { value },
} = e;
setFoldersNames((old) => ({
...old,
[name]: value,
}));
}
useEffect(() => {
folders.map((obj) => ({ name: obj.name, edit: false }));
}, [folders]);
console.log(folderId, folderIdDragging);
return (
<>
<div className="flex shrink-0 items-center justify-between">
<DropdownButton
firstButtonName="New Folder"
onFirstBtnClick={handleAddFolder!}
options={[]}
plusButton={true}
dropdownOptions={false}
/>
<Button variant="primary" onClick={addNewFolder}>
<ForwardedIconComponent
name="Plus"
className="main-page-nav-button"
/>
New Folder
</Button>
<Button
variant="primary"
onClick={handleUploadFlowsToFolder}
className="px-7"
onClick={handleUploadFlowsToFolder}
>
<ForwardedIconComponent
name="Upload"
@ -87,81 +118,206 @@ const SideBarFoldersButtonsComponent = ({
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
<>
{folders.map((item, index) => (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
className={cn(
buttonVariants({ variant: "ghost" }),
checkPathName(item.id!)
? "border border-border bg-muted hover:bg-muted"
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
)}
onClick={() => handleChangeFolder!(item.id!)}
>
<div className="flex w-full items-center gap-2">
<IconComponent
name={"folder"}
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
<span className="block max-w-full truncate opacity-100">
{item.name}
</span>
<div className="flex-1" />
{index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDeleteFolder!(item);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"trash"}
className=" w-4 stroke-[1.5]"
/>
</Button>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
)[0];
return (
<div
onDragOver={(e) => dragOver(e, item.id!)}
onDragEnter={(e) => dragEnter(e, item.id!)}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
className={cn(
buttonVariants({ variant: "ghost" }),
checkPathName(item.id!)
? "border border-border bg-muted hover:bg-muted"
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
folderIdDragging === item.id! ? "bg-border" : "",
)}
{index > 0 && (
onClick={() => handleChangeFolder!(item.id!)}
>
<div
onDoubleClick={(event) => {
if (item.name === "My Projects") {
return;
}
if (!foldersNames[item.name]) {
setFoldersNames({ [item.name]: item.name });
}
if (
editFolders.find((obj) => obj.name === item.name)?.name
) {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: true };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
return;
}
setEditFolderName((old) => [
...old,
{ name: item.name, edit: true },
]);
setFoldersNames((oldFolder) => ({
...oldFolder,
[item.name]: item.name,
}));
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
className="flex w-full items-center gap-2"
>
<IconComponent
name={"folder"}
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
{editFolderName?.edit ? (
<div>
<Input
className="w-36"
onChange={(e) => {
handleEditFolderName(e, item.name);
}}
ref={refInput}
onKeyDown={(e) => {
if (e.key === "Escape") {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
}
if (e.key === "Enter") {
refInput.current?.blur();
}
handleKeyDown(e, e.key, "");
}}
autoFocus={true}
onBlur={async () => {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
if (foldersNames[item.name].trim() !== "") {
setFoldersNames((old) => ({
...old,
[item.name]: foldersNames[item.name],
}));
const body = {
...item,
name: foldersNames[item.name],
flows: item.flows?.length > 0 ? item.flows : [],
components:
item.components?.length > 0
? item.components
: [],
};
const updatedFolder = await updateFolder(
body,
item.id!,
);
const updateFolders = folders.filter(
(f) => f.name !== item.name,
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
} else {
setFoldersNames((old) => ({
...old,
[item.name]: item.name,
}));
}
}}
value={foldersNames[item.name]}
id={`input-folder-${item.name}`}
/>
</div>
) : (
<span className="block max-w-full truncate opacity-100">
{item.name}
</span>
)}
<div className="flex-1" />
{index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDeleteFolder!(item);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"trash"}
className=" w-4 stroke-[1.5]"
/>
</Button>
)}
{/* {index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"pencil"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
)} */}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleEditFolder!(item);
handleDownloadFolder(item.id!);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"pencil"}
name={"Download"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
)}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDownloadFolder(item.id!);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"Download"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
</div>
</div>
</div>
))}
);
})}
</>
</div>
</>

View file

@ -7,13 +7,16 @@ import { uploadFlowsFromFolders } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../stores/foldersStore";
import { FlowType } from "../../../types/flow";
import { addVersionToDuplicates } from "../../../utils/reactflowUtils";
const useFileDrop = (folderId, folderChangeCallback) => {
const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
const setFolderIdDragging = useFolderStore(
(state) => state.setFolderIdDragging,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
const flows = useFlowsManagerStore((state) => state.flows);
const triggerFolderChange = (folderId) => {
@ -42,12 +45,14 @@ const useFileDrop = (folderId, folderChangeCallback) => {
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
};
const dragEnter = (
@ -55,10 +60,12 @@ const useFileDrop = (folderId, folderChangeCallback) => {
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
e.preventDefault();
};
@ -71,6 +78,7 @@ const useFileDrop = (folderId, folderChangeCallback) => {
e.preventDefault();
if (e.target === e.currentTarget) {
setFolderDragging(false);
setFolderIdDragging("");
}
};
@ -92,7 +100,6 @@ const useFileDrop = (folderId, folderChangeCallback) => {
e.preventDefault();
handleFileDrop(e);
setFolderDragging(false);
};
const uploadFromDragCard = (flowId, folderId) => {
@ -101,11 +108,15 @@ const useFileDrop = (folderId, folderChangeCallback) => {
if (!selectedFlow) {
throw new Error("Flow not found");
}
const updatedFlow = { ...selectedFlow, folder_id: folderId };
const newName = addVersionToDuplicates(updatedFlow, flows);
updatedFlow.name = newName;
setFolderDragging(false);
setFolderIdDragging("");
const updatedFlow: FlowType = {
...selectedFlow,
folder_id: folderId,
};
updateFlowInDatabase(updatedFlow).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
@ -115,11 +126,11 @@ const useFileDrop = (folderId, folderChangeCallback) => {
const uploadFormData = (data) => {
const formData = new FormData();
formData.append("file", data);
setFolderDragging(false);
setFolderIdDragging("");
uploadFlowsFromFolders(formData).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
refreshFlows();
});
};

View file

@ -5,6 +5,9 @@ import { cn } from "../../utils/utils";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import SideBarButtonsComponent from "./components/sideBarButtons";
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
import { addFolder } from "../../pages/MainPage/services";
import { useNavigate } from "react-router-dom";
import useFlowStore from "../../stores/flowStore";
type SidebarNavProps = {
items: {
@ -22,7 +25,6 @@ type SidebarNavProps = {
export default function SidebarNav({
className,
items,
handleOpenNewFolderModal,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
@ -39,11 +41,7 @@ export default function SidebarNav({
return (
<nav className={cn(className)} {...props}>
<HorizontalScrollFadeComponent>
<SideBarButtonsComponent
items={items}
pathname={pathname}
handleOpenNewFolderModal={handleOpenNewFolderModal}
/>
<SideBarButtonsComponent items={items} pathname={pathname} />
{!loadingFolders && folders?.length > 0 && isFolderPath && (
<SideBarFoldersButtonsComponent
@ -52,7 +50,6 @@ export default function SidebarNav({
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
handleAddFolder={handleOpenNewFolderModal}
/>
)}
</HorizontalScrollFadeComponent>

View file

@ -32,51 +32,6 @@ const TableComponent = forwardRef<
ref,
) => {
const dark = useDarkStore((state) => state.dark);
var currentRowHeight: number;
var minRowHeight = 25;
const getRowHeight = useCallback(() => {
return currentRowHeight;
}, []);
const onGridReady = useCallback((params: any) => {
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
currentRowHeight = minRowHeight;
}, []);
const updateRowHeight = (params: { api: any }) => {
const bodyViewport = document.querySelector(".ag-body-viewport");
if (!bodyViewport) {
return;
}
var gridHeight = bodyViewport.clientHeight;
var renderedRowCount = params.api.getDisplayedRowCount();
if (renderedRowCount * minRowHeight >= gridHeight) {
if (currentRowHeight !== minRowHeight) {
currentRowHeight = minRowHeight;
params.api.resetRowHeights();
}
} else {
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
params.api.resetRowHeights();
}
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
if (props.rowData.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded-md border">
@ -102,10 +57,6 @@ const TableComponent = forwardRef<
<AgGridReact
{...props}
className={cn(props.className, "custom-scroll")}
getRowHeight={getRowHeight}
onGridReady={onGridReady}
onFirstDataRendered={onFirstDataRendered}
onGridSizeChanged={onGridSizeChanged}
defaultColDef={{
minWidth: 100,
}}

View file

@ -800,6 +800,15 @@ export const NATIVE_CATEGORIES = [
"agents",
];
export const AUTHORIZED_DUPLICATE_REQUESTS = [
"/health",
"/flows",
"/logout",
"/refresh",
"/login",
"/auto_login",
];
export const SAVE_DEBOUNCE_TIME = 300;
export const defaultShortcuts = [
@ -900,3 +909,5 @@ export const unavailableShortcutss = [
export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to display right now. Please check back later.`;
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];

View file

@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { renewAccessToken } from ".";
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore";
@ -81,16 +82,26 @@ function ApiInterceptor() {
const requestInterceptor = api.interceptors.request.use(
(config) => {
const lastUrl = localStorage.getItem("lastUrlCalled");
const lastMethodCalled = localStorage.getItem("lastMethodCalled");
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) =>
config?.url!.includes(request),
);
if (
config?.url === lastUrl &&
config?.url !== "/health" &&
config?.method === "get"
!isContained &&
lastMethodCalled === config.method
) {
return Promise.reject("Duplicate request");
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem(
"lastRequestData",
JSON.stringify(config.data) ?? "",
);
const accessToken = cookies.get("access_token_lf");
if (accessToken && !isAuthorizedURL(config?.url)) {

View file

@ -173,8 +173,6 @@ export default function ParameterComponent({
renderTooltips();
}, [tooltipTitle, flow]);
console.log(left === true && type === "dict");
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>

View file

@ -27,14 +27,11 @@ import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import {
classNames,
cn,
getFieldTitle,
isWrappedWithClass,
sortFields,
} from "../../utils/utils";
import { classNames, cn } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
import isWrappedWithClass from "../../pages/FlowPage/components/PageComponent/utils/is-wrapped-with-class";
export default function GenericNode({
data,

View file

@ -0,0 +1,10 @@
import { APITemplateType } from "../../types/api";
export default function getFieldTitle(
template: APITemplateType,
templateField: string,
): string {
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name ?? templateField;
}

View file

@ -0,0 +1,40 @@
import { priorityFields } from "../../constants/constants";
export default function sortFields(a, b, fieldOrder) {
// Early return for empty fields
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
// Normalize the case to ensure case-insensitive comparison
const normalizedFieldA = a.toLowerCase();
const normalizedFieldB = b.toLowerCase();
const aIsPriority = priorityFields.has(normalizedFieldA);
const bIsPriority = priorityFields.has(normalizedFieldB);
// Sort by priority
if (aIsPriority && !bIsPriority) return -1;
if (!aIsPriority && bIsPriority) return 1;
// Check if either field is in the fieldOrder array
const indexOfA = fieldOrder.indexOf(normalizedFieldA);
const indexOfB = fieldOrder.indexOf(normalizedFieldB);
// If both fields are in fieldOrder, sort by their order in the array
if (indexOfA !== -1 && indexOfB !== -1) {
return indexOfA - indexOfB;
}
// If only one of the fields is in fieldOrder, that field comes first
if (indexOfA !== -1) {
return -1;
}
if (indexOfB !== -1) {
return 1;
}
// Default case for fields not in priorityFields and not found in fieldOrder
// You might want to sort them alphabetically or in another specific manner
return a.localeCompare(b);
}

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { useLocationStore } from "../stores/locationStore";
function useTrackLastVisitedPath() {
const location = useLocation();
const setHistory = useLocationStore((state) => state.setRouteHistory);
useEffect(() => {
setHistory(location.pathname);
}, [location]);
}
export default useTrackLastVisitedPath;

View file

@ -0,0 +1,17 @@
const SvgCouchbaseIcon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 256"
{...props}
>
<path
fill="#ED2226"
d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"
/>
</svg>
);
export default SvgCouchbaseIcon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" preserveAspectRatio="xMidYMid" viewBox="0 0 256 256" id="couchbase"><path fill="#ED2226" d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"></path></svg>

After

Width:  |  Height:  |  Size: 720 B

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgCouchbaseIcon from "./Couchbase";
export const CouchbaseIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgCouchbaseIcon ref={ref} {...props} />;
});

View file

@ -0,0 +1,26 @@
/**
* Function to get the curl code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code
*/
export default function getCurlCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
}/api/v1/run/${flowId}?stream=false \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}'
`;
}

View file

@ -0,0 +1,58 @@
/**
* Function to get the python code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export default function getPythonApiCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
from typing import Optional
BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str,
flow_id: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {
"input_value": message,
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
message = "message"
${!isAuth ? `api_key = "<your api key>"` : ""}
print(run_flow(message=message, flow_id=FLOW_ID, tweaks=TWEAKS${
!isAuth ? `, api_key=api_key` : ""
}))`;
}

View file

@ -0,0 +1,20 @@
/**
* Function to get the python code for the API
* @param {string} flow - The current flow
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export default function getPythonCode(
flowName: string,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}

View file

@ -0,0 +1,24 @@
/**
* Function to get the widget code for the API
* @param {string} flow - The current flow.
* @returns {string} - The widget code
*/
export default function getWidgetCode(
flowId: string,
flowName: string,
isAuth: boolean,
): string {
return `<script src="https://cdn.jsdelivr.net/gh/langflow-ai/langflow-embedded-chat@1.0_alpha/dist/build/static/js/bundle.min.js"></script>
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
host_url="http://localhost:7860"${
!isAuth
? `
api_key="..."`
: ""
}
></langflow-chat>`;
}

View file

@ -0,0 +1,78 @@
export default function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
];
}
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
language: "py",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[4],
},
];
}

View file

@ -13,13 +13,6 @@ import { useTweaksStore } from "../../../stores/tweaksStore";
import { TemplateVariableType } from "../../../types/api";
import { uniqueTweakType } from "../../../types/components";
import { FlowType } from "../../../types/flow/index";
import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
tabsArray,
} from "../../../utils/utils";
import BaseModal from "../../baseModal";
import { buildContent } from "../utils/build-content";
import { buildTweaks } from "../utils/build-tweaks";
@ -27,6 +20,11 @@ import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object"
import { getChangesType } from "../utils/get-changes-types";
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
import { getValue } from "../utils/get-value";
import getPythonApiCode from "../utils/get-python-api-code";
import getCurlCode from "../utils/get-curl-code";
import getPythonCode from "../utils/get-python-code";
import getWidgetCode from "../utils/get-widget-code";
import tabsArray from "../utils/tabs-array";
const ApiModal = forwardRef(
(

View file

@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import { FlowType, NodeDataType } from "../../types/flow";
import BaseModal from "../baseModal";
import TableComponent from "../../components/tableComponent";
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
@ -12,15 +12,20 @@ import {
ColGroupDef,
SizeColumnsToFitGridStrategy,
} from "ag-grid-community";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow);
const nodes = useFlowStore((state) => state.nodes);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const flows = useFlowsManagerStore((state) => state.flows);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
useEffect(() => {
setName(currentFlow!.name);
setDescription(currentFlow!.description);
@ -31,6 +36,7 @@ export default function FlowLogsModal({
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]);
const [activeTab, setActiveTab] = useState("Executions");
const noticed = useRef(false);
function handleClick(): void {
currentFlow!.name = name;
@ -53,6 +59,25 @@ export default function FlowLogsModal({
setRows(rows);
});
}
if (open && activeTab === "Messages" && !noticed.current) {
const haStream = nodes
.map((nodes) => (nodes.data as NodeDataType).node!.template)
.some((template) => template["stream"] && template["stream"].value);
console.log(
haStream,
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template),
);
if (haStream) {
setNoticeData({
title: "Streamed messages will not appear in this table.",
});
noticed.current = true;
}
}
if (!open) {
noticed.current = false;
}
}, [open, activeTab]);
const [nameLists, setNameList] = useState<string[]>([]);

View file

@ -24,8 +24,9 @@ import { postValidatePrompt } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { genericModalPropsType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, varHighlightHTML } from "../../utils/utils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
import varHighlightHTML from "./utils/var-highlight-html";
export default function GenericModal({
field_name = "",
@ -82,7 +83,7 @@ export default function GenericModal({
}
const filteredWordsHighlight = matches.filter(
(word) => !invalid_chars.includes(word)
(word) => !invalid_chars.includes(word),
);
setWordsHighlight(filteredWordsHighlight);
@ -133,7 +134,7 @@ export default function GenericModal({
// to the first key of the custom_fields object
if (field_name === "") {
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""]
apiReturn.data?.frontend_node?.custom_fields?.[""],
)
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
@ -209,7 +210,7 @@ export default function GenericModal({
<div
className={classNames(
!isEdit ? "rounded-lg border" : "",
"flex h-full w-full"
"flex h-full w-full",
)}
>
{type === TypeModal.PROMPT && isEdit && !readonly ? (

View file

@ -0,0 +1,6 @@
import { IVarHighlightType } from "../../../types/components";
export default function varHighlightHTML({ name }: IVarHighlightType): string {
const html = `<span class="font-semibold chat-message-highlight">{${name}}</span>`;
return html;
}

View file

@ -23,10 +23,10 @@ import {
removeFileNameFromComponents,
removeGlobalVariableFromComponents,
} from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import BaseModal from "../baseModal";
import ConfirmationModal from "../confirmationModal";
import ExportModal from "../exportModal";
import getTagsIds from "./utils/get-tags-ids";
export default function ShareModal({
component,
@ -138,14 +138,14 @@ export default function ShareModal({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
},
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
unavaliableNames.find((e) => e.name === name)!.id,
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
@ -234,7 +234,7 @@ export default function ShareModal({
<div className="mt-3 flex h-8 w-full">
<TagsSelector
tags={tags}
loadingTags={loadingTags}
loadingTags={loadingTags}
disabled={false}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
@ -296,7 +296,7 @@ export default function ShareModal({
className={is_component ? "w-40" : "w-28"}
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
(element) => element.name === name,
);
if (isNameAvailable) {

View file

@ -0,0 +1,8 @@
export default function getTagsIds(
tags: string[],
tagListId: { name: string; id: string }[],
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
}

View file

@ -43,9 +43,10 @@ import {
updateIds,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import isWrappedWithClass from "./utils/is-wrapped-with-class";
import getRandomName from "./utils/get-random-name";
const nodeTypes = {
genericNode: GenericNode,

View file

@ -0,0 +1,35 @@
import { ADJECTIVES, NOUNS } from "../../../../../flow_constants";
import { getRandomElement } from "../../../../../utils/reactflowUtils";
import { toTitleCase } from "../../../../../utils/utils";
export default function getRandomName(
retry: number = 0,
noSpace: boolean = false,
maxRetries: number = 3,
): string {
const left: string[] = ADJECTIVES;
const right: string[] = NOUNS;
const lv = getRandomElement(left);
const rv = getRandomElement(right);
// Condition to avoid "boring wozniak"
if (lv === "boring" && rv === "wozniak") {
if (retry < maxRetries) {
return getRandomName(retry + 1, noSpace, maxRetries);
} else {
console.warn("Max retries reached, returning as is");
}
}
// Append a suffix if retrying and noSpace is true
if (retry > 0 && noSpace) {
const retrySuffix = Math.floor(Math.random() * 10);
return `${lv}_${rv}${retrySuffix}`;
}
// Construct the final name
let final_name = noSpace ? `${lv}_${rv}` : `${lv} ${rv}`;
// Return title case final name
return toTitleCase(final_name);
}

View file

@ -0,0 +1,4 @@
const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`);
export default isWrappedWithClass;

View file

@ -19,15 +19,12 @@ import {
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import {
classNames,
removeCountFromString,
sensitiveSort,
} from "../../../../utils/utils";
import { classNames, removeCountFromString } from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import ParentDisclosureComponent from "../ParentDisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
import { sortKeys } from "./utils";
import sensitiveSort from "./utils/sensitive-sort";
export default function ExtraSidebar(): JSX.Element {
const data = useTypesStore((state) => state.data);
@ -45,7 +42,7 @@ export default function ExtraSidebar(): JSX.Element {
const [search, setSearch] = useState("");
function onDragStart(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType }
data: { type: string; node?: APIClassType },
): void {
//start drag event
var crt = event.currentTarget.cloneNode(true);
@ -71,7 +68,7 @@ export default function ExtraSidebar(): JSX.Element {
let keys = Object.keys(data[d]).filter(
(nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase()),
);
keys.forEach((element) => {
ret[d][element] = data[d][element];
@ -138,7 +135,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -175,7 +172,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -204,7 +201,7 @@ export default function ExtraSidebar(): JSX.Element {
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: ""
: "",
)}
>
<IconComponent
@ -213,14 +210,14 @@ export default function ExtraSidebar(): JSX.Element {
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
: "",
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, currentFlow, hasStore]
[hasApiKey, validApiKey, currentFlow, hasStore],
);
const ExportMemo = useMemo(
@ -231,7 +228,7 @@ export default function ExtraSidebar(): JSX.Element {
</button>
</ExportModal>
),
[]
[],
);
const getIcon = useMemo(() => {
@ -315,8 +312,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -360,7 +357,7 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}{" "}
<ParentDisclosureComponent
openDisc={false}
@ -397,8 +394,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -472,7 +469,7 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}
</ParentDisclosureComponent>
</div>

View file

@ -0,0 +1,24 @@
export default 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);
}
}

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore";
@ -16,9 +16,19 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const { id } = useParams();
const navigate = useNavigate();
const flows = useFlowsManagerStore((state) => state.flows);
// Set flow tab id
useEffect(() => {
const isAnExistingFlow = flows.some((flow) => flow.id === id);
if (!isAnExistingFlow) {
navigate("/all");
return;
}
setCurrentFlowId(id!);
setOnFlowPage(true);

View file

@ -19,6 +19,11 @@ const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
const [tabActive, setTabActive] = useState("Flows");
const setErrorData = useAlertStore((state) => state.setErrorData);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const [inputValue, setInputValue] = useState("");
const setSearchFlowsComponents = useFlowsManagerStore(
(state) => state.setSearchFlowsComponents,
);
const handleDownloadFolder = () => {
if (allFlows.length === 0) {
@ -34,8 +39,19 @@ const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
return (
<>
<div className="relative flex items-end gap-4">
<InputSearchComponent loading={isLoading} />
<InputSearchComponent
loading={isLoading}
value={inputValue}
onChange={(e) => {
setSearchFlowsComponents(e.target.value);
setInputValue(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchFlowsComponents(inputValue);
}
}}
/>
<TabsSearchComponent
tabsOptions={["All", "Flows", "Components"]}
setActiveTab={setTabActive}

View file

@ -1,22 +1,27 @@
import { useState } from "react";
import { ChangeEvent, KeyboardEvent } from "react";
import { Input } from "../../../../../../components/ui/input";
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
type InputSearchComponentProps = {
loading: boolean;
divClasses?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onClick?: () => void;
value: string;
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
};
const InputSearchComponent = ({ loading }: InputSearchComponentProps) => {
const InputSearchComponent = ({
loading,
divClasses,
onChange,
onClick,
value,
onKeyDown,
}: InputSearchComponentProps) => {
const pagePath = window.location.pathname;
const [inputValue, setInputValue] = useState("");
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const setSearchFlowsComponents = useFlowsManagerStore(
(state) => state.setSearchFlowsComponents,
);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
);
@ -38,24 +43,18 @@ const InputSearchComponent = ({ loading }: InputSearchComponentProps) => {
return (
<>
<div className="relative h-12 w-[60%]">
<div className={`${divClasses ? divClasses : "relative h-12 w-[60%]"}`}>
<Input
data-testid="search-store-input"
disabled={disableInputSearch}
placeholder={getSearchPlaceholder()}
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setSearchFlowsComponents(e.target.value);
setInputValue(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchFlowsComponents(inputValue);
}
}}
value={inputValue}
onChange={onChange}
onKeyDown={onKeyDown}
value={value}
/>
<button
onClick={onClick}
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
data-testid="search-store-button"

View file

@ -6,13 +6,14 @@ export type FolderType = {
id?: string | null;
parent_id: string;
flows: FlowType[];
components: string[];
};
export type AddFolderType = {
name: string;
description: string;
id?: string | null;
parent_id: string;
parent_id: string | null;
flows?: string[];
components?: string[];
};

View file

@ -1,7 +1,7 @@
import { BASE_URL_API } from "../../../constants/constants";
import { api } from "../../../controllers/API/api";
import { FlowType } from "../../../types/flow";
import { AddFolderType, FolderType, StarterProjectsType } from "../entities";
import { AddFolderType, FolderType } from "../entities";
export async function getFolders(): Promise<FolderType[]> {
try {
@ -61,15 +61,6 @@ export async function getFolderById(folderId: string): Promise<FolderType> {
}
}
export async function getStarterProjects(): Promise<StarterProjectsType> {
try {
const response = await api.get(`${BASE_URL_API}folders/starter-projects`);
return response?.data;
} catch (error) {
throw error;
}
}
export async function downloadFlowsFromFolders(folderId: string): Promise<{
flows: FlowType[];
folder_name: string;

View file

@ -11,7 +11,7 @@ import { Badge } from "../../../../components/ui/badge";
import { Card, CardContent } from "../../../../components/ui/card";
import { deleteGlobalVariable } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
import { useGlobalVariablesStore } from "../../../../stores/globalVariables";
import { useGlobalVariablesStore } from "../../../../stores/globalVariablesStore/globalVariables";
import { cn } from "../../../../utils/utils";
export default function GlobalVariablesPage() {
@ -55,6 +55,7 @@ export default function GlobalVariablesPage() {
name: string;
default_fields: string | undefined;
}> = [];
if (globalVariablesEntries === undefined) return;
globalVariablesEntries.forEach((entrie) => {
const globalVariableObj = globalVariables[entrie];
rows.push({

View file

@ -35,6 +35,7 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils";
import InputSearchComponent from "../MainPage/components/myCollectionComponent/components/inputSearchComponent";
export default function StorePage(): JSX.Element {
const hasApiKey = useStoreStore((state) => state.hasApiKey);
@ -47,7 +48,7 @@ export default function StorePage(): JSX.Element {
const setErrorData = useAlertStore((state) => state.setErrorData);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
(state) => state.setCurrentFlowId,
);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [loading, setLoading] = useState(true);
@ -144,7 +145,7 @@ export default function StorePage(): JSX.Element {
setTotalRowsCount(
filteredCategories?.length === 0
? Number(res?.count ?? 0)
: res?.results?.length ?? 0
: res?.results?.length ?? 0,
);
}
})
@ -187,7 +188,7 @@ export default function StorePage(): JSX.Element {
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : ""
loading ? "cursor-not-allowed" : "",
)}
variant="primary"
>
@ -202,36 +203,20 @@ export default function StorePage(): JSX.Element {
<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
data-testid="search-store-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={() => {
<InputSearchComponent
loading={loading}
divClasses="relative h-12 w-[40%]"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchNow(uniqueId());
}}
data-testid="search-store-button"
>
<IconComponent
name={loading ? "Loader2" : "Search"}
className={loading ? " animate-spin cursor-not-allowed" : ""}
/>
</button>
</div>
}
}}
onClick={() => {
setSearchNow(uniqueId());
}}
/>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
data-testid="all-button-store"

View file

@ -36,7 +36,7 @@ export const useDarkStore = create<DarkStoreType>((set, get) => ({
window.localStorage.setItem("githubStars", res.toString());
window.localStorage.setItem(
"githubStarsLastUpdated",
new Date().toString()
new Date().toString(),
);
set(() => ({ stars: res, lastUpdated: new Date() }));
});

View file

@ -11,7 +11,6 @@ import {
updateFlowInDatabase,
uploadFlowsToDatabase,
} from "../controllers/API";
import { getStarterProjects } from "../pages/MainPage/services";
import { FlowType, NodeDataType } from "../types/flow";
import {
FlowsManagerStoreType,
@ -81,38 +80,40 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
return new Promise<void>((resolve, reject) => {
set({ isLoading: true });
getStarterProjects().then((starterProjects) => {
get().setExamples(starterProjects?.flows!);
const starterFolderId = useFolderStore.getState().starterProjectId;
readFlowsFromDatabase()
.then((dbData) => {
if (dbData) {
const { data, flows } = processFlows(dbData, false);
const starterProjectsIds = starterProjects.flows!.map(
(flow) => flow.id,
);
get().setFlows(
flows.filter((f) => !starterProjectsIds.includes(f.id)),
);
useTypesStore.setState((state) => ({
data: { ...state.data, ["saved_components"]: data },
ComponentFields: extractFieldsFromComponenents({
...state.data,
["saved_components"]: data,
}),
}));
set({ isLoading: false });
resolve();
}
})
.catch((e) => {
readFlowsFromDatabase()
.then((dbData) => {
if (dbData) {
const { data, flows } = processFlows(dbData, false);
const examples = flows.filter(
(flow) => flow.folder_id === starterFolderId,
);
get().setExamples(examples);
const flowsWithoutStarterFolder = flows.filter(
(flow) => flow.folder_id !== starterFolderId,
);
get().setFlows(flowsWithoutStarterFolder);
useTypesStore.setState((state) => ({
data: { ...state.data, ["saved_components"]: data },
ComponentFields: extractFieldsFromComponenents({
...state.data,
["saved_components"]: data,
}),
}));
set({ isLoading: false });
useAlertStore.getState().setErrorData({
title: "Could not load flows from database",
});
reject(e);
resolve();
}
})
.catch((e) => {
set({ isLoading: false });
useAlertStore.getState().setErrorData({
title: "Could not load flows from database",
});
});
reject(e);
});
});
},
autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => {
@ -204,11 +205,16 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
const folder_id = useFolderStore.getState().folderUrl;
const my_collection_id = useFolderStore.getState().myCollectionId;
if (override) {
get().deleteComponent(flow!.name);
const newFlow = createNewFlow(flowData!, flow!);
newFlow.folder_id = useFolderStore.getState().folderUrl;
const newFlow = createNewFlow(
flowData!,
flow!,
folder_id || my_collection_id!,
);
const { id } = await saveFlowToDatabase(newFlow);
newFlow.id = id;
//setTimeout to prevent update state with wrong state
@ -227,8 +233,12 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
// addFlowToLocalState(newFlow);
return;
}
const newFlow = createNewFlow(flowData!, flow!);
console.log("folder id", folder_id);
const newFlow = createNewFlow(
flowData!,
flow!,
folder_id || my_collection_id!,
);
const newName = addVersionToDuplicates(newFlow, get().flows);

View file

@ -1,5 +1,5 @@
import { create } from "zustand";
import { DEFAULT_FOLDER } from "../constants/constants";
import { DEFAULT_FOLDER, STARTER_FOLDER_NAME } from "../constants/constants";
import {
getFolderById,
getFolders,
@ -11,24 +11,44 @@ import useFlowsManagerStore from "./flowsManagerStore";
export const useFolderStore = create<FoldersStoreType>((set, get) => ({
folders: [],
getFoldersApi: (refetch = false) => {
if (get()?.folders.length === 0 || refetch === true) {
get().setLoading(true);
getFolders().then(
(res) => {
set({ folders: res });
const myCollectionId = res?.find(
(f) => f.name === DEFAULT_FOLDER,
)?.id;
set({ myCollectionId });
get().setLoading(false);
useFlowsManagerStore.getState().refreshFlows();
},
() => {
set({ folders: [] });
get().setLoading(false);
},
);
}
return new Promise<void>((resolve, reject) => {
if (get()?.folders.length === 0 || refetch === true) {
get().setLoading(true);
getFolders().then(
(res) => {
const foldersWithoutStarterProjects = res.filter(
(folder) => folder.name !== STARTER_FOLDER_NAME,
);
const starterProjects = res.find(
(folder) => folder.name === STARTER_FOLDER_NAME,
);
set({ starterProjectId: starterProjects!.id ?? "" });
set({ folders: foldersWithoutStarterProjects });
const myCollectionId = res?.find(
(f) => f.name === DEFAULT_FOLDER,
)?.id;
set({ myCollectionId });
if (refetch === true) {
useFlowsManagerStore.getState().refreshFlows();
useFlowsManagerStore.getState().setAllFlows;
}
get().setLoading(false);
resolve();
},
() => {
set({ folders: [] });
get().setLoading(false);
reject();
},
);
}
});
},
setFolders: (folders) => set(() => ({ folders: folders })),
loading: false,
@ -93,12 +113,12 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
formData.append("file", file);
uploadFlowsFromFolders(formData).then(() => {
get().getFoldersApi(true);
useFlowsManagerStore.getState().refreshFlows();
});
useFlowsManagerStore.getState().setAllFlows;
}
};
input.click();
});
},
starterProjectId: "",
setStarterProjectId: (id) => set(() => ({ starterProjectId: id })),
}));

View file

@ -1,6 +1,6 @@
import { create } from "zustand";
import { GlobalVariablesStore } from "../types/zustand/globalVariables";
import { getUnavailableFields } from "../utils/utils";
import { GlobalVariablesStore } from "../../types/zustand/globalVariables";
import getUnavailableFields from "./utils/get-unavailable-fields";
export const useGlobalVariablesStore = create<GlobalVariablesStore>(
(set, get) => ({
@ -13,7 +13,7 @@ export const useGlobalVariablesStore = create<GlobalVariablesStore>(
delete newFields[field];
set({ unavaliableFields: newFields });
},
globalVariablesEntries: [],
globalVariablesEntries: undefined,
globalVariables: {},
setGlobalVariables: (variables) => {
set({
@ -45,5 +45,5 @@ export const useGlobalVariablesStore = create<GlobalVariablesStore>(
getVariableId: (name) => {
return get().globalVariables[name]?.id;
},
})
}),
);

View file

@ -0,0 +1,13 @@
export default function getUnavailableFields(variables: {
[key: string]: { default_fields?: string[] };
}): { [name: string]: string } {
const unVariables: { [name: string]: string } = {};
Object.keys(variables).forEach((key) => {
if (variables[key].default_fields) {
variables[key].default_fields!.forEach((field) => {
unVariables[field] = key;
});
}
});
return unVariables;
}

View file

@ -0,0 +1,21 @@
import { create } from "zustand";
import { LocationStoreType } from "../types/zustand/location";
export const useLocationStore = create<LocationStoreType>((set, get) => ({
routeHistory: [],
setRouteHistory: (location) => {
let routeHistoryArray = get().routeHistory;
routeHistoryArray.push(location);
if (routeHistoryArray?.length > 9) {
routeHistoryArray.shift();
set({
routeHistory: routeHistoryArray,
});
}
set({
routeHistory: routeHistoryArray,
});
},
}));

View file

@ -6,6 +6,7 @@ import { ChatMessageType } from "../chat";
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
import { sourceHandleType, targetHandleType } from "./../flow/index";
export type InputComponentType = {
name?: string;
autoFocus?: boolean;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
value?: string;
@ -32,6 +33,7 @@ export type InputComponentType = {
setSelectedOptions?: (value: string[]) => void;
objectOptions?: Array<{ name: string; id: string }>;
isObjectOption?: boolean;
onChangeFolderName?: (e: any) => void;
};
export type ToggleComponentType = {
enabled: boolean;

View file

@ -2,7 +2,7 @@ import { FolderType } from "../../../pages/MainPage/entities";
export type FoldersStoreType = {
folders: FolderType[];
getFoldersApi: (refetch?: boolean) => void;
getFoldersApi: (refetch?: boolean) => Promise<void>;
setFolders: (folders: FolderType[]) => void;
loading: boolean;
setLoading: (loading: boolean) => void;
@ -23,4 +23,6 @@ export type FoldersStoreType = {
uploadFolder: (folderId: string) => void;
folderIdDragging: string;
setFolderIdDragging: (id: string) => void;
starterProjectId: string;
setStarterProjectId: (id: string) => void;
};

View file

@ -1,5 +1,5 @@
export type GlobalVariablesStore = {
globalVariablesEntries: Array<string>;
globalVariablesEntries: Array<string> | undefined;
globalVariables: {
[name: string]: {
id: string;

View file

@ -0,0 +1,4 @@
export type LocationStoreType = {
routeHistory: string[];
setRouteHistory: (location: string) => void;
};

View file

@ -76,7 +76,6 @@ export async function updateVerticesOrder(
title: "Oops! Looks like you missed something",
list: [error.response?.data?.detail ?? "Unknown Error"],
});
debugger;
useFlowStore.getState().setIsBuilding(false);
throw new Error("Invalid nodes");
}

View file

@ -36,12 +36,9 @@ import {
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import {
createRandomKey,
getFieldTitle,
getRandomDescription,
toTitleCase,
} from "./utils";
import { createRandomKey, toTitleCase } from "./utils";
import { DESCRIPTIONS } from "../flow_constants";
import getFieldTitle from "../customNodes/utils/get-field-title";
const uid = new ShortUniqueId({ length: 5 });
export function checkChatInput(nodes: Node[]) {
@ -353,7 +350,12 @@ export function updateEdges(edges: Edge[]) {
}
export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
const existingNames = flows.map((item) => item.name);
console.log("flow", flow);
console.log("flows", flows);
const existingNames = flows
.filter((f) => f.folder_id === flow.folder_id)
.map((item) => item.name);
console.log("existingNames", existingNames);
let newName = flow.name;
let count = 1;
@ -1289,9 +1291,18 @@ export function downloadFlows() {
});
}
export function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
export function getRandomDescription(): string {
return getRandomElement(DESCRIPTIONS);
}
export const createNewFlow = (
flowData: ReactFlowJsonObject,
flow: FlowType,
folderId: string,
) => {
return {
description: flow?.description ?? getRandomDescription(),
@ -1299,7 +1310,7 @@ export const createNewFlow = (
data: flowData,
id: "",
is_component: flow?.is_component ?? false,
folder_id: "",
folder_id: folderId,
};
};

View file

@ -20,15 +20,6 @@ export default function cloneFLowWithParent(
return childFLow;
}
export function getTagsIds(
tags: string[],
tagListId: { name: string; id: string }[],
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
}
export function getInputsAndOutputs(nodes: Node[]) {
let inputs: { type: string; id: string; displayName: string }[] = [];
let outputs: { type: string; id: string; displayName: string }[] = [];

View file

@ -156,6 +156,7 @@ import { AzureIcon } from "../icons/Azure";
import { BingIcon } from "../icons/Bing";
import { BotMessageSquareIcon } from "../icons/BotMessageSquare";
import { ChromaIcon } from "../icons/ChromaIcon";
import { CouchbaseIcon } from "../icons/Couchbase";
import { CohereIcon } from "../icons/Cohere";
import { ElasticsearchIcon } from "../icons/ElasticsearchStore";
import { EvernoteIcon } from "../icons/Evernote";
@ -327,6 +328,7 @@ export const nodeIconsLucide: iconsType = {
Vectara: VectaraIcon,
ArrowUpToLine: ArrowUpToLine,
Chroma: ChromaIcon,
Couchbase: CouchbaseIcon,
AirbyteJSONLoader: AirbyteIcon,
AmazonBedrockEmbeddings: AWSIcon,
Amazon: AWSIcon,

View file

@ -4,13 +4,8 @@ import { twMerge } from "tailwind-merge";
import TableAutoCellRender from "../components/tableAutoCellRender";
import { priorityFields } from "../constants/constants";
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,
@ -92,20 +87,6 @@ export function toTitleCase(
.join(" ");
}
export function getUnavailableFields(variables: {
[key: string]: { default_fields?: string[] };
}): { [name: string]: string } {
const unVariables: { [name: string]: string } = {};
Object.keys(variables).forEach((key) => {
if (variables[key].default_fields) {
variables[key].default_fields!.forEach((field) => {
unVariables[field] = key;
});
}
});
return unVariables;
}
export const upperCaseWords: string[] = ["llm", "uri"];
export function checkUpperWords(str: string): string {
const words = str.split(" ").map((word) => {
@ -117,8 +98,121 @@ export function checkUpperWords(str: string): string {
return words.join(" ");
}
export const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`);
export function buildInputs(): string {
return '{"input_value": "message"}';
}
export function getRandomKeyByssmm(): string {
const now = new Date();
const seconds = String(now.getSeconds()).padStart(2, "0");
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
return seconds + milliseconds + Math.abs(Math.floor(Math.random() * 10001));
}
export function buildTweakObject(tweak: tweakType) {
tweak.forEach((el) => {
Object.keys(el).forEach((key) => {
for (let kp in el[key]) {
try {
el[key][kp] = JSON.parse(el[key][kp]);
} catch {}
}
});
});
const tweakString = JSON.stringify(tweak.at(-1), null, 2);
return tweakString;
}
/**
* Function to get Chat Input Field
* @param {FlowsState} tabsState - The current tabs state.
* @returns {string} - The chat input field
*/
export function getChatInputField(flowState?: FlowState) {
let chat_input_field = "text";
if (flowState && flowState.input_keys) {
chat_input_field = Object.keys(flowState.input_keys!)[0];
}
return chat_input_field;
}
export function getOutputIds(flow) {
const nodes = flow.data!.nodes;
const arrayOfOutputs = nodes.reduce((acc: string[], node) => {
if (node.data.type.toLowerCase().includes("output")) {
acc.push(node.id);
}
return acc;
}, []);
const arrayOfOutputsJoin = arrayOfOutputs
.map((output) => `"${output}"`)
.join(", ");
return arrayOfOutputsJoin;
}
export function truncateLongId(id: string): string {
let [componentName, newId] = id.split("-");
if (componentName.length > 15) {
componentName = componentName.slice(0, 15);
componentName += "...";
return componentName + "-" + newId;
}
return id;
}
export function extractIdFromLongId(id: string): string {
let [_, newId] = id.split("-");
return newId;
}
export function truncateDisplayName(name: string): string {
if (name.length > 15) {
name = name.slice(0, 15);
name += "...";
}
return name;
}
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 extractTypeFromLongId(id: string): string {
let [newId, _] = id.split("-");
return newId;
}
export function createRandomKey(key: string, uid: string): string {
return removeCountFromString(key) + ` (${uid})`;
}
export function groupByFamily(
data: APIDataType,
@ -233,398 +327,6 @@ export function groupByFamily(
}));
}
export function buildInputs(): string {
return '{"input_value": "message"}';
}
export function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
export function getRandomDescription(): string {
return getRandomElement(DESCRIPTIONS);
}
export function getRandomName(
retry: number = 0,
noSpace: boolean = false,
maxRetries: number = 3,
): string {
const left: string[] = ADJECTIVES;
const right: string[] = NOUNS;
const lv = getRandomElement(left);
const rv = getRandomElement(right);
// Condition to avoid "boring wozniak"
if (lv === "boring" && rv === "wozniak") {
if (retry < maxRetries) {
return getRandomName(retry + 1, noSpace, maxRetries);
} else {
console.warn("Max retries reached, returning as is");
}
}
// Append a suffix if retrying and noSpace is true
if (retry > 0 && noSpace) {
const retrySuffix = Math.floor(Math.random() * 10);
return `${lv}_${rv}${retrySuffix}`;
}
// Construct the final name
let final_name = noSpace ? `${lv}_${rv}` : `${lv} ${rv}`;
// Return title case final name
return toTitleCase(final_name);
}
export function getRandomKeyByssmm(): string {
const now = new Date();
const seconds = String(now.getSeconds()).padStart(2, "0");
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
return seconds + milliseconds + Math.abs(Math.floor(Math.random() * 10001));
}
export function varHighlightHTML({ name }: IVarHighlightType): string {
const html = `<span class="font-semibold chat-message-highlight">{${name}}</span>`;
return html;
}
export function buildTweakObject(tweak: tweakType) {
tweak.forEach((el) => {
Object.keys(el).forEach((key) => {
for (let kp in el[key]) {
try {
el[key][kp] = JSON.parse(el[key][kp]);
} catch {}
}
});
});
const tweakString = JSON.stringify(tweak.at(-1), null, 2);
return tweakString;
}
/**
* Function to get Chat Input Field
* @param {FlowsState} tabsState - The current tabs state.
* @returns {string} - The chat input field
*/
export function getChatInputField(flowState?: FlowState) {
let chat_input_field = "text";
if (flowState && flowState.input_keys) {
chat_input_field = Object.keys(flowState.input_keys!)[0];
}
return chat_input_field;
}
/**
* Function to get the python code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export function getPythonApiCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
from typing import Optional
BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str,
flow_id: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {
"input_value": message,
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
message = "message"
${!isAuth ? `api_key = "<your api key>"` : ""}
print(run_flow(message=message, flow_id=FLOW_ID, tweaks=TWEAKS${
!isAuth ? `, api_key=api_key` : ""
}))`;
}
/**
* Function to get the curl code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code
*/
export function getCurlCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
}/api/v1/run/${flowId}?stream=false \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}}'
`;
}
export function getOutputIds(flow) {
const nodes = flow.data!.nodes;
const arrayOfOutputs = nodes.reduce((acc: string[], node) => {
if (node.data.type.toLowerCase().includes("output")) {
acc.push(node.id);
}
return acc;
}, []);
const arrayOfOutputsJoin = arrayOfOutputs
.map((output) => `"${output}"`)
.join(", ");
return arrayOfOutputsJoin;
}
/**
* Function to get the python code for the API
* @param {string} flow - The current flow
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export function getPythonCode(flowName: string, tweaksBuildedObject): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}
/**
* Function to get the widget code for the API
* @param {string} flow - The current flow.
* @returns {string} - The widget code
*/
export function getWidgetCode(
flowId: string,
flowName: string,
isAuth: boolean,
): string {
return `<script src="https://cdn.jsdelivr.net/gh/langflow-ai/langflow-embedded-chat@1.0_alpha/dist/build/static/js/bundle.min.js"></script>
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
host_url="http://localhost:7860"${
!isAuth
? `
api_key="..."`
: ""
}
></langflow-chat>`;
}
export function truncateLongId(id: string): string {
let [componentName, newId] = id.split("-");
if (componentName.length > 15) {
componentName = componentName.slice(0, 15);
componentName += "...";
return componentName + "-" + newId;
}
return id;
}
export function extractIdFromLongId(id: string): string {
let [_, newId] = id.split("-");
return newId;
}
export function truncateDisplayName(name: string): string {
if (name.length > 15) {
name = name.slice(0, 15);
name += "...";
}
return name;
}
export function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
];
}
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
language: "py",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[4],
},
];
}
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 extractTypeFromLongId(id: string): string {
let [newId, _] = id.split("-");
return newId;
}
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>();
@ -640,67 +342,10 @@ export function getSetFromObject(obj: object, key?: string): Set<string> {
return set;
}
export function getFieldTitle(
template: APITemplateType,
templateField: string,
): string {
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name ?? templateField;
}
export function sortFields(a, b, fieldOrder) {
// Early return for empty fields
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
// Normalize the case to ensure case-insensitive comparison
const normalizedFieldA = a.toLowerCase();
const normalizedFieldB = b.toLowerCase();
const aIsPriority = priorityFields.has(normalizedFieldA);
const bIsPriority = priorityFields.has(normalizedFieldB);
// Sort by priority
if (aIsPriority && !bIsPriority) return -1;
if (!aIsPriority && bIsPriority) return 1;
// Check if either field is in the fieldOrder array
const indexOfA = fieldOrder.indexOf(normalizedFieldA);
const indexOfB = fieldOrder.indexOf(normalizedFieldB);
// If both fields are in fieldOrder, sort by their order in the array
if (indexOfA !== -1 && indexOfB !== -1) {
return indexOfA - indexOfB;
}
// If only one of the fields is in fieldOrder, that field comes first
if (indexOfA !== -1) {
return -1;
}
if (indexOfB !== -1) {
return 1;
}
// Default case for fields not in priorityFields and not found in fieldOrder
// You might want to sort them alphabetically or in another specific manner
return a.localeCompare(b);
}
export function freezeObject(obj: any) {
if (!obj) return obj;
return JSON.parse(JSON.stringify(obj));
}
export function convertTestName(name: string): string {
return name.replace(/ /g, "-").toLowerCase();
}
export function sortByName(stringList: string[]): string[] {
return stringList.sort((a, b) => a.localeCompare(b));
}
export function isTimeStampString(str: string): boolean {
const timestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3}Z)?$/;
return timestampRegex.test(str);

View file

@ -1,101 +1,10 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test("user must interact with chat with Input/Output", async ({ page }) => {
if (!process.env.CI) {
dotenv.config();
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
}
await page.goto("/");
await page.waitForTimeout(1000);
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForTimeout(1000);
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
if (!process.env.OPENAI_API_KEY) {
//You must set the OPENAI_API_KEY on .env file to run this test
expect(false).toBe(true);
}
await page
.getByTestId("popover-anchor-input-openai_api_key")
.fill(process.env.OPENAI_API_KEY ?? "");
await page.getByText("Playground", { exact: true }).click();
await page.getByPlaceholder("Send a message...").fill("Hello, how are you?");
await page.getByTestId("icon-LucideSend").click();
let valueUser = await page.getByTestId("sender_name_user").textContent();
let valueAI = await page.getByTestId("sender_name_ai").textContent();
expect(valueUser).toBe("User");
expect(valueAI).toBe("AI");
await page.keyboard.press("Escape");
await page
.getByTestId("textarea-input_value")
.nth(1)
.fill(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
);
await page
.getByTestId("popover-anchor-input-sender_name")
.nth(1)
.fill("TestSenderNameUser");
await page
.getByTestId("popover-anchor-input-sender_name")
.nth(0)
.fill("TestSenderNameAI");
await page.getByText("Playground", { exact: true }).click();
await page.getByTestId("icon-LucideSend").click();
valueUser = await page
.getByTestId("sender_name_testsendernameuser")
.textContent();
valueAI = await page
.getByTestId("sender_name_testsendernameai")
.textContent();
expect(valueUser).toBe("TestSenderNameUser");
expect(valueAI).toBe("TestSenderNameAI");
expect(
await page
.getByText(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
{ exact: true },
)
.isVisible(),
);
});
test("chat_io_teste", async ({ page }) => {
await page.goto("/");
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
await page.waitForTimeout(3000);
await page.waitForTimeout(2000);
let modalCount = 0;
try {

View file

@ -0,0 +1,92 @@
import { expect, test } from "@playwright/test";
test("user must interact with chat with Input/Output", async ({ page }) => {
if (!process.env.CI) {
dotenv.config();
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
}
await page.goto("/");
await page.waitForTimeout(1000);
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForTimeout(1000);
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
if (!process.env.OPENAI_API_KEY) {
//You must set the OPENAI_API_KEY on .env file to run this test
expect(false).toBe(true);
}
await page
.getByTestId("popover-anchor-input-openai_api_key")
.fill(process.env.OPENAI_API_KEY ?? "");
await page.getByText("Playground", { exact: true }).click();
await page.getByPlaceholder("Send a message...").fill("Hello, how are you?");
await page.getByTestId("icon-LucideSend").click();
let valueUser = await page.getByTestId("sender_name_user").textContent();
let valueAI = await page.getByTestId("sender_name_ai").textContent();
expect(valueUser).toBe("User");
expect(valueAI).toBe("AI");
await page.keyboard.press("Escape");
await page
.getByTestId("textarea-input_value")
.nth(1)
.fill(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
);
await page
.getByTestId("popover-anchor-input-sender_name")
.nth(1)
.fill("TestSenderNameUser");
await page
.getByTestId("popover-anchor-input-sender_name")
.nth(0)
.fill("TestSenderNameAI");
await page.getByText("Playground", { exact: true }).click();
await page.getByTestId("icon-LucideSend").click();
valueUser = await page
.getByTestId("sender_name_testsendernameuser")
.textContent();
valueAI = await page
.getByTestId("sender_name_testsendernameai")
.textContent();
expect(valueUser).toBe("TestSenderNameUser");
expect(valueAI).toBe("TestSenderNameAI");
expect(
await page
.getByText(
"testtesttesttesttesttestte;.;.,;,.;,.;.,;,..,;;;;;;;;;;;;;;;;;;;;;,;.;,.;,.,;.,;.;.,~~çççççççççççççççççççççççççççççççççççççççisdajfdasiopjfaodisjhvoicxjiovjcxizopjviopasjioasfhjaiohf23432432432423423sttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestççççççççççççççççççççççççççççççççç,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,!",
{ exact: true },
)
.isVisible(),
);
});

View file

@ -43,7 +43,7 @@ test("GlobalVariables", async ({ page }) => {
const genericName = Math.random().toString();
const credentialName = Math.random().toString();
await page.getByTestId("icon-Globe").nth(1).click();
await page.getByTestId("icon-Globe").nth(0).click();
await page.getByText("Add New Variable", { exact: true }).click();
await page
.getByPlaceholder("Insert a name for the variable...")
@ -69,6 +69,9 @@ test("GlobalVariables", async ({ page }) => {
await page.getByText("Save Variable", { exact: true }).click();
expect(page.getByText(credentialName, { exact: true })).not.toBeNull();
await page.getByText(credentialName, { exact: true }).isVisible();
await page.getByText("Save Variable", { exact: true }).click();
await page.waitForTimeout(2000);
await page
.getByText(credentialName, { exact: true })
.hover()

View file

@ -28,7 +28,7 @@ test("InputListComponent", async ({ page }) => {
await page.waitForTimeout(1000);
await page
.getByTestId("vectorsearchAstra DB Search")
.getByTestId("vectorstoresAstra DB")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();