Merge cz/mergeAll to two_edges

This commit is contained in:
ogabrielluiz 2024-06-10 11:31:02 -03:00
commit 735a7e3780
333 changed files with 6448 additions and 2498 deletions

View file

@ -60,6 +60,7 @@
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.51.4",
"react-hotkeys-hook": "^4.5.0",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
@ -1923,12 +1924,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"dependencies": {
"playwright": "1.44.0"
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
@ -5988,9 +5989,9 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.778",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.778.tgz",
"integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA=="
"version": "1.4.780",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.780.tgz",
"integrity": "sha512-NPtACGFe7vunRYzvYqVRhQvsDrTevxpgDKxG/Vcbe0BTNOY+5+/2mOXSw2ls7ToNbE5Bf/+uQbjTxcmwMozpCw=="
},
"node_modules/emoji-regex": {
"version": "10.3.0",
@ -7618,6 +7619,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"devOptional": true,
"dependencies": {
"once": "^1.3.0",
@ -10303,11 +10305,11 @@
}
},
"node_modules/playwright": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dependencies": {
"playwright-core": "1.44.0"
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
@ -10320,9 +10322,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"bin": {
"playwright-core": "cli.js"
},
@ -11021,6 +11023,15 @@
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-hotkeys-hook": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
"integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
"peerDependencies": {
"react": ">=16.8.1",
"react-dom": ">=16.8.1"
}
},
"node_modules/react-icons": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",

View file

@ -6,6 +6,7 @@
"@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",

View file

@ -45,6 +45,9 @@ export default defineConfig({
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: {
// headless: false,
},
contextOptions: {
// chromium-specific permissions
permissions: ["clipboard-read", "clipboard-write"],
@ -57,6 +60,7 @@ export default defineConfig({
// use: {
// ...devices["Desktop Firefox"],
// launchOptions: {
// headless: false,
// firefoxUserPrefs: {
// "dom.events.asyncClipboard.readText": true,
// "dom.events.testing.asyncClipboard": true,

View file

@ -174,3 +174,12 @@ body {
border: none !important;
outline: none;
}
/* selected */
.react-flow__edge.selected .react-flow__edge-path {
stroke: var(--selected) !important;
}
.react-flow__edge .react-flow__edge-path {
stroke: var(--connection) !important;
}

View file

@ -1,10 +1,9 @@
import { useRef } from "react";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { groupByFamily } from "../../../../utils/utils";
import TooltipRenderComponent from "../tooltipRenderComponent";
import useFlowStore from "../../../../stores/flowStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { NodeType } from "../../../../types/flow";
import useFlowStore from "../../../../stores/flowStore";
import { groupByFamily } from "../../../../utils/utils";
import TooltipRenderComponent from "../tooltipRenderComponent";
export default function HandleTooltips({
left,

View file

@ -1,6 +1,8 @@
import { cloneDeep } from "lodash";
import { useUpdateNodeInternals } from "reactflow";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Button } from "../../../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@ -11,8 +13,6 @@ import useFlowStore from "../../../../stores/flowStore";
import { outputComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import { cn } from "../../../../utils/utils";
import { Button } from "../../../../components/ui/button";
import ShadTooltip from "../../../../components/shadTooltipComponent";
export default function OutputComponent({
selected,
@ -40,7 +40,7 @@ export default function OutputComponent({
size="xs"
className={cn(
frozen ? "text-ice" : "",
"items-center gap-1 pl-2 pr-1.5 align-middle text-xs font-normal",
"items-center gap-1 pl-2 pr-1.5 align-middle text-xs font-normal"
)}
>
<span className="pb-px">{selected}</span>

View file

@ -0,0 +1,12 @@
import { Textarea } from "../../../../../../../components/ui/textarea";
export default function ErrorOutput({ value }: { value: string }) {
return (
<Textarea
className={`h-full w-full text-destructive custom-scroll`}
placeholder={"Empty"}
value={value}
readOnly
/>
);
}

View file

@ -0,0 +1,4 @@
export const convertToTableRows = (obj: Object) => {
const tokensArray = [Object.values(obj)[0]];
return tokensArray;
};

View file

@ -0,0 +1,77 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
import RecordsOutputComponent from "../../../../../../components/recordsOutputComponent";
import {
Alert,
AlertDescription,
AlertTitle,
} from "../../../../../../components/ui/alert";
import { Case } from "../../../../../../shared/components/caseComponent";
import TextOutputView from "../../../../../../shared/components/textOutputView";
import useFlowStore from "../../../../../../stores/flowStore";
import ErrorOutput from "./components";
export default function SwitchOutputView(nodeId): JSX.Element {
const nodeIdentity = nodeId.nodeId;
const nodes = useFlowStore((state) => state.nodes);
const flowPool = useFlowStore((state) => state.flowPool);
const node = nodes.find((node) => node?.id === nodeIdentity);
const flowPoolNode = (flowPool[nodeIdentity] ?? [])[
(flowPool[nodeIdentity]?.length ?? 1) - 1
];
const results = flowPoolNode?.data?.logs[0] ?? "";
const resultType = results?.type;
let resultMessage = results?.message;
const RECORD_TYPES = ["record", "object", "array", "message"];
if (resultMessage.raw) {
resultMessage = resultMessage.raw;
}
console.log("resultType", results);
return (
<>
<Case condition={!resultType || resultType === "unknown"}>
<div>NO OUTPUT</div>
</Case>
<Case condition={resultType === "ValueError"}>
<ErrorOutput value={resultMessage}></ErrorOutput>
</Case>
<Case condition={node && resultType === "text"}>
<TextOutputView left={false} value={resultMessage} />
</Case>
<Case condition={RECORD_TYPES.includes(resultType)}>
<RecordsOutputComponent
rows={
Array.isArray(resultMessage)
? (resultMessage as Array<any>).every((item) => item.data)
? (resultMessage as Array<any>).map((item) => item.data)
: resultMessage
: [resultMessage]
}
pagination={true}
columnMode="union"
/>
</Case>
<Case condition={resultType === "stream"}>
<div className="flex h-full w-full items-center justify-center align-middle">
<Alert variant={"default"} className="w-fit">
<ForwardedIconComponent
name="AlertCircle"
className="h-5 w-5 text-primary"
/>
<AlertTitle>{"Streaming is not supported"}</AlertTitle>
<AlertDescription>
{
"Use the playground to interact with components that stream data"
}
</AlertDescription>
</Alert>
</div>
</Case>
</>
);
}

View file

@ -0,0 +1,25 @@
import { Button } from "../../../../components/ui/button";
import BaseModal from "../../../../modals/baseModal";
import SwitchOutputView from "./components/switchOutputView";
export default function OutputModal({ open, setOpen, nodeId }): JSX.Element {
return (
<BaseModal open={open} setOpen={setOpen} size="medium-tall">
<BaseModal.Header description="Inspect the output of the component below.">
<div className="flex items-center">
<span className="pr-2">Component Output</span>
</div>
</BaseModal.Header>
<BaseModal.Content>
<SwitchOutputView nodeId={nodeId} />
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full justify-end pt-2">
<Button className="flex gap-2 px-3" onClick={() => setOpen(false)}>
Close
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -0,0 +1,26 @@
import { NodeDataType } from "../../types/flow";
export function countHandlesFn(data: NodeDataType): number {
let count = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.map((templateCamp) => {
const { template } = data.node!;
if (template[templateCamp].input_types) return true;
if (!template[templateCamp].show) return false;
switch (template[templateCamp].type) {
case "str":
case "bool":
case "float":
case "code":
case "prompt":
case "file":
case "int":
return false;
default:
return true;
}
})
.reduce((total, value) => total + (value ? 1 : 0), 0);
return count;
}

View file

@ -0,0 +1,25 @@
import { BuildStatus } from "../../constants/enums";
import { VertexBuildTypeAPI } from "../../types/api";
export const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null,
isDark: boolean
) => {
let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE) {
// INACTIVE should have its own class
return "inactive-status";
}
if (
(buildStatus === BuildStatus.BUILT && isInvalid) ||
buildStatus === BuildStatus.ERROR
) {
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
} else if (buildStatus === BuildStatus.BUILDING) {
return "building-status";
} else {
return "";
}
};

View file

@ -0,0 +1,40 @@
import { useEffect } from "react";
import { NATIVE_CATEGORIES } from "../../constants/constants";
import { NodeDataType } from "../../types/flow";
const useCheckCodeValidity = (
data: NodeDataType,
templates: { [key: string]: any },
setIsOutdated: (value: boolean) => void,
types
) => {
useEffect(() => {
// This one should run only once
// first check if data.type in NATIVE_CATEGORIES
// if not return
if (
!NATIVE_CATEGORIES.includes(types[data.type]) ||
!data.node?.template?.code?.value
)
return;
const thisNodeTemplate = templates[data.type].template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
const currentCode = thisNodeTemplate.code?.value;
const thisNodesCode = data.node!.template?.code?.value;
const componentsToIgnore = ["CustomComponent", "Prompt"];
if (
currentCode !== thisNodesCode &&
!componentsToIgnore.includes(data.type) &&
!(data.node?.edited ?? false)
) {
setIsOutdated(true);
} else {
setIsOutdated(false);
}
// template.code can be undefined
}, [data.node?.template?.code?.value, templates, setIsOutdated]);
};
export default useCheckCodeValidity;

View file

@ -0,0 +1,45 @@
import { useCallback } from "react";
import { NodeDataType } from "../../types/flow";
const useIconNodeRender = (
data: NodeDataType,
types: { [key: string]: string },
nodeColors: { [key: string]: string },
name: string,
showNode: boolean,
isEmoji: boolean,
nodeIconFragment: (iconElement: string) => JSX.Element,
checkNodeIconFragment: (
iconColor: string,
iconName: string,
iconClassName: string
) => JSX.Element
) => {
const iconNodeRender = useCallback(() => {
const iconElement = data?.node?.icon;
const iconColor = nodeColors[types[data.type]];
const iconName =
iconElement || (data.node?.flow ? "group_components" : name);
const iconClassName = `generic-node-icon ${
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
}`;
if (iconElement && isEmoji) {
return nodeIconFragment(iconElement);
} else {
return checkNodeIconFragment(iconColor, iconName, iconClassName);
}
}, [
data,
types,
nodeColors,
name,
showNode,
isEmoji,
nodeIconFragment,
checkNodeIconFragment,
]);
return iconNodeRender;
};
export default useIconNodeRender;

View file

@ -0,0 +1,54 @@
import IconComponent from "../../components/genericIconComponent";
import Checkmark from "../../components/ui/checkmark";
import Loading from "../../components/ui/loading";
import Xmark from "../../components/ui/xmark";
import { BuildStatus } from "../../constants/enums";
import { VertexBuildTypeAPI } from "../../types/api";
const useIconStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: VertexBuildTypeAPI | null
) => {
const renderIconStatus = () => {
if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />;
} else {
return (
<>
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
/>
{validationStatus && validationStatus.valid ? (
<Checkmark
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
isVisible={true}
/>
) : validationStatus &&
!validationStatus.valid &&
buildStatus === BuildStatus.INACTIVE ? (
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
/>
) : buildStatus === BuildStatus.ERROR ||
(validationStatus && !validationStatus.valid) ? (
<Xmark
isVisible={true}
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
/>
) : (
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
/>
)}
</>
);
}
};
return renderIconStatus();
};
export default useIconStatus;

View file

@ -0,0 +1,39 @@
import { cloneDeep } from "lodash"; // or any other deep cloning library you prefer
import { useCallback } from "react";
import { APIClassType } from "../../types/api";
const useUpdateNodeCode = (
dataId: string,
dataNode: APIClassType, // Define YourNodeType according to your data structure
setNode: (id: string, callback: (oldNode) => any) => void,
setIsOutdated: (value: boolean) => void,
updateNodeInternals: (id: string) => void
) => {
const updateNodeCode = useCallback(
(newNodeClass: APIClassType, code: string, name: string) => {
setNode(dataId, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? dataNode.description,
display_name: newNodeClass.display_name ?? dataNode.display_name,
edited: false,
};
newNode.data.node.template[name].value = code;
setIsOutdated(false);
return newNode;
});
updateNodeInternals(dataId);
},
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals]
);
return updateNodeCode;
};
export default useUpdateNodeCode;

View file

@ -0,0 +1,18 @@
import { useEffect } from "react";
const useUpdateValidationStatus = (dataId, flowPool, setValidationStatus) => {
useEffect(() => {
const relevantData =
flowPool[dataId] && flowPool[dataId]?.length > 0
? flowPool[dataId][flowPool[dataId].length - 1]
: null;
if (relevantData) {
// Extract validation information from relevantData and update the validationStatus state
setValidationStatus(relevantData);
} else {
setValidationStatus(null);
}
}, [flowPool[dataId], dataId, setValidationStatus]);
};
export default useUpdateValidationStatus;

View file

@ -0,0 +1,22 @@
import { useEffect } from "react";
const useValidationStatusString = (validationStatus, setValidationString) => {
useEffect(() => {
if (validationStatus?.data.logs) {
// if it is not a string turn it into a string
let newValidationString = "";
if (Array.isArray(validationStatus.data.logs)) {
newValidationString = validationStatus.data.logs
.map((log) => (log?.message ? log.message : JSON.stringify(log)))
.join("\n");
}
if (typeof newValidationString !== "string") {
newValidationString = JSON.stringify(validationStatus.data.logs);
}
setValidationString(newValidationString);
}
}, [validationStatus, validationStatus?.data.logs, setValidationString]);
};
export default useValidationStatusString;

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,10 +1,12 @@
import TableAutoCellRender from "../tableComponent/components/tableAutoCellRender";
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
//TODO check array type
return (
<div>
<ul>
{array.map((item, index) => (
<li key={index}>{item}</li>
<li key={index}>{<TableAutoCellRender value={item} />}</li>
))}
</ul>
</div>

View file

@ -217,7 +217,7 @@ export default function CollectionCardComponent({
data-testid={`card-${convertTestName(data.name)}`}
//TODO check color schema
className={cn(
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
"group relative flex h-[11rem] flex-col justify-between overflow-hidden hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#5f5f5f0e]",
disabled ? "pointer-events-none opacity-50" : "",
onClick ? "cursor-pointer" : "",
isSelectedCard ? "border border-selected" : ""

View file

@ -1,40 +1,53 @@
import { Transition } from "@headlessui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import IOModal from "../../modals/IOModal";
import ApiModal from "../../modals/apiModal/views";
import ShareModal from "../../modals/shareModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../stores/shortcuts";
import { useStoreStore } from "../../stores/storeStore";
import { classNames } from "../../utils/utils";
import { classNames, isThereModal } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Separator } from "../ui/separator";
export default function FlowToolbar(): JSX.Element {
const [open, setOpen] = useState(false);
const preventDefault = true;
const [open, setOpen] = useState<boolean>(false);
const [openCodeModal, setOpenCodeModal] = useState<boolean>(false);
const [openShareModal, setOpenShareModal] = useState<boolean>(false);
function handleAPIWShortcut(e: KeyboardEvent) {
if (isThereModal() && !openCodeModal) return;
setOpenCodeModal((oldOpen) => !oldOpen);
}
function handleChatWShortcut(e: KeyboardEvent) {
if (isThereModal() && !open) return;
if (useFlowStore.getState().hasIO) {
setOpen((oldState) => !oldState);
}
}
function handleShareWShortcut(e: KeyboardEvent) {
if (isThereModal() && !openShareModal) return;
setOpenShareModal((oldState) => !oldState);
}
const openPlayground = useShortcutsStore((state) => state.open);
const api = useShortcutsStore((state) => state.api);
const flow = useShortcutsStore((state) => state.flow);
useHotkeys(openPlayground, handleChatWShortcut, { preventDefault });
useHotkeys(api, handleAPIWShortcut, { preventDefault });
useHotkeys(flow, handleShareWShortcut, { preventDefault });
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
(event.key === "K" || event.key === "k") &&
(event.metaKey || event.ctrlKey) &&
useFlowStore.getState().hasIO
) {
event.preventDefault();
setOpen((oldState) => !oldState);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const prevNodesRef = useRef<any[] | undefined>();
const ModalMemo = useMemo(
@ -43,6 +56,8 @@ export default function FlowToolbar(): JSX.Element {
is_component={false}
component={currentFlow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
open={openShareModal}
setOpen={setOpenShareModal}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
@ -66,7 +81,14 @@ export default function FlowToolbar(): JSX.Element {
</button>
</ShareModal>
),
[hasApiKey, validApiKey, currentFlow, hasStore]
[
hasApiKey,
validApiKey,
currentFlow,
hasStore,
openShareModal,
setOpenShareModal,
]
);
return (
@ -115,7 +137,11 @@ export default function FlowToolbar(): JSX.Element {
</div>
<div className="flex cursor-pointer items-center gap-2">
{currentFlow && currentFlow.data && (
<ApiModal flow={currentFlow}>
<ApiModal
flow={currentFlow}
open={openCodeModal}
setOpen={setOpenCodeModal}
>
<div
className={classNames(
"relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-hover"

View file

@ -26,7 +26,6 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import ExportModal from "../../modals/exportModal";
import { Case } from "../../shared/components/caseComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
@ -36,13 +35,14 @@ import {
convertValuesToNumbers,
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { classNames, cn } from "../../utils/utils";
import AccordionComponent from "../accordionComponent";
import DictComponent from "../dictComponent";
import IconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
@ -93,28 +93,6 @@ export default function CodeTabsComponent({
return node.data.node.template[templateParam].type;
};
const downloadAsFile = () => {
const fileExtension = tabs[activeTab].language || ".txt";
const suggestedFileName = `${"generated-code."}${fileExtension}`;
const fileName = window.prompt("Enter the file name.", suggestedFileName);
if (!fileName) {
// user pressed cancel on prompt
return;
}
const blob = new Blob([tabs[activeTab].code], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = fileName;
link.href = url;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
return (
<Tabs
value={activeTab}
@ -146,15 +124,9 @@ export default function CodeTabsComponent({
<div></div>
)}
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
<div className="float-right mx-2 mb-1 mt-2 flex items-center gap-4">
{tweaks && (
<div
className={
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
>
<div className={Number(activeTab) > 2 ? "hidden" : "flex gap-2"}>
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
@ -164,46 +136,31 @@ export default function CodeTabsComponent({
autoFocus={false}
/>
<Label
className={
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " +
(activeTweaks
? "font-bold text-black dark:text-white"
: "font-medium")
}
className={cn(
"relative right-1 top-[4px] text-xs font-medium text-muted-foreground",
activeTweaks ? "text-primary" : ""
)}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div>
)}
{allowExport && (
<ExportModal>
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300">
<IconComponent name="FileDown" className="h-4 w-4" />
Export Flow
</div>
</ExportModal>
)}
{Number(activeTab) < 4 && (
<>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={copyToClipboard}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
<IconComponent name="Copy" className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={downloadAsFile}
>
<IconComponent name="Download" className="h-5 w-5" />
</button>
</Button>
</>
)}
</div>

View file

@ -115,6 +115,7 @@ function CsvOutputComponent({
style={{ height: "100%", width: "100%" }}
>
<TableComponent
key={"csv-output"}
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}

View file

@ -1,21 +0,0 @@
import { gradients } from "../../utils/styleUtils";
export default function GradientChooserComponent({ value, onChange }) {
return (
<div className="flex flex-wrap items-center justify-start gap-2">
{gradients.map((gradient, idx) => (
<div
onClick={() => {
onChange(gradient);
}}
className={
"duration-400 h-12 w-12 cursor-pointer rounded-full transition-all " +
gradient +
(value === gradient ? " shadow-lg ring-2 ring-primary" : "")
}
key={idx}
></div>
))}
</div>
);
}

View file

@ -9,13 +9,14 @@ import {
import { useNavigate } from "react-router-dom";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import { IS_MAC, SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import FlowLogsModal from "../../../../modals/flowLogsModal";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../../../stores/shortcuts";
import { useTypesStore } from "../../../../stores/typesStore";
import { cn } from "../../../../utils/utils";
import IconComponent from "../../../genericIconComponent";
@ -23,6 +24,7 @@ import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({}: {}): JSX.Element => {
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -145,17 +147,19 @@ export const MenuBar = ({}: {}): JSX.Element => {
>
<IconComponent name="Undo" className="header-menu-options " />
Undo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
{IS_MAC ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
{
shortcuts.find((s) => s.name.toLowerCase() === "undo")
?.shortcut
}
</span>
)}
<span className="absolute right-2 top-[0.4em]">Z</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@ -165,17 +169,19 @@ export const MenuBar = ({}: {}): JSX.Element => {
>
<IconComponent name="Redo" className="header-menu-options " />
Redo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
{IS_MAC ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
{
shortcuts.find((s) => s.name.toLowerCase() === "redo")
?.shortcut
}
</span>
)}
<span className="absolute right-2 top-[0.4em]">Y</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {

View file

@ -3,7 +3,10 @@ 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 profileCircle from "../../assets/profile-circle.png";
import {
BACKEND_URL,
BASE_URL_API,
LOCATIONS_TO_RETURN,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
@ -15,7 +18,6 @@ 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";
import { Button } from "../ui/button";
import {
@ -46,6 +48,12 @@ export default function Header(): JSX.Element {
const routeHistory = useLocationStore((state) => state.routeHistory);
const profileImageUrl =
`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1
)}${BASE_URL_API}files/profile_pictures/${userData?.profile_image}` ??
profileCircle;
async function checkForChanges(): Promise<void> {
if (nodes.length === 0) {
await removeFlow(id!);
@ -56,7 +64,7 @@ export default function Header(): JSX.Element {
const lastFlowVisitedIndex = routeHistory
.reverse()
.findIndex(
(path) => path.includes("/flow/") && path !== location.pathname,
(path) => path.includes("/flow/") && path !== location.pathname
);
const lastFlowVisited = routeHistory[lastFlowVisitedIndex];
@ -192,34 +200,30 @@ export default function Header(): JSX.Element {
variant="none"
size="none"
data-testid="user-profile-settings"
className={
"h-7 w-7 rounded-full focus-visible:outline-0 " +
(userData?.profile_image ??
(userData?.id
? gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: "bg-gray-500"))
}
/>
>
<img
src={
`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1
)}${BASE_URL_API}files/profile_pictures/${
userData?.profile_image ?? "Space/046-rocket.png"
}` ?? profileCircle
}
className="h-7 w-7 focus-visible:outline-0 "
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!autoLogin && (
<>
<DropdownMenuLabel>
<div className="flex items-center gap-3">
<div
className={
"h-5 w-5 rounded-full focus-visible:outline-0 " +
(userData?.profile_image ??
(userData?.id
? gradients[
parseInt(userData?.id ?? "", 30) %
gradients.length
]
: "bg-gray-500"))
}
<img
src={profileImageUrl}
className="h-5 w-5 focus-visible:outline-0 "
/>
{userData?.username ?? "User"}
</div>
</DropdownMenuLabel>

View file

@ -1,19 +1,22 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { FlowPoolObjectType } from "../../types/chat";
import { extractColumnsFromRows } from "../../utils/utils";
import TableComponent from "../tableComponent";
function RecordsOutputComponent({
flowPool,
pagination,
rows,
columnMode = "union",
}: {
flowPool: FlowPoolObjectType;
pagination: boolean;
rows: any;
columnMode?: "intersection" | "union";
}) {
const rows = flowPool?.data?.artifacts?.records ?? [];
const columns = extractColumnsFromRows(rows, "union");
console.log("rows", rows);
const columns = extractColumnsFromRows(rows, columnMode);
console.log("columns", columns);
const columnDefs = columns.map((col, idx) => ({
...col,
resizable: idx !== columns.length - 1,
@ -22,6 +25,7 @@ function RecordsOutputComponent({
return (
<TableComponent
key={"recordsOutputComponent"}
overlayNoRowsTemplate="No data available"
suppressRowClickSelection={true}
pagination={pagination}

View file

@ -0,0 +1,46 @@
import ForwardedIconComponent from "../genericIconComponent";
export default function RenderIcons({
isMac,
hasShift,
filteredShortcut,
shortcutWPlus,
}: {
isMac: boolean;
hasShift: boolean;
filteredShortcut: string[];
shortcutWPlus: string[];
}): JSX.Element {
return hasShift ? (
<>
{isMac ? (
<ForwardedIconComponent name="Command" className="h-4 w-4" />
) : (
filteredShortcut[0]
)}
<ForwardedIconComponent name="ArrowBigUp" className="ml-1 h-5 w-5" />
{filteredShortcut.map((key, idx) => {
if (idx > 0) {
return <span className="ml-1"> {key.toUpperCase()} </span>;
}
})}
</>
) : (
<>
{shortcutWPlus[0].toLowerCase() === "space" ? (
"Space"
) : shortcutWPlus[0].length <= 1 ? (
shortcutWPlus[0]
) : isMac ? (
<ForwardedIconComponent name="Command" className="h-4 w-4" />
) : (
shortcutWPlus[0]
)}
{shortcutWPlus.map((key, idx) => {
if (idx > 0) {
return <span className="ml-0.5"> {key.toUpperCase()} </span>;
}
})}
</>
);
}

View file

@ -3,6 +3,7 @@ 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 useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { handleKeyDown } from "../../../../utils/reactflowUtils";
@ -13,19 +14,15 @@ import IconComponent, {
import { Button, buttonVariants } from "../../../ui/button";
import { Input } from "../../../ui/input";
import useFileDrop from "../../hooks/use-on-file-drop";
import useAlertStore from "../../../../stores/alertStore";
type SideBarFoldersButtonsComponentProps = {
folders: FolderType[];
pathname: string;
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
};
const SideBarFoldersButtonsComponent = ({
pathname,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
}: SideBarFoldersButtonsComponentProps) => {
const refInput = useRef<HTMLInputElement>(null);
@ -34,7 +31,7 @@ const SideBarFoldersButtonsComponent = ({
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })),
folders.map((obj) => ({ name: obj.name, edit: false }))
);
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
@ -53,6 +50,7 @@ const SideBarFoldersButtonsComponent = ({
const folderId = location?.state?.folderId ?? myCollectionId;
const getFolderById = useFolderStore((state) => state.getFolderById);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const handleFolderChange = (folderId: string) => {
getFolderById(folderId);
@ -60,13 +58,16 @@ const SideBarFoldersButtonsComponent = ({
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId,
handleFolderChange,
handleFolderChange
);
const handleUploadFlowsToFolder = () => {
uploadFolder(folderId)
.then(() => {
getFolderById(folderId);
setSuccessData({
title: "Uploaded successfully",
});
})
.catch((err) => {
console.log(err);
@ -85,7 +86,7 @@ const SideBarFoldersButtonsComponent = ({
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
},
}
);
}
@ -131,7 +132,7 @@ const SideBarFoldersButtonsComponent = ({
<>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
(folder) => folder.name === item.name
)[0];
return (
<div
@ -147,7 +148,7 @@ const SideBarFoldersButtonsComponent = ({
? "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" : "",
folderIdDragging === item.id! ? "bg-border" : ""
)}
onClick={() => handleChangeFolder!(item.id!)}
>
@ -217,7 +218,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
}))
);
}
if (e.key === "Enter") {
@ -250,10 +251,10 @@ const SideBarFoldersButtonsComponent = ({
};
const updatedFolder = await updateFolder(
body,
item.id!,
item.id!
);
const updateFolders = folders.filter(
(f) => f.name !== item.name,
(f) => f.name !== item.name
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
@ -261,7 +262,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
}))
);
} else {
setFoldersNames((old) => ({

View file

@ -44,10 +44,8 @@ export default function SidebarNav({
folders?.length > 0 &&
isFolderPath && (
<SideBarFoldersButtonsComponent
folders={folders}
pathname={pathname}
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
/>
)

View file

@ -6,15 +6,6 @@ export default function ResetColumns({
resetGrid: () => void;
}): JSX.Element {
return (
/*<div className="absolute left-2 bottom-1 cursor-pointer">
<div
className="flex h-10 items-center justify-center px-2 pl-3 rounded-md border border-ring/60 text-sm text-[#bccadc] ring-offset-background placeholder:text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => setShow(!show)}
>
<ForwardedIconComponent name="Settings"></ForwardedIconComponent>
<ForwardedIconComponent name={show ? "ChevronLeft" : "ChevronRight"} className="transition-all"></ForwardedIconComponent>
</div>
</div>*/
<div className={cn("absolute bottom-4 left-6")}>
<span
className="cursor-pointer underline"

View file

@ -0,0 +1,95 @@
import { cn } from "../../../../utils/utils";
import IconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
export default function TableOptions({
resetGrid,
duplicateRow,
deleteRow,
hasSelection,
stateChange,
}: {
resetGrid: () => void;
duplicateRow?: () => void;
deleteRow?: () => void;
hasSelection: boolean;
stateChange: boolean;
}): JSX.Element {
return (
<div className={cn("absolute bottom-3 left-6")}>
<div className="flex items-center gap-2">
<div>
<ShadTooltip content="Reset Columns">
<Button
variant="none"
size="none"
onClick={() => {
resetGrid();
}}
disabled={!stateChange}
>
<IconComponent
name="RotateCcw"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</Button>
</ShadTooltip>
</div>
{duplicateRow && (
<div>
<ShadTooltip
content={
!hasSelection ? (
<span>Select items to duplicate</span>
) : (
<span>Duplicate selected items</span>
)
}
>
<Button
variant="none"
size="none"
onClick={duplicateRow}
disabled={!hasSelection}
>
<IconComponent
name="Copy"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</Button>
</ShadTooltip>
</div>
)}
{deleteRow && (
<div>
<ShadTooltip
content={
!hasSelection ? (
<span>Select items to delete</span>
) : (
<span>Delete selected items</span>
)
}
>
<Button
variant="none"
size="none"
onClick={deleteRow}
disabled={!hasSelection}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
!hasSelection ? "" : "hover:text-destructive"
)}
/>
</Button>
</ShadTooltip>
</div>
)}{" "}
</div>
</div>
);
}

View file

@ -1,6 +1,5 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cn, isTimeStampString } from "../../../../utils/utils";
import ArrayReader from "../../../arrayReaderComponent";
import DateReader from "../../../dateReaderComponent";
import NumberReader from "../../../numberReader";
import ObjectRender from "../../../objectRender";
@ -9,20 +8,17 @@ import { Badge } from "../../../ui/badge";
export default function TableAutoCellRender({
value,
}: CustomCellRendererProps) {
}: CustomCellRendererProps | { value: any }) {
function getCellType() {
switch (typeof value) {
case "object":
if (value === null) {
return String(value);
} else if (Array.isArray(value)) {
return <ArrayReader array={value} />;
} else if (value.definitions) {
// use a custom render defined by the sender
return <ObjectRender object={value} />;
} else {
return <ObjectRender object={value} />;
}
break;
case "string":
if (isTimeStampString(value)) {
return <DateReader date={value} />;
@ -51,7 +47,7 @@ export default function TableAutoCellRender({
}
return (
<div className="group flex h-full w-full items-center align-middle">
<div className="group flex h-full w-full items-center truncate align-middle">
{getCellType()}
</div>
);

View file

@ -1,6 +1,14 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cloneDeep } from "lodash";
import { useState } from "react";
import useFlowStore from "../../../../stores/flowStore";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
import CodeAreaComponent from "../../../codeAreaComponent";
import DictComponent from "../../../dictComponent";
import Dropdown from "../../../dropdownComponent";
@ -13,14 +21,6 @@ import KeypairListComponent from "../../../keypairListComponent";
import PromptAreaComponent from "../../../promptComponent";
import TextAreaComponent from "../../../textAreaComponent";
import ToggleShadComponent from "../../../toggleShadComponent";
import useFlowStore from "../../../../stores/flowStore";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
export default function TableNodeCellRender({
node: { data },
@ -63,8 +63,8 @@ export default function TableNodeCellRender({
...id,
proxy: templateData.proxy,
}
: id,
),
: id
)
) ?? false;
function getCellType() {
switch (templateData.type) {
@ -135,7 +135,7 @@ export default function TableNodeCellRender({
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
templateValue?.length > 1 ? "my-3" : "",
templateValue?.length > 1 ? "my-3" : ""
)}
>
<KeypairListComponent

View file

@ -1,7 +1,7 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
import { ElementRef, forwardRef, useRef } from "react";
import { ElementRef, forwardRef, useRef, useState } from "react";
import {
DEFAULT_TABLE_ALERT_MSG,
DEFAULT_TABLE_ALERT_TITLE,
@ -12,8 +12,8 @@ import { cn, toTitleCase } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import ResetColumns from "./components/ResetColumns";
import TableOptions from "./components/TableOptions";
import resetGrid from "./utils/reset-grid-columns";
import { useParams } from "react-router-dom";
interface TableComponentProps extends AgGridReactProps {
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
@ -21,6 +21,8 @@ interface TableComponentProps extends AgGridReactProps {
alertTitle?: string;
alertDescription?: string;
editable?: boolean | string[];
onDelete?: () => void;
onDuplicate?: () => void;
}
const TableComponent = forwardRef<
@ -33,7 +35,7 @@ const TableComponent = forwardRef<
alertDescription = DEFAULT_TABLE_ALERT_MSG,
...props
},
ref,
ref
) => {
let colDef = props.columnDefs.map((col, index) => {
let newCol = {
@ -68,9 +70,12 @@ const TableComponent = forwardRef<
});
const gridRef = useRef(null);
// @ts-ignore
const realRef = ref?.current ? ref : gridRef;
const realRef: React.MutableRefObject<AgGridReact> = ref?.current
? ref
: gridRef;
const dark = useDarkStore((state) => state.dark);
const initialColumnDefs = useRef(colDef);
const [columnStateChange, setColumnStateChange] = useState(false);
const makeLastColumnNonResizable = (columnDefs) => {
columnDefs.forEach((colDef, index) => {
@ -86,11 +91,14 @@ const TableComponent = forwardRef<
params.api.setGridOption("columnDefs", updatedColumnDefs);
initialColumnDefs.current = params.api.getColumnDefs();
if (props.onGridReady) props.onGridReady(params);
setTimeout(() => {
setColumnStateChange(false);
}, 50);
};
const onColumnMoved = (params) => {
const updatedColumnDefs = makeLastColumnNonResizable(
params.columnApi.getAllGridColumns().map((col) => col.getColDef()),
params.columnApi.getAllGridColumns().map((col) => col.getColDef())
);
params.api.setGridOption("columnDefs", updatedColumnDefs);
if (props.onColumnMoved) props.onColumnMoved(params);
@ -115,25 +123,46 @@ const TableComponent = forwardRef<
className={cn(
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
"ag-theme-shadcn flex h-full flex-col",
"relative",
"relative"
)} // applying the grid theme
>
<AgGridReact
{...props}
className={cn(props.className, "custom-scroll")}
animateRows={false}
className={cn(props.className, "cusm-scroll")}
defaultColDef={{
minWidth: 100,
autoHeight: true,
}}
columnDefs={colDef}
ref={realRef}
pagination={true}
onGridReady={onGridReady}
onColumnMoved={onColumnMoved}
onStateUpdated={(e) => {
console.log(e);
if (e.sources.some((source) => source.includes("column"))) {
setColumnStateChange(true);
}
}}
/>
<TableOptions
stateChange={columnStateChange}
hasSelection={realRef.current?.api?.getSelectedRows().length > 0}
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}
deleteRow={props.onDelete ? props.onDelete : undefined}
resetGrid={() => {
console.log("teste");
resetGrid(realRef, initialColumnDefs);
setTimeout(() => {
setColumnStateChange(false);
}, 100);
}}
/>
<ResetColumns resetGrid={() => resetGrid(realRef, initialColumnDefs)} />
</div>
);
},
}
);
export default TableComponent;

View file

@ -33,7 +33,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"relative z-50 min-w-[14rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View file

@ -31,6 +31,8 @@ export const INVALID_SELECTION_ERROR_ALERT = "Invalid selection";
export const EDIT_PASSWORD_ERROR_ALERT = "Error changing password";
export const EDIT_PASSWORD_ALERT_LIST = "Passwords do not match";
export const SAVE_ERROR_ALERT = "Error saving changes";
export const PROFILE_PICTURES_GET_ERROR_ALERT =
"Error retrieving profile pictures";
export const SIGNUP_ERROR_ALERT = "Error signing up";
export const APIKEY_ERROR_ALERT = "API Key Error";
export const NOAPI_ERROR_ALERT =

View file

@ -589,7 +589,7 @@ export const CONTROL_INPUT_STATE = {
export const CONTROL_PATCH_USER_STATE = {
password: "",
cnfPassword: "",
gradient: "",
profilePicture: "",
apikey: "",
};
@ -661,6 +661,9 @@ export const OUTPUT_TYPES = new Set([
export const CHAT_FIRST_INITIAL_TEXT =
"Start a conversation and click the agent's thoughts";
export const TOOLTIP_OUTDATED_NODE =
"Your component is outdated. Click to update (data may be lost)";
export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
export const ZERO_NOTIFICATIONS = "No new notifications";
@ -731,6 +734,91 @@ export const AUTHORIZED_DUPLICATE_REQUESTS = [
export const SAVE_DEBOUNCE_TIME = 300;
export const IS_MAC = navigator.userAgent.toUpperCase().includes("MAC");
export const defaultShortcuts = [
{
name: "Advanced Settings",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + A`,
},
{
name: "Minimize",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Q`,
},
{
name: "Code",
shortcut: `Space`,
},
{
name: "Copy",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + C`,
},
{
name: "Duplicate",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + D`,
},
{
name: "Share",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + S`,
},
{
name: "Docs",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + D`,
},
{
name: "Save",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + S`,
},
{
name: "Delete",
shortcut: "Backspace",
},
{
name: "Open playground",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + K`,
},
{
name: "Undo",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Z`,
},
{
name: "Redo",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Y`,
},
{
name: "Group",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + G`,
},
{
name: "Cut",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + X`,
},
{
name: "Paste",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + V`,
},
{
name: "API",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + R`,
},
{
name: "Download",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + J`,
},
{
name: "Update",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + U`,
},
{
name: "Freeze",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + F`,
},
{
name: "Flow Share",
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + B`,
},
];
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";
@ -738,3 +826,6 @@ export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];
export const MAX_BATCH_SIZE = 50;
export const MODAL_CLASSES =
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0";

View file

@ -83,8 +83,10 @@ function ApiInterceptor() {
(config) => {
const checkRequest = checkDuplicateRequestAndStoreRequest(config);
const controller = new AbortController();
if (!checkRequest) {
return Promise.reject("Duplicate request.");
controller.abort("Duplicate Request");
}
const accessToken = cookies.get("access_token_lf");
@ -92,7 +94,10 @@ function ApiInterceptor() {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
return {
...config,
signal: controller.signal,
};
},
(error) => {
return Promise.reject(error);

View file

@ -8,6 +8,7 @@ import {
APITemplateType,
Component,
LoginType,
ProfilePicturesTypeAPI,
Users,
VertexBuildTypeAPI,
VerticesOrderTypeAPI,
@ -367,6 +368,19 @@ export async function uploadFile(
return await api.post(`${BASE_URL_API}files/upload/${id}`, formData);
}
export async function getProfilePictures(): Promise<ProfilePicturesTypeAPI | null> {
try {
const res = await api.get(`${BASE_URL_API}files/profile_pictures/list`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
throw error;
}
return null;
}
export async function postCustomComponent(
code: string,
apiClass: APIClassType
@ -967,12 +981,20 @@ export async function getVerticesOrder(
export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string
input_value: string,
files?: string[]
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
let data = {};
if (typeof input_value !== "undefined") {
data["inputs"] = { input_value: input_value };
}
if (data && files) {
data["files"] = files;
}
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined
data
);
}

View file

@ -8,17 +8,14 @@ import "./style/index.css";
// @ts-ignore
import "./style/applies.css";
// @ts-ignore
import { StrictMode } from "react";
import "./style/classes.css";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<StrictMode>
<ContextWrapper>
<App />
</ContextWrapper>
</StrictMode>
<ContextWrapper>
<App />
</ContextWrapper>
);
reportWebVitals();

View file

@ -0,0 +1,40 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../../../components/ui/select";
export default function CsvSelect({ node, handleChangeSelect }): JSX.Element {
return (
<>
<div className="flex justify-between">
Expand the ouptut to see the CSV
</div>
<div className="flex items-center justify-between pt-5">
<span>CSV separator </span>
<Select
value={node.data.node.template.separator.value}
onValueChange={(e) => handleChangeSelect(e)}
>
<SelectTrigger className="w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{node?.data?.node?.template?.separator?.options.map(
(separator) => (
<SelectItem key={separator} value={separator}>
{separator}
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>
</div>
</>
);
}

View file

@ -5,17 +5,10 @@ import CsvOutputComponent from "../../../../components/csvOutputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import PdfViewer from "../../../../components/pdfViewer";
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../components/ui/select";
import { Textarea } from "../../../../components/ui/textarea";
import { PDFViewConstant } from "../../../../constants/constants";
import { InputOutput } from "../../../../constants/enums";
import TextOutputView from "../../../../shared/components/textOutputView";
import useFlowStore from "../../../../stores/flowStore";
import { IOFieldViewProps } from "../../../../types/components";
import {
@ -24,6 +17,7 @@ import {
} from "../../../../utils/reactflowUtils";
import IOFileInput from "./components/FileInput";
import IoJsonInput from "./components/JSONInput";
import CsvSelect from "./components/csvSelect";
import IOKeyPairInput from "./components/keyPairInput";
export default function IOFieldView({
@ -51,6 +45,12 @@ export default function IOFieldView({
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const textOutputValue =
(flowPool[node!.id] ?? [])[(flowPool[node!.id]?.length ?? 1) - 1]?.data
.results.result ?? "";
console.log(flowPoolNode?.data?.artifacts?.records);
function handleOutputType() {
if (!node) return <>"No node found!"</>;
switch (type) {
@ -163,21 +163,7 @@ export default function IOFieldView({
case InputOutput.OUTPUT:
switch (fieldType) {
case "TextOutput":
return (
<Textarea
className={`w-full custom-scroll ${
left ? " min-h-32" : " h-full"
}`}
placeholder={"Empty"}
// update to real value on flowPool
value={
(flowPool[node.id] ?? [])[
(flowPool[node.id]?.length ?? 1) - 1
]?.params ?? ""
}
readOnly
/>
);
return <TextOutputView left={left} value={textOutputValue} />;
case "PDFOutput":
return left ? (
<div>{PDFViewConstant}</div>
@ -187,31 +173,10 @@ export default function IOFieldView({
case "CSVOutput":
return left ? (
<>
<div className="flex justify-between">
Expand the ouptut to see the CSV
</div>
<div className="flex items-center justify-between pt-5">
<span>CSV separator </span>
<Select
value={node.data.node.template.separator.value}
onValueChange={(e) => handleChangeSelect(e)}
>
<SelectTrigger className="w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{node?.data?.node?.template?.separator?.options.map(
(separator) => (
<SelectItem key={separator} value={separator}>
{separator}
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>
</div>
<CsvSelect
node={node}
handleChangeSelect={handleChangeSelect}
/>
</>
) : (
<>
@ -286,8 +251,9 @@ export default function IOFieldView({
return (
<div className={left ? "h-56" : "h-full"}>
<RecordsOutputComponent
flowPool={flowPoolNode}
pagination={!left}
rows={flowPoolNode?.data?.artifacts?.records ?? []}
columnMode="union"
/>
</div>
);
@ -303,7 +269,7 @@ export default function IOFieldView({
value={
(flowPool[node.id] ?? [])[
(flowPool[node.id]?.length ?? 1) - 1
]?.params ?? ""
]?.data.results.result ?? ""
}
readOnly
/>

View file

@ -0,0 +1,29 @@
import { deleteMessagesFn } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore";
const useRemoveSession = (setSuccessData, setErrorData) => {
const deleteSession = useMessagesStore((state) => state.deleteSession);
const messages = useMessagesStore((state) => state.messages);
const handleRemoveSession = async (session_id: string) => {
try {
await deleteMessagesFn(
messages
.filter((msg) => msg.session_id === session_id)
.map((msg) => msg.index)
);
deleteSession(session_id);
setSuccessData({
title: "Session deleted successfully.",
});
} catch (error) {
setErrorData({
title: "Error deleting Session.",
});
}
};
return { handleRemoveSession };
};
export default useRemoveSession;

View file

@ -0,0 +1,56 @@
import { CellEditRequestEvent, SelectionChangedEvent } from "ag-grid-community";
import { useState } from "react";
import TableComponent from "../../../../components/tableComponent";
import useRemoveMessages from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages";
import useUpdateMessage from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage";
import useAlertStore from "../../../../stores/alertStore";
import { useMessagesStore } from "../../../../stores/messagesStore";
export default function SessionView({ rows }: { rows: Array<any> }) {
const columns = useMessagesStore((state) => state.columns);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const { handleRemoveMessages } = useRemoveMessages(
setSelectedRows,
setSuccessData,
setErrorData,
selectedRows
);
const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
function handleUpdateMessage(event: CellEditRequestEvent<any, string>) {
const newValue = event.newValue;
const field = event.column.getColId();
const row = event.data;
const data = {
...row,
[field]: newValue,
};
handleUpdate(data);
}
return (
<TableComponent
key={"sessionView"}
onDelete={handleRemoveMessages}
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);
}}
editable={["Sender Name", "Message"]}
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(event.api.getSelectedRows().map((row) => row.index));
}}
rowSelection="multiple"
suppressRowClickSelection={true}
pagination={true}
columnDefs={columns}
rowData={rows}
/>
);
}

View file

@ -0,0 +1,75 @@
import IconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
import { Case } from "../../../../../../../shared/components/caseComponent";
import { FilePreviewType } from "../../../../../../../types/components";
import { classNames } from "../../../../../../../utils/utils";
type ButtonSendWrapperProps = {
send: () => void;
lockChat: boolean;
noInput: boolean;
saveLoading: boolean;
chatValue: string;
files: FilePreviewType[];
};
const ButtonSendWrapper = ({
send,
lockChat,
noInput,
saveLoading,
chatValue,
files,
}: ButtonSendWrapperProps) => {
return (
<Button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background"
)}
disabled={lockChat || saveLoading}
onClick={(): void => send()}
variant="none"
size="none"
>
<Case
condition={
lockChat || saveLoading || files.some((file) => file.loading)
}
>
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
</Case>
<Case condition={noInput && !lockChat}>
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
</Case>
<Case
condition={
!(lockChat || saveLoading || files.some((file) => file.loading)) &&
!noInput
}
>
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
</Case>
</Button>
);
};
export default ButtonSendWrapper;

View file

@ -0,0 +1,81 @@
import { Textarea } from "../../../../../../../components/ui/textarea";
import { classNames } from "../../../../../../../utils/utils";
const TextAreaWrapper = ({
checkSendingOk,
send,
lockChat,
noInput,
saveLoading,
chatValue,
setChatValue,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
inputRef,
setInputFocus,
files,
isDragging,
}) => {
const getPlaceholderText = (
isDragging: boolean,
noInput: boolean
): string => {
if (isDragging) {
return "Drop here";
} else if (noInput) {
return CHAT_INPUT_PLACEHOLDER;
} else {
return CHAT_INPUT_PLACEHOLDER_SEND;
}
};
const lockClass =
lockChat || saveLoading
? "form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: "form-modal-lock-false bg-background";
const fileClass =
files.length > 0
? "rounded-b-lg ring-0 focus:ring-0 focus:border-2 rounded-t-none border-t-0 border-border focus:border-t-0 focus:border-ring"
: "rounded-md border-t border-border focus:ring-0 focus:border-2 focus:border-ring";
const additionalClassNames = "form-modal-lockchat pl-14";
return (
<Textarea
onFocus={(e) => {
setInputFocus(true);
e.target.style.borderTopWidth = "0";
}}
onBlur={() => setInputFocus(false)}
onKeyDown={(event) => {
if (checkSendingOk(event)) {
send();
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(lockClass, fileClass, additionalClassNames)}
placeholder={getPlaceholderText(isDragging, noInput)}
/>
);
};
export default TextAreaWrapper;

View file

@ -0,0 +1,34 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
const UploadFileButton = ({
fileInputRef,
handleFileChange,
handleButtonClick,
lockChat,
}) => {
return (
<div>
<input
disabled={lockChat}
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Button
disabled={lockChat}
className={`font-bold text-white transition-all ${
lockChat ? "cursor-not-allowed" : "hover:text-muted-foreground"
}`}
onClick={handleButtonClick}
variant="none"
size="none"
>
<ForwardedIconComponent name="PaperclipIcon" />
</Button>
</div>
);
};
export default UploadFileButton;

View file

@ -0,0 +1,7 @@
export const getClassNamesFilePreview = (inputFocus) => {
return `flex w-full items-center gap-4 rounded-t-lg bg-background px-14 py-5 overflow-auto custom-scroll ${
inputFocus
? "border border-b-0 border-ring border-2"
: "border border-b-0 border-border"
}`;
};

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
const useAutoResizeTextArea = (value, inputRef) => {
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
inputRef.current.style!.height = "inherit"; // Reset the height
inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight
}
}, [value]);
return inputRef;
};
export default useAutoResizeTextArea;

View file

@ -0,0 +1,75 @@
import ShortUniqueId from "short-unique-id";
import useFileUpload from "./use-file-upload";
const fsErrorText =
"Please ensure your file has one of the following extensions:";
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
const useDragAndDrop = (
setIsDragging,
setFiles,
currentFlowId,
setErrorData
) => {
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files, setFiles, currentFlowId, setErrorData);
e.dataTransfer.clearData();
}
setIsDragging(false);
};
return {
dragOver,
dragEnter,
dragLeave,
onDrop,
};
};
const handleFiles = (files, setFiles, currentFlowId, setErrorData) => {
if (files) {
const allowedExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
const file = files?.[0];
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
console.log("Error uploading file");
setErrorData({
title: "Error uploading file",
list: [fsErrorText, snErrorTxt],
});
return;
}
const uid = new ShortUniqueId({ length: 3 });
const id = uid();
const type = files[0].type.split("/")[0];
const blob = files[0];
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
};
export default useDragAndDrop;

View file

@ -0,0 +1,27 @@
import { uploadFile } from "../../../../../../controllers/API";
const useFileUpload = (blob, currentFlowId, setFiles, id) => {
uploadFile(blob, currentFlowId)
.then((res) => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].path = res.data.file_path;
return newFiles;
});
})
.catch(() => {
setFiles((prev) => {
const newFiles = [...prev];
const updatedIndex = newFiles.findIndex((file) => file.id === id);
newFiles[updatedIndex].loading = false;
newFiles[updatedIndex].error = true;
return newFiles;
});
});
return null;
};
export default useFileUpload;

View file

@ -0,0 +1,13 @@
import { useEffect } from "react";
const useFocusOnUnlock = (lockChat, inputRef) => {
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
return inputRef;
};
export default useFocusOnUnlock;

View file

@ -0,0 +1,50 @@
import ShortUniqueId from "short-unique-id";
import useAlertStore from "../../../../../../stores/alertStore";
import useFileUpload from "./use-file-upload";
const fsErrorText =
"Please ensure your file has one of the following extensions:";
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
export const useHandleFileChange = (setFiles, currentFlowId) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const fileInput = event.target;
const file = fileInput.files?.[0];
if (file) {
const allowedExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
setErrorData({
title: "Error uploading file",
list: [fsErrorText, snErrorTxt],
});
return;
}
const uid = new ShortUniqueId({ length: 10 }); // Increase the length to ensure uniqueness
const id = uid();
const type = file.type.split("/")[0];
const blob = file;
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
// Clear the file input value to ensure the change event is triggered even for the same file
fileInput.value = "";
};
return {
handleFileChange,
};
};
export default useHandleFileChange;

View file

@ -0,0 +1,61 @@
import { useEffect } from "react";
import ShortUniqueId from "short-unique-id";
import useAlertStore from "../../../../../../stores/alertStore";
import useFileUpload from "./use-file-upload";
const fsErrorText =
"Please ensure your file has one of the following extensions:";
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
const useUpload = (uploadFile, currentFlowId, setFiles, lockChat) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {
if (lockChat) {
return;
}
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const type = items[0].type.split("/")[0];
const uid = new ShortUniqueId({ length: 3 });
const blob = items[i].getAsFile();
if (blob) {
const allowedExtensions = [
"png",
"jpg",
"jpeg",
"gif",
"bmp",
"webp",
];
const fileExtension = blob.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
setErrorData({
title: "Error uploading file",
list: [fsErrorText, snErrorTxt],
});
return;
}
const id = uid();
setFiles((prevFiles) => [
...prevFiles,
{ file: blob, loading: true, error: false, id, type },
]);
useFileUpload(blob, currentFlowId, setFiles, id);
}
}
}
};
document.addEventListener("paste", handlePaste);
return () => {
document.removeEventListener("paste", handlePaste);
};
}, [uploadFile, currentFlowId, lockChat]);
return null;
};
export default useUpload;

View file

@ -1,14 +1,23 @@
import { useEffect, useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import { Textarea } from "../../../../../components/ui/textarea";
import { useRef, useState } from "react";
import {
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
} from "../../../../../constants/constants";
import { uploadFile } from "../../../../../controllers/API";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { chatInputType } from "../../../../../types/components";
import { classNames } from "../../../../../utils/utils";
import {
ChatInputType,
FilePreviewType,
} from "../../../../../types/components";
import FilePreview from "../filePreviewChat";
import ButtonSendWrapper from "./components/buttonSendWrapper";
import TextAreaWrapper from "./components/textAreaWrapper";
import UploadFileButton from "./components/uploadFileButton";
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
import useFocusOnUnlock from "./hooks/use-focus-unlock";
import useHandleFileChange from "./hooks/use-handle-file-change";
import useUpload from "./hooks/use-upload";
export default function ChatInput({
lockChat,
chatValue,
@ -16,125 +25,105 @@ export default function ChatInput({
setChatValue,
inputRef,
noInput,
}: chatInputType): JSX.Element {
files,
setFiles,
isDragging,
}: ChatInputType): JSX.Element {
const [repeat, setRepeat] = useState(1);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();
}
}, [lockChat, inputRef]);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [inputFocus, setInputFocus] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight !== 0) {
inputRef.current.style.height = "inherit"; // Reset the height
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
}
}, [chatValue]);
useFocusOnUnlock(lockChat, inputRef);
useAutoResizeTextArea(chatValue, inputRef);
useUpload(uploadFile, currentFlowId, setFiles, lockChat || saveLoading);
const { handleFileChange } = useHandleFileChange(setFiles, currentFlowId);
const send = () => {
sendMessage({
repeat,
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
});
setFiles([]);
};
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
return (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
);
};
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
const handleButtonClick = () => {
fileInputRef.current!.click();
};
return (
<div className="flex w-full gap-2">
<div className="flex w-full flex-col-reverse">
<div className="relative w-full">
<Textarea
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!lockChat &&
!saveLoading &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
sendMessage(repeat);
}
}}
rows={1}
ref={inputRef}
disabled={lockChat || noInput || saveLoading}
style={{
resize: "none",
bottom: `${inputRef?.current?.scrollHeight}px`,
maxHeight: "150px",
overflow: `${
inputRef.current && inputRef.current.scrollHeight > 150
? "auto"
: "hidden"
}`,
}}
value={
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
}
onChange={(event): void => {
setChatValue(event.target.value);
}}
className={classNames(
lockChat || saveLoading
? " form-modal-lock-true bg-input"
: noInput
? "form-modal-no-input bg-input"
: " form-modal-lock-false bg-background",
"form-modal-lockchat"
)}
placeholder={
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
<TextAreaWrapper
checkSendingOk={checkSendingOk}
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
setChatValue={setChatValue}
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
inputRef={inputRef}
setInputFocus={setInputFocus}
files={files}
isDragging={isDragging}
/>
<div className="form-modal-send-icon-position">
<button
className={classNames(
"form-modal-send-button",
noInput
? "bg-high-indigo text-background"
: chatValue === ""
? "text-primary"
: "bg-chat-send text-background"
)}
disabled={lockChat || saveLoading}
onClick={(): void => sendMessage(repeat)}
>
{lockChat || saveLoading ? (
<IconComponent
name="Lock"
className="form-modal-lock-icon"
aria-hidden="true"
/>
) : noInput ? (
<IconComponent
name="Zap"
className="form-modal-play-icon"
aria-hidden="true"
/>
) : (
<IconComponent
name="LucideSend"
className="form-modal-send-icon "
aria-hidden="true"
/>
)}
</button>
<ButtonSendWrapper
send={send}
lockChat={lockChat}
noInput={noInput}
saveLoading={saveLoading}
chatValue={chatValue}
files={files}
/>
</div>
<div
className={`absolute bottom-2 left-4 ${
lockChat || saveLoading ? "cursor-not-allowed" : ""
}`}
>
<UploadFileButton
lockChat={lockChat || saveLoading}
fileInputRef={fileInputRef}
handleFileChange={handleFileChange}
handleButtonClick={handleButtonClick}
/>
</div>
</div>
{/*
<Popover>
<PopoverTrigger asChild>
<Button variant="primary" className="h-13 px-4">
<IconComponent name="Repeat" className="" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">
<div className="flex flex-col items-center justify-center gap-2">
<span className="text-sm">Repetitions: </span>
<Input
onChange={(e) => {
handleChange(parseInt(e.target.value));
{files.length > 0 && (
<div className={classNameFilePreview}>
{files.map((file) => (
<FilePreview
error={file.error}
file={file.file}
loading={file.loading}
key={file.id}
onDelete={() => {
setFiles((prev: FilePreviewType[]) =>
prev.filter((f) => f.id !== file.id)
);
// TODO: delete file on backend
}}
className="w-16"
type="number"
min={0}
/>
</div>
</PopoverContent>
</Popover> */}
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,35 @@
import { useState } from "react";
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import FileCard from "../../../fileComponent";
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
export default function FileCardWrapper({
index,
name,
type,
path,
}: {
index: number;
name: string;
type: string;
path: string;
}) {
const [show, setShow] = useState<boolean>(true);
return (
<div key={index} className="flex flex-col gap-2">
<span
onClick={() => setShow(!show)}
className="flex cursor-pointer gap-2 text-sm text-muted-foreground"
>
{formatFileName(name, 50)}
<ForwardedIconComponent name={show ? "ChevronDown" : "ChevronRight"} />
</span>
<FileCard
showFile={show}
fileName={name}
fileType={type}
content={path}
/>
</div>
);
}

View file

@ -13,7 +13,7 @@ import useAlertStore from "../../../../../stores/alertStore";
import useFlowStore from "../../../../../stores/flowStore";
import { chatMessagePropsType } from "../../../../../types/components";
import { classNames, cn } from "../../../../../utils/utils";
import FileCard from "../fileComponent";
import FileCardWrapper from "./components/fileCardWrapper";
export default function ChatMessage({
chat,
@ -22,6 +22,7 @@ export default function ChatMessage({
updateChat,
setLockChat,
}: chatMessagePropsType): JSX.Element {
const [showFile, setShowFile] = useState<boolean>(true);
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
@ -251,21 +252,6 @@ dark:prose-invert"
[chat.message, chatMessage]
)}
</div>
{chat.files && (
<div className="my-2 w-full">
{chat.files.map((file, index) => {
return (
<div key={index} className="my-2 w-full">
<FileCard
fileName={"Generated File"}
fileType={file.data_type}
content={file.data}
/>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
@ -323,14 +309,30 @@ dark:prose-invert"
</span>
</>
) : (
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
<div className="flex flex-col">
<span
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}
>
{chatMessage}
</span>
{chat.files && (
<div className="my-2 flex flex-col gap-5">
{chat.files.map((file, index) => {
return (
<FileCardWrapper
index={index}
name={file.name}
type={file.type}
path={file.path}
/>
);
})}
</div>
)}
</div>
)}
</div>
)}

View file

@ -0,0 +1,30 @@
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../../components/ui/button";
export default function DownloadButton({
isHovered,
handleDownload,
}: {
isHovered: boolean;
handleDownload: () => void;
}): JSX.Element | undefined {
if (isHovered) {
return (
<div
className={`absolute right-1 top-1 rounded-md bg-muted text-sm font-bold text-foreground `}
>
<Button
variant={"none"}
className="bg-transparent px-2 py-1 text-ring"
onClick={handleDownload}
>
<ForwardedIconComponent
name="DownloadCloud"
className="h-5 w-5 bg-transparent text-current hover:scale-110"
/>
</Button>
</div>
);
}
return undefined;
}

View file

@ -1,26 +1,23 @@
import * as base64js from "base64-js";
import { useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import { ForwardedIconComponent } from "../../../../../components/genericIconComponent";
import { BACKEND_URL, BASE_URL_API } from "../../../../../constants/constants";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { fileCardPropsType } from "../../../../../types/components";
import formatFileName from "../filePreviewChat/utils/format-file-name";
import DownloadButton from "./components/downloadButton/downloadButton";
import getClasses from "./utils/get-classes";
import handleDownload from "./utils/handle-download";
const imgTypes = new Set(["png", "jpg", "jpeg", "gif", "webp", "image"]);
export default function FileCard({
fileName,
content,
fileType,
}: fileCardPropsType): JSX.Element {
const handleDownload = (): void => {
const byteArray = new Uint8Array(base64js.toByteArray(content));
const blob = new Blob([byteArray], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName + ".png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
showFile = true,
}: fileCardPropsType): JSX.Element | undefined {
const [isHovered, setIsHovered] = useState(false);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
function handleMouseEnter(): void {
setIsHovered(true);
}
@ -28,58 +25,57 @@ export default function FileCard({
setIsHovered(false);
}
if (fileType === "image") {
const fileWrapperClasses = getClasses(isHovered);
const imgSrc = `${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1
)}${BASE_URL_API}files/images/${content}`;
if (showFile) {
if (imgTypes.has(fileType)) {
return (
<div
className="inline-block w-full rounded-lg transition-all"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ display: "inline-block" }}
>
<div className="relative w-[50%] rounded-lg border border-border">
<img
src={imgSrc}
alt="generated image"
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
/>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
</div>
);
}
return (
<div
className="relative h-1/4 w-1/4"
className={fileWrapperClasses}
onClick={() => handleDownload({ fileName, content })}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<img
src={`data:image/png;base64,${content}`}
alt="generated image"
className="h-full w-full rounded-lg"
/>
{isHovered && (
<div className={`file-card-modal-image-div `}>
<button
className="file-card-modal-image-button "
onClick={handleDownload}
>
<IconComponent
name="DownloadCloud"
className="h-5 w-5 text-current hover:scale-110"
/>
</button>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(fileName, 20)}</span>
<span>File</span>
</div>
)}
</div>
<DownloadButton
isHovered={isHovered}
handleDownload={() => handleDownload({ fileName, content })}
/>
</div>
);
}
return (
<button onClick={handleDownload} className="file-card-modal-button">
<div className="file-card-modal-div">
ooooooooooooooo{" "}
{fileType === "image" ? (
<img
src={`data:image/png;base64,${content}`}
alt=""
className="h-8 w-8"
/>
) : (
<IconComponent name="File" className="h-8 w-8" />
)}
<div className="file-card-modal-footer">
{" "}
<div className="file-card-modal-name">{fileName}</div>
<div className="file-card-modal-type">{fileType}</div>
</div>
<IconComponent
name="DownloadCloud"
className="ml-auto h-6 w-6 text-current"
/>
</div>
</button>
);
return undefined;
}

View file

@ -0,0 +1,7 @@
export default function getClasses(isHovered: boolean): string {
return `relative ${
false ? "h-20 w-20" : "h-20 w-80"
} cursor-pointer rounded-lg border border-ring bg-muted shadow transition duration-300 hover:drop-shadow-lg ${
isHovered ? "shadow-md" : ""
}`;
}

View file

@ -0,0 +1,46 @@
import {
BACKEND_URL,
BASE_URL_API,
} from "../../../../../../constants/constants";
let isDownloading = false;
export default async function handleDownload({
fileName,
content,
}: {
fileName: string;
content: string;
}): Promise<void> {
if (isDownloading) return;
try {
isDownloading = true;
const response = await fetch(
`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1
)}${BASE_URL_API}files/download/${content}`
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName); // Set the filename
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up the URL object
} catch (error) {
console.error("Failed to download file:", error);
} finally {
isDownloading = false;
}
}

View file

@ -0,0 +1,110 @@
import { useState } from "react";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../../components/genericIconComponent";
import { Skeleton } from "../../../../../components/ui/skeleton";
import formatFileName from "./utils/format-file-name";
const supImgFiles = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "image"];
export default function FilePreview({
error,
file,
loading,
onDelete,
}: {
loading: boolean;
file: File;
error: boolean;
onDelete: () => void;
}) {
const fileType = file.type.toLowerCase();
const isImage = supImgFiles.some((type) => fileType.includes(type));
const [isHovered, setIsHovered] = useState(false);
return (
<div className="relative inline-block">
{loading ? (
isImage ? (
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background ">
<svg
aria-hidden="true"
className={`h-10 w-10 animate-spin fill-black text-muted`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
) : (
<div
className={`relative ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
>
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex flex-col gap-1">
<Skeleton className="h-3 w-48" />
<Skeleton className="h-3 w-10" />
</div>
</div>
</div>
)
) : error ? (
<div>Error...</div>
) : (
<div
className={`relative mt-2 ${
isImage ? "h-20 w-20" : "h-20 w-80"
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
isHovered ? "shadow-md" : ""
}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isImage ? (
<img
src={URL.createObjectURL(file)}
alt="file"
className="block h-full w-full rounded-md border border-border"
/>
) : (
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
<ForwardedIconComponent name="File" className="h-8 w-8" />
<div className="flex flex-col">
<span className="font-bold">{formatFileName(file.name)}</span>
<span>File</span>
</div>
</div>
)}
{isHovered && (
<div
className={`absolute ${
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
} flex h-5 w-5 items-center justify-center`}
>
<div
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
onClick={onDelete}
>
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
</div>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,14 @@
export default function formatFileName(
name: string,
numberToTruncate: number = 25
): string {
if (name[numberToTruncate] === undefined) {
return name;
}
const fileExtension = name.split(".").pop(); // Get the file extension
const baseName = name.slice(0, name.lastIndexOf(".")); // Get the base name without the extension
if (baseName.length > 6) {
return `${baseName.slice(0, numberToTruncate)}...${fileExtension}`;
}
return name;
}

View file

@ -9,15 +9,12 @@ import { deleteFlowPool } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { sendAllProps } from "../../../../types/api";
import {
ChatMessageType,
ChatOutputType,
FlowPoolObjectType,
} from "../../../../types/chat";
import { chatViewProps } from "../../../../types/components";
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
import { FilePreviewType, chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput";
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
import ChatMessage from "./chatMessage";
export default function ChatView({
@ -28,7 +25,7 @@ export default function ChatView({
setLockChat,
}: chatViewProps): JSX.Element {
const { flowPool, outputs, inputs, CleanFlowPool } = useFlowStore();
const { setNoticeData } = useAlertStore();
const { setErrorData } = useAlertStore();
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const messagesRef = useRef<HTMLDivElement | null>(null);
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
@ -47,7 +44,7 @@ export default function ChatView({
//build chat history
useEffect(() => {
const chatOutputResponses: FlowPoolObjectType[] = [];
const chatOutputResponses: VertexBuildTypeAPI[] = [];
outputIds.forEach((outputId) => {
if (outputId.includes("ChatOutput")) {
if (flowPool[outputId] && flowPool[outputId].length > 0) {
@ -65,11 +62,11 @@ export default function ChatView({
const chatMessages: ChatMessageType[] = chatOutputResponses
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
//
.filter((output) => output.data.artifacts?.message !== null)
.filter((output) => output.data.message)
.map((output, index) => {
try {
const { sender, message, sender_name, stream_url } = output.data
.artifacts as ChatOutputType;
const { sender, message, sender_name, stream_url, files } = output
.data.message as ChatOutputType;
const is_ai = sender === "Machine" || sender === null;
return {
@ -78,6 +75,7 @@ export default function ChatView({
sender_name,
componentId: output.id,
stream_url: stream_url,
files,
};
} catch (e) {
console.error(e);
@ -133,7 +131,7 @@ export default function ChatView({
function updateChat(
chat: ChatMessageType,
message: string,
stream_url?: string,
stream_url?: string
) {
// if (message === "") return;
chat.message = message;
@ -156,9 +154,24 @@ export default function ChatView({
// return newChatHistory;
// });
}
const [files, setFiles] = useState<FilePreviewType[]>([]);
const [isDragging, setIsDragging] = useState(false);
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
setIsDragging,
setFiles,
currentFlowId,
setErrorData
);
return (
<div className="eraser-column-arrangement">
<div
className="eraser-column-arrangement"
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
>
<div className="eraser-size">
<div className="eraser-position">
<Button
@ -253,11 +266,16 @@ export default function ChatView({
chatValue={chatValue}
noInput={!inputTypes.includes("ChatInput")}
lockChat={lockChat}
sendMessage={(count) => sendMessage(count)}
sendMessage={({ repeat, files }) =>
sendMessage({ repeat, files })
}
setChatValue={(value) => {
setChatValue(value);
}}
inputRef={ref}
files={files}
setFiles={setFiles}
isDragging={isDragging}
/>
</div>
</div>

View file

@ -3,26 +3,28 @@ import AccordionComponent from "../../components/accordionComponent";
import IconComponent from "../../components/genericIconComponent";
import ShadTooltip from "../../components/shadTooltipComponent";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import {
CHAT_FORM_DIALOG_SUBTITLE,
OUTPUTS_MODAL_TITLE,
TEXT_INPUT_MODAL_TITLE,
} from "../../constants/constants";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { InputOutput } from "../../constants/enums";
import { getMessagesTable } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useMessagesStore } from "../../stores/messagesStore";
import { IOModalPropsType } from "../../types/components";
import { NodeType } from "../../types/flow";
import { NodeDataType, NodeType } from "../../types/flow";
import { updateVerticesOrder } from "../../utils/buildUtils";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView";
import SessionView from "./components/SessionView";
import useRemoveSession from "./components/SessionView/hooks";
import ChatView from "./components/chatView";
export default function IOModal({
@ -32,6 +34,7 @@ export default function IOModal({
disable,
}: IOModalPropsType): JSX.Element {
const allNodes = useFlowStore((state) => state.nodes);
const setMessages = useMessagesStore((state) => state.setMessages);
const inputs = useFlowStore((state) => state.inputs).filter(
(input) => input.type !== "ChatInput"
);
@ -53,6 +56,8 @@ export default function IOModal({
const [selectedTab, setSelectedTab] = useState(
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
function startView() {
if (!chatInput && !chatOutput) {
@ -78,56 +83,93 @@ export default function IOModal({
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode);
const [sessions, setSessions] = useState<string[]>([]);
const messages = useMessagesStore((state) => state.messages);
const setColumns = useMessagesStore((state) => state.setColumns);
async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null);
}
async function sendMessage(count = 1): Promise<void> {
async function sendMessage({
repeat = 1,
files,
}: {
repeat: number;
files?: string[];
}): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
setChatValue("");
for (let i = 0; i < count; i++) {
for (let i = 0; i < repeat; i++) {
await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
files: files,
silent: true,
}).catch((err) => {
console.error(err);
setLockChat(false);
});
}
const { rows, columns } = await getMessagesTable("union", currentFlow!.id);
setMessages(rows);
setColumns(columns);
setLockChat(false);
if (chatInput) {
setNode(chatInput.id, (node: NodeType) => {
const newNode = { ...node };
newNode.data.node!.template["input_value"].value = chatValue;
return newNode;
});
}
}
const { handleRemoveSession } = useRemoveSession(
setSuccessData,
setErrorData
);
useEffect(() => {
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [allNodes.length]);
const flow_sessions = allNodes.map((node) => {
if ((node.data as NodeDataType).node?.template["session_id"]) {
return {
id: node.id,
session_id: (node.data as NodeDataType).node?.template["session_id"]
.value,
};
}
});
useEffect(() => {
setSelectedViewField(startView());
// if (haveChat) {
// getSessions().then((sessions) => {
// setSessions(sessions);
// });
// }
if (haveChat) {
getMessagesTable("union", currentFlow!.id).then(({ rows, columns }) => {
setMessages(rows);
setColumns(columns);
});
}
}, [open]);
useEffect(() => {
const sessions = new Set<string>();
messages.forEach((row) => {
sessions.add(row.session_id);
});
setSessions(Array.from(sessions));
sessions;
}, [messages]);
return (
<BaseModal
size={selectedTab === 0 ? "sm-thin" : "md-thin"}
size={"md-thin"}
open={open}
setOpen={setOpen}
disable={disable}
onSubmit={() => sendMessage(1)}
onSubmit={() => sendMessage({ repeat: 1 })}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
@ -144,179 +186,233 @@ export default function IOModal({
<BaseModal.Content>
<div className="flex h-full flex-col ">
<div className="flex-max-width h-full">
{selectedTab !== 0 && (
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300"
)}
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300"
)}
>
<Tabs
value={selectedTab.toString()}
className={
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
}
onValueChange={(value) => {
setSelectedTab(Number(value));
}}
>
<Tabs
value={selectedTab.toString()}
className={
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
}
onValueChange={(value) => {
setSelectedTab(Number(value));
}}
>
<div className="api-modal-tablist-div">
<TabsList>
{inputs.length > 0 && (
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
)}
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
{/* {haveChat && (
<TabsTrigger value={"3"}>History</TabsTrigger>
)} */}
</TabsList>
</div>
<div className="api-modal-tablist-div">
<TabsList>
{inputs.length > 0 && (
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
)}
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
{haveChat && (
<TabsTrigger value={"0"}>Memories</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent
value={"1"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Type"} />
{TEXT_INPUT_MODAL_TITLE}
</div>
{nodes
.filter((node) =>
inputs.some((input) => input.id === node.id)
)
.map((node, index) => {
const input = inputs.find(
(input) => input.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={input.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(input);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
<TabsContent value={"1"} className="api-modal-tabs-content">
{nodes
.filter((node) =>
inputs.some((input) => input.id === node.id)
)
.map((node, index) => {
const input = inputs.find(
(input) => input.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
disabled={
node.data.node!.template["input_value"]?.value ===
""
}
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={input.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{input && (
<IOFieldView
type={InputOutput.INPUT}
left={true}
fieldType={input.type}
fieldId={input.id}
/>
)}
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(input);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent
value={"2"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Type"} />
{OUTPUTS_MODAL_TITLE}
</div>
{nodes
.filter((node) =>
outputs.some((output) => output.id === node.id)
)
.map((node, index) => {
const output = outputs.find(
(output) => output.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
}
key={index}
keyValue={input.id}
>
<AccordionComponent
disabled={
node.data.node!.template["input_value"]
?.value === ""
}
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={output.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(output);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
<div className="file-component-tab-column">
<div className="">
{input && (
<IOFieldView
type={InputOutput.INPUT}
left={true}
fieldType={input.type}
fieldId={input.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent value={"2"} className="api-modal-tabs-content">
{nodes
.filter((node) =>
outputs.some((output) => output.id === node.id)
)
.map((node, index) => {
const output = outputs.find(
(output) => output.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<ShadTooltip
content={output.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</div>
}
key={index}
keyValue={output.id}
>
<div className="file-component-tab-column">
<div className="">
{output && (
<IOFieldView
type={InputOutput.OUTPUT}
left={true}
fieldType={output.type}
fieldId={output.id}
/>
)}
</ShadTooltip>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(output);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
</AccordionComponent>
}
key={index}
keyValue={output.id}
>
<div className="file-component-tab-column">
<div className="">
{output && (
<IOFieldView
type={InputOutput.OUTPUT}
left={true}
fieldType={output.type}
fieldId={output.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent value={"0"} className="api-modal-tabs-content">
{sessions.map((session, index) => {
return (
<div
className="file-component-accordion-div cursor-pointer"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField({
id: session,
type: "Session",
});
}}
>
<div className="flex w-full items-center justify-between border-b px-2 py-1 align-middle">
<Badge variant="gray" size="md">
{session}
</Badge>
<div className="flex items-center justify-center gap-2 align-middle">
<Button
variant="ghost"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveSession(session);
if (selectedViewField?.id === session)
setSelectedViewField(undefined);
}}
>
<ShadTooltip
styleClasses="z-50"
content={"delete"}
>
<div>
<IconComponent
name="Trash2"
className="h-4 w-4"
></IconComponent>
</div>
</ShadTooltip>
</Button>
{/* <div>
<ShadTooltip
styleClasses="z-50"
content={
flow_sessions.some(
(f_session) =>
f_session?.session_id === session,
)
? "Active Session"
: "Inactive Session"
}
>
<div
className={cn(
"h-2 w-2 rounded-full",
flow_sessions.some(
(f_session) =>
f_session?.session_id === session,
)
? "bg-status-green"
: "bg-slate-500",
)}
></div>
</ShadTooltip>
</div> */}
</div>
);
})}
</TabsContent>
</Tabs>
</div>
)}
</div>
</div>
);
})}
</TabsContent>
</Tabs>
</div>
<div className="flex h-full min-w-96 flex-grow">
{selectedViewField && (
<div
@ -342,14 +438,17 @@ export default function IOModal({
<div className="h-full w-full">
{inputs.some(
(input) => input.id === selectedViewField.id
) ? (
) && (
<IOFieldView
type={InputOutput.INPUT}
left={false}
fieldType={selectedViewField.type!}
fieldId={selectedViewField.id!}
/>
) : (
)}
{outputs.some(
(output) => output.id === selectedViewField.id
) && (
<IOFieldView
type={InputOutput.OUTPUT}
left={false}
@ -357,6 +456,16 @@ export default function IOModal({
fieldId={selectedViewField.id!}
/>
)}
{sessions.some(
(session) => session === selectedViewField.id
) && (
<SessionView
rows={messages.filter(
(message) =>
message.session_id === selectedViewField.id
)}
/>
)}
</div>
</div>
)}

View file

@ -0,0 +1,252 @@
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import {
ReactNode,
forwardRef,
useContext,
useEffect,
useRef,
useState,
} from "react";
// import "ace-builds/webpack-resolver";
import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent";
import {
EXPORT_CODE_DIALOG,
LANGFLOW_SUPPORTED_TYPES,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import useFlowStore from "../../stores/flowStore";
import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index";
import { buildTweaks, convertArrayToObj } from "../../utils/reactflowUtils";
import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
tabsArray,
} from "../../utils/utils";
import BaseModal from "../baseModal";
const ApiModal = forwardRef(
(
{
flow,
children,
open: myOpen,
setOpen: mySetOpen,
}: {
flow: FlowType;
children: ReactNode;
open: any;
setOpen: any;
},
ref
) => {
const { autoLogin } = useContext(AuthContext);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]);
const [getTweak, setTweak] = useState<tweakType>([]);
const flowState = useFlowStore((state) => state.flowState);
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
const pythonCode = getPythonCode(flow, tweak.current);
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
const tweaksCode = buildTweaks(flow);
const codesArray = [
curl_code,
pythonApiCode,
pythonCode,
widgetCode,
pythonCode,
];
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
function startState() {
tweak.current = [];
setTweak([]);
tweaksList.current = [];
}
useEffect(() => {
if (flow["data"]!["nodes"].length == 0) {
startState();
} else {
tweak.current = [];
const t = buildTweaks(flow);
tweak.current.push(t);
}
filterNodes();
if (Object.keys(tweaksCode).length > 0) {
setActiveTab("0");
setTabs(tabsArray(codesArray, 1));
} else {
setTabs(tabsArray(codesArray, 1));
}
}, [flow["data"]!["nodes"], open]);
function filterNodes() {
let arrNodesWithValues: string[] = [];
flow["data"]!["nodes"].forEach((node) => {
if (!node["data"]["node"]["template"]) {
return;
}
Object.keys(node["data"]["node"]["template"])
.filter(
(templateField) =>
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show &&
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type
)
)
.map((n, i) => {
arrNodesWithValues.push(node["id"]);
});
});
tweaksList.current = arrNodesWithValues.filter((value, index, self) => {
return self.indexOf(value) === index;
});
}
function buildTweakObject(
tw: string,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType
) {
if (typeof changes === "string" && template.type === "float") {
changes = parseFloat(changes);
}
if (typeof changes === "string" && template.type === "int") {
changes = parseInt(changes);
}
if (template.list === true && Array.isArray(changes)) {
changes = changes?.filter((x) => x !== "");
}
if (template.type === "dict" && Array.isArray(changes)) {
changes = convertArrayToObj(changes);
}
if (template.type === "NestedDict") {
changes = JSON.stringify(changes);
}
const existingTweak = tweak.current.find((element) =>
element.hasOwnProperty(tw)
);
if (existingTweak) {
existingTweak[tw][template["name"]!] = changes as string;
if (existingTweak[tw][template["name"]!] == template.value) {
tweak.current.forEach((element) => {
if (element[tw] && Object.keys(element[tw])?.length === 0) {
tweak.current = tweak.current.filter((obj) => {
const prop = obj[Object.keys(obj)[0]].prop;
return prop !== undefined && prop !== null && prop !== "";
});
}
});
}
} else {
const newTweak = {
[tw]: {
[template["name"]!]: changes,
},
} as uniqueTweakType;
tweak.current.push(newTweak);
}
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
const pythonCode = getPythonCode(flow, tweak.current);
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
tabs![0].code = curl_code;
tabs![1].code = pythonApiCode;
tabs![2].code = pythonCode;
tabs![3].code = widgetCode;
setTweak(tweak.current);
}
function buildContent(value: string) {
const htmlContent = (
<div className="w-[200px]">
<span>{value != null && value != "" ? value : "None"}</span>
</div>
);
return htmlContent;
}
function getValue(
value: string,
node: NodeType,
template: TemplateVariableType
) {
let returnValue = value ?? "";
if (getTweak.length > 0) {
for (const obj of getTweak) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (key == node["id"]) {
Object.keys(value).forEach((key) => {
if (key == template["name"]) {
returnValue = value[key];
}
});
}
});
}
} else {
return value ?? "";
}
return returnValue;
}
return (
<BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">API</span>
<IconComponent
name="Code2"
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<CodeTabsComponent
flow={flow}
tabs={tabs!}
activeTab={activeTab}
setActiveTab={setActiveTab}
tweaks={{
tweak,
tweaksList,
buildContent,
buildTweakObject,
getValue,
}}
/>
</BaseModal.Content>
</BaseModal>
);
}
);
export default ApiModal;

View file

@ -31,9 +31,13 @@ const ApiModal = forwardRef(
{
flow,
children,
open: myOpen,
setOpen: mySetOpen,
}: {
flow: FlowType;
children: ReactNode;
open?: boolean;
setOpen?: (a: boolean | ((o?: boolean) => boolean)) => void;
},
ref
) => {
@ -44,7 +48,10 @@ const ApiModal = forwardRef(
const [activeTweaks, setActiveTweaks] = useState(false);
const { autoLogin } = useContext(AuthContext);
const [open, setOpen] = useState(false);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const [activeTab, setActiveTab] = useState("0");
const pythonApiCode = getPythonApiCode(
flow?.id,

View file

@ -16,11 +16,11 @@ import {
} from "../../components/ui/dialog-with-no-close";
import { DialogClose } from "@radix-ui/react-dialog";
import * as Form from "@radix-ui/react-form";
import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size";
import * as Form from "@radix-ui/react-form";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
@ -103,7 +103,7 @@ interface BaseModalProps {
React.ReactElement<ContentProps>,
React.ReactElement<HeaderProps>,
React.ReactElement<TriggerProps>?,
React.ReactElement<FooterProps>?,
React.ReactElement<FooterProps>?
];
open?: boolean;
setOpen?: (open: boolean) => void;
@ -121,7 +121,8 @@ interface BaseModalProps {
| "medium-h-full"
| "md-thin"
| "sm-thin"
| "smaller-h-full";
| "smaller-h-full"
| "medium-log";
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
@ -138,16 +139,16 @@ function BaseModal({
onSubmit,
}: BaseModalProps) {
const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header,
(child) => (child as React.ReactElement).type === Header
);
const triggerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Trigger,
(child) => (child as React.ReactElement).type === Trigger
);
const ContentChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Content,
(child) => (child as React.ReactElement).type === Content
);
const ContentFooter = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Footer,
(child) => (child as React.ReactElement).type === Footer
);
let { minWidth, height } = switchCaseModalSize(size);

View file

@ -1,6 +1,5 @@
import { ColDef, ValueGetterParams } from "ag-grid-community";
import { useMemo } from "react";
import TableAutoCellRender from "../../../components/tableComponent/components/tableAutoCellRender";
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
import TableTooltipRender from "../../../components/tableComponent/components/tableTooltipRender";
@ -9,7 +8,7 @@ const useColumnDefs = (
myData: any,
handleOnNewValue: (newValue: any, name: string) => void,
changeAdvanced: (n: string) => void,
open: boolean,
open: boolean
) => {
const columnDefs: ColDef[] = useMemo(
() => [
@ -82,7 +81,7 @@ const useColumnDefs = (
cellClass: "no-border",
},
],
[open, myData],
[open, myData]
);
return columnDefs;

View file

@ -15,11 +15,13 @@ const EditNodeModal = forwardRef(
nodeLength,
open,
setOpen,
// setOpenWDoubleClick,
data,
}: {
nodeLength: number;
open: boolean;
setOpen: (open: boolean) => void;
// setOpenWDoubleClick: (open: boolean) => void;
data: NodeDataType;
},
ref
@ -92,6 +94,7 @@ const EditNodeModal = forwardRef(
<div className="h-full">
{nodeLength > 0 && (
<TableComponent
key={"editNode"}
onGridReady={(params) => {
setGridApi(params.api);
}}

View file

@ -45,7 +45,7 @@ const ExportModal = forwardRef(
is_component: false,
},
name!,
description,
description
);
setNoticeData({
title: API_WARNING_NOTICE_ALERT,
@ -61,7 +61,7 @@ const ExportModal = forwardRef(
is_component: false,
}),
name!,
description,
description
);
setOpen(false);
}}
@ -102,6 +102,6 @@ const ExportModal = forwardRef(
<BaseModal.Footer submit={{ label: "Download Flow" }} />
</BaseModal>
);
},
}
);
export default ExportModal;

View file

@ -86,6 +86,7 @@ export default function FlowLogsModal({
</TabsList>
</Tabs>
<TableComponent
key={activeTab}
readOnlyEdit
className="h-max-full h-full w-full"
pagination={rows.length === 0 ? false : true}

View file

@ -209,7 +209,7 @@ export default function GenericModal({
<div
className={classNames(
!isEdit ? "rounded-lg border" : "",
"flex h-full w-full"
"flex h-full max-h-[85%] w-full"
)}
>
{type === TypeModal.PROMPT && isEdit && !readonly ? (
@ -244,7 +244,7 @@ export default function GenericModal({
) : type !== TypeModal.PROMPT ? (
<Textarea
ref={textRef}
className="form-input h-full w-full rounded-lg focus-visible:ring-1"
className="form-input h-full w-full overflow-auto rounded-lg focus-visible:ring-1"
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value);

View file

@ -7,7 +7,6 @@ import { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
import { createApiKey } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { ApiKeyType } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
export default function SecretKeyModal({

View file

@ -47,7 +47,10 @@ export default function ShareModal({
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [internalOpen, internalSetOpen] =
setOpen !== undefined && open !== undefined
? [open, setOpen]
: useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "component" : "workflow";
@ -66,13 +69,13 @@ export default function ShareModal({
const description = component?.description ?? "";
useEffect(() => {
if (open || internalOpen) {
if (internalOpen) {
if (hasApiKey && hasStore) {
handleGetTags();
handleGetNames();
}
}
}, [open, internalOpen, hasApiKey, hasStore]);
}, [internalOpen, hasApiKey, hasStore]);
function handleGetTags() {
setLoadingTags(true);
@ -146,8 +149,7 @@ export default function ShareModal({
const handleUpdateComponent = () => {
handleShareComponent(true);
if (setOpen) setOpen(false);
else internalSetOpen(false);
internalSetOpen(false);
};
const handleExportComponent = () => {
@ -199,8 +201,8 @@ export default function ShareModal({
<>
<BaseModal
size="smaller-h-full"
open={(!disabled && open) ?? internalOpen}
setOpen={setOpen ?? internalSetOpen}
open={!disabled && internalOpen}
setOpen={internalSetOpen}
onSubmit={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
@ -208,7 +210,7 @@ export default function ShareModal({
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
internalSetOpen(false);
} else {
setOpenConfirmationModal(true);
}
@ -289,7 +291,7 @@ export default function ShareModal({
variant="outline"
className="gap-2"
onClick={() => {
(setOpen || internalSetOpen)(false);
internalSetOpen(false);
handleExportComponent();
}}
>

View file

@ -1,5 +1,13 @@
import _, { cloneDeep } from "lodash";
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
import {
KeyboardEvent,
MouseEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useHotkeys } from "react-hotkeys-hook";
import ReactFlow, {
Background,
Connection,
@ -11,6 +19,7 @@ import ReactFlow, {
SelectionDragHandler,
updateEdge,
} from "reactflow";
import GenericNode from "../../../../CustomNodes/GenericNode";
import {
INVALID_SELECTION_ERROR_ALERT,
UPLOAD_ALERT_LIST,
@ -20,6 +29,7 @@ import {
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../../../stores/shortcuts";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow";
@ -28,7 +38,6 @@ import {
generateNodeFromFlow,
getNodeId,
isValidConnection,
reconnectEdges,
scapeJSONParse,
updateIds,
validateSelection,
@ -37,7 +46,6 @@ import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import getRandomName from "./utils/get-random-name";
import isWrappedWithClass from "./utils/is-wrapped-with-class";
import GenericNode from "../../../../CustomNodes/genericNode";
const nodeTypes = {
genericNode: GenericNode,
@ -50,21 +58,22 @@ export default function Page({
flow: FlowType;
view?: boolean;
}): JSX.Element {
const preventDefault = true;
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const autoSaveCurrentFlow = useFlowsManagerStore(
(state) => state.autoSaveCurrentFlow,
(state) => state.autoSaveCurrentFlow
);
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [showCanvas, setSHowCanvas] = useState(
Object.keys(templates).length > 0 && Object.keys(types).length > 0,
Object.keys(templates).length > 0 && Object.keys(types).length > 0
);
const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance);
const setReactFlowInstance = useFlowStore(
(state) => state.setReactFlowInstance,
(state) => state.setReactFlowInstance
);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -81,10 +90,10 @@ export default function Page({
const paste = useFlowStore((state) => state.paste);
const resetFlow = useFlowStore((state) => state.resetFlow);
const lastCopiedSelection = useFlowStore(
(state) => state.lastCopiedSelection,
(state) => state.lastCopiedSelection
);
const setLastCopiedSelection = useFlowStore(
(state) => state.setLastCopiedSelection,
(state) => state.setLastCopiedSelection
);
const onConnect = useFlowStore((state) => state.onConnect);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
@ -107,7 +116,7 @@ export default function Page({
clonedSelection!,
clonedNodes,
clonedEdges,
getRandomName(),
getRandomName()
);
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
// const newEdges = reconnectEdges(newGroupNode, removedEdges);
@ -115,8 +124,8 @@ export default function Page({
...clonedNodes.filter(
(oldNodes) =>
!clonedSelection?.nodes.some(
(selectionNode) => selectionNode.id === oldNodes.id,
),
(selectionNode) => selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
@ -141,119 +150,13 @@ export default function Page({
const setNode = useFlowStore((state) => state.setNode);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
const selectedNode = lastSelection?.nodes ?? [];
const selectedEdges = lastSelection?.edges ?? [];
if (
selectionMenuVisible &&
(event.ctrlKey || event.metaKey) &&
event.key === "g"
) {
event.preventDefault();
handleGroupNode();
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "p" &&
selectedNode.length > 0
) {
event.preventDefault();
setNode(selectedNode[0].id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "d" &&
selectedNode.length > 0
) {
event.preventDefault();
paste(
{ nodes: selectedNode, edges: selectedEdges },
{
x: position.current.x,
y: position.current.y,
},
);
}
if (!isWrappedWithClass(event, "noundo")) {
if (
(event.key === "y" || (event.key === "z" && event.shiftKey)) &&
(event.ctrlKey || event.metaKey)
) {
event.preventDefault(); // prevent the default action
redo();
} else if (event.key === "z" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
undo();
}
}
if (
!isWrappedWithClass(event, "nocopy") &&
window.getSelection()?.toString().length === 0
) {
if (
(event.ctrlKey || event.metaKey) &&
event.key === "c" &&
lastSelection
) {
event.preventDefault();
setLastCopiedSelection(_.cloneDeep(lastSelection));
} else if (
(event.ctrlKey || event.metaKey) &&
event.key === "x" &&
lastSelection
) {
event.preventDefault();
setLastCopiedSelection(_.cloneDeep(lastSelection), true);
} else if (
(event.ctrlKey || event.metaKey) &&
event.key === "v" &&
lastCopiedSelection
) {
event.preventDefault();
takeSnapshot();
paste(lastCopiedSelection, {
x: position.current.x,
y: position.current.y,
});
} else if (
(event.ctrlKey || event.metaKey) &&
event.key === "g" &&
lastSelection
) {
event.preventDefault();
}
}
if (!isWrappedWithClass(event, "nodelete")) {
if (
(event.key === "Delete" || event.key === "Backspace") &&
lastSelection
) {
event.preventDefault();
takeSnapshot();
deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id));
}
}
};
const handleMouseMove = (event) => {
position.current = { x: event.clientX, y: event.clientY };
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("mousemove", handleMouseMove);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove);
};
}, [lastCopiedSelection, lastSelection, takeSnapshot, selectionMenuVisible]);
@ -274,9 +177,118 @@ export default function Page({
};
}, []);
function handleUndo(e: KeyboardEvent) {
e.preventDefault();
if (!isWrappedWithClass(e, "noundo")) {
undo();
}
}
function handleRedo(e: KeyboardEvent) {
e.preventDefault();
if (!isWrappedWithClass(e, "noundo")) {
redo();
}
}
function handleGroup(e: KeyboardEvent) {
e.preventDefault();
if (selectionMenuVisible) {
handleGroupNode();
}
}
function handleDuplicate(e: KeyboardEvent) {
e.preventDefault();
e.stopPropagation();
const selectedNode = nodes.filter((obj) => obj.selected);
if (selectedNode.length > 0) {
paste(
{ nodes: selectedNode, edges: [] },
{
x: position.current.x,
y: position.current.y,
}
);
}
}
function handleCopy(e: KeyboardEvent) {
e.preventDefault();
if (
!isWrappedWithClass(e, "nocopy") &&
window.getSelection()?.toString().length === 0 &&
lastSelection
) {
setLastCopiedSelection(_.cloneDeep(lastSelection));
}
}
function handleCut(e: KeyboardEvent) {
e.preventDefault();
if (
!isWrappedWithClass(e, "nocopy") &&
window.getSelection()?.toString().length === 0 &&
lastSelection
) {
setLastCopiedSelection(_.cloneDeep(lastSelection), true);
}
}
function handlePaste(e: KeyboardEvent) {
e.preventDefault();
if (
!isWrappedWithClass(e, "nocopy") &&
window.getSelection()?.toString().length === 0 &&
lastCopiedSelection
) {
takeSnapshot();
paste(lastCopiedSelection, {
x: position.current.x,
y: position.current.y,
});
}
}
function handleDelete(e: KeyboardEvent) {
e.preventDefault();
if (!isWrappedWithClass(e, "nodelete") && lastSelection) {
takeSnapshot();
deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id));
}
}
const undoAction = useShortcutsStore((state) => state.undo);
const redoAction = useShortcutsStore((state) => state.redo);
const copyAction = useShortcutsStore((state) => state.copy);
const duplicate = useShortcutsStore((state) => state.duplicate);
const deleteAction = useShortcutsStore((state) => state.delete);
const groupAction = useShortcutsStore((state) => state.group);
const cutAction = useShortcutsStore((state) => state.cut);
const pasteAction = useShortcutsStore((state) => state.paste);
//@ts-ignore
useHotkeys(undoAction, handleUndo, { preventDefault });
//@ts-ignore
useHotkeys(redoAction, handleRedo, { preventDefault });
//@ts-ignore
useHotkeys(groupAction, handleGroup, { preventDefault });
//@ts-ignore
useHotkeys(duplicate, handleDuplicate, { preventDefault });
//@ts-ignore
useHotkeys(copyAction, handleCopy, { preventDefault });
//@ts-ignore
useHotkeys(cutAction, handleCut, { preventDefault });
//@ts-ignore
useHotkeys(pasteAction, handlePaste, { preventDefault });
//@ts-ignore
useHotkeys(deleteAction, handleDelete, { preventDefault });
//@ts-ignore
useHotkeys("delete", handleDelete, { preventDefault });
useEffect(() => {
setSHowCanvas(
Object.keys(templates).length > 0 && Object.keys(types).length > 0,
Object.keys(templates).length > 0 && Object.keys(types).length > 0
);
}, [templates, types]);
@ -285,7 +297,7 @@ export default function Page({
takeSnapshot();
onConnect(params);
},
[takeSnapshot, onConnect],
[takeSnapshot, onConnect]
);
const onNodeDragStart: NodeDragHandler = useCallback(() => {
@ -326,7 +338,7 @@ export default function Page({
// Extract the data from the drag event and parse it as a JSON object
const data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("nodedata"),
event.dataTransfer.getData("nodedata")
);
const newId = getNodeId(data.type);
@ -342,7 +354,7 @@ export default function Page({
};
paste(
{ nodes: [newNode], edges: [] },
{ x: event.clientX, y: event.clientY },
{ x: event.clientX, y: event.clientY }
);
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
@ -371,7 +383,7 @@ export default function Page({
}
},
// Specify dependencies for useCallback
[getNodeId, setNodes, takeSnapshot, paste],
[getNodeId, setNodes, takeSnapshot, paste]
);
const onEdgeUpdateStart = useCallback(() => {
@ -387,7 +399,7 @@ export default function Page({
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[setEdges],
[setEdges]
);
const onEdgeUpdateEnd = useCallback((_, edge: Edge): void => {
@ -420,7 +432,7 @@ export default function Page({
(flow: OnSelectionChangeParams): void => {
setLastSelection(flow);
},
[],
[]
);
const onPaneClick = useCallback((flow) => {
@ -473,6 +485,7 @@ export default function Page({
zoomOnScroll={!view}
zoomOnPinch={!view}
panOnDrag={!view}
panActivationKeyCode={""}
proOptions={{ hideAttribution: true }}
onPaneClick={onPaneClick}
>

View file

@ -31,7 +31,6 @@ export default function ExtraSidebar(): JSX.Element {
const templates = useTypesStore((state) => state.templates);
const getFilterEdge = useFlowStore((state) => state.getFilterEdge);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const hasStore = useStoreStore((state) => state.hasStore);
const hasApiKey = useStoreStore((state) => state.hasApiKey);

View file

@ -1,8 +1,10 @@
import _, { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useUpdateNodeInternals } from "reactflow";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import IconComponent from "../../../../components/genericIconComponent";
import RenderIcons from "../../../../components/renderIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import {
Select,
@ -10,7 +12,6 @@ import {
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select-custom";
import { postCustomComponent } from "../../../../controllers/API";
import ConfirmationModal from "../../../../modals/confirmationModal";
import EditNodeModal from "../../../../modals/editNodeModal";
import ShareModal from "../../../../modals/shareModal";
@ -18,8 +19,8 @@ import useAlertStore from "../../../../stores/alertStore";
import { useDarkStore } from "../../../../stores/darkStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useShortcutsStore } from "../../../../stores/shortcuts";
import { useStoreStore } from "../../../../stores/storeStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import { nodeToolbarPropsType } from "../../../../types/components";
import { FlowType } from "../../../../types/flow";
@ -29,7 +30,7 @@ import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, cn } from "../../../../utils/utils";
import { classNames, cn, isThereModal } from "../../../../utils/utils";
import ToolbarSelectItem from "./toolbarSelectItem";
export default function NodeToolbarComponent({
@ -40,11 +41,18 @@ export default function NodeToolbarComponent({
showNode,
name = "code",
selected,
updateNodeCode,
setShowState,
onCloseAdvancedModal,
isOutdated,
}: nodeToolbarPropsType): JSX.Element {
const version = useDarkStore((state) => state.version);
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>(
createFlowComponent(cloneDeep(data), version)
);
const preventDefault = true;
const isMac = navigator.platform.toUpperCase().includes("MAC");
const nodeLength = Object.keys(data.node!.template).filter(
(templateField) =>
templateField.charAt(0) !== "_" &&
@ -58,12 +66,129 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "Any" ||
data.node.template[templateField].type === "int" ||
data.node.template[templateField].type === "dict" ||
data.node.template[templateField].type === "NestedDict"),
data.node.template[templateField].type === "NestedDict")
).length;
const templates = useTypesStore((state) => state.templates);
const hasStore = useStoreStore((state) => state.hasStore);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey);
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const unselectAll = useFlowStore((state) => state.unselectAll);
function handleMinimizeWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isMinimal) {
setShowState((show) => !show);
setShowNode(data.showNode ?? true ? false : true);
return;
}
setNoticeData({
title:
"Minimization are only available for nodes with one handle or fewer.",
});
return;
}
function handleUpdateWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (hasApiKey || hasStore) {
handleSelectChange("update");
}
}
function handleGroupWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isGroup) {
handleSelectChange("ungroup");
}
}
function handleShareWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isThereModal() && !showOverrideModal) return;
if (hasApiKey || hasStore) {
setShowconfirmShare((state) => !state);
}
}
function handleCodeWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isThereModal() && !openModal) return;
if (hasCode) return setOpenModal((state) => !state);
setNoticeData({ title: `You can not access ${data.id} code` });
}
function handleAdvancedWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isThereModal() && !showModalAdvanced) return;
if (!isGroup) {
setShowModalAdvanced((state) => !state);
}
}
function handleSaveWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (isThereModal() && !showOverrideModal) return;
if (isSaved) {
setShowOverrideModal((state) => !state);
return;
}
if (hasCode && !isSaved) {
saveComponent(cloneDeep(data), false);
setSuccessData({ title: `${data.id} saved successfully` });
return;
}
}
function handleDocsWShortcut(e: KeyboardEvent) {
e.preventDefault();
if (data.node?.documentation) {
return openInNewTab(data.node?.documentation);
}
setNoticeData({
title: `${data.id} docs is not available at the moment.`,
});
}
function handleDownloadWShortcut(e: KeyboardEvent) {
e.preventDefault();
downloadNode(flowComponent!);
}
function handleFreeze(e: KeyboardEvent) {
e.preventDefault();
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
}
const advanced = useShortcutsStore((state) => state.advanced);
const minimize = useShortcutsStore((state) => state.minimize);
const share = useShortcutsStore((state) => state.share);
const save = useShortcutsStore((state) => state.save);
const docs = useShortcutsStore((state) => state.docs);
const code = useShortcutsStore((state) => state.code);
const group = useShortcutsStore((state) => state.group);
const update = useShortcutsStore((state) => state.update);
const download = useShortcutsStore((state) => state.download);
const freeze = useShortcutsStore((state) => state.freeze);
useHotkeys(minimize, handleMinimizeWShortcut, { preventDefault });
useHotkeys(update, handleUpdateWShortcut, { preventDefault });
useHotkeys(group, handleGroupWShortcut, { preventDefault });
useHotkeys(share, handleShareWShortcut, { preventDefault });
useHotkeys(code, handleCodeWShortcut, { preventDefault });
useHotkeys(advanced, handleAdvancedWShortcut, { preventDefault });
useHotkeys(save, handleSaveWShortcut, { preventDefault });
useHotkeys(docs, handleDocsWShortcut, { preventDefault });
useHotkeys(download, handleDownloadWShortcut, { preventDefault });
useHotkeys(freeze, handleFreeze, { preventDefault });
const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false;
@ -75,18 +200,14 @@ export default function NodeToolbarComponent({
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const unselectAll = useFlowStore((state) => state.unselectAll);
const saveComponent = useFlowsManagerStore((state) => state.saveComponent);
const getNodePosition = useFlowStore((state) => state.getNodePosition);
const flows = useFlowsManagerStore((state) => state.flows);
const version = useDarkStore((state) => state.version);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>(
createFlowComponent(cloneDeep(data), version),
);
// useEffect(() => {
// if (openWDoubleClick) setShowModalAdvanced(true);
// }, [openWDoubleClick, setOpenWDoubleClick]);
const openInNewTab = (url) => {
window.open(url, "_blank", "noreferrer");
@ -100,7 +221,7 @@ export default function NodeToolbarComponent({
const updateNodeInternals = useUpdateNodeInternals();
const setLastCopiedSelection = useFlowStore(
(state) => state.setLastCopiedSelection,
(state) => state.setLastCopiedSelection
);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
@ -120,6 +241,27 @@ export default function NodeToolbarComponent({
const handleSelectChange = (event) => {
switch (event) {
case "save":
if (isSaved) {
return setShowOverrideModal(true);
}
saveComponent(cloneDeep(data), false);
break;
case "freeze":
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
frozen: old.data?.node?.frozen ? false : true,
},
},
}));
break;
case "code":
setOpenModal(!openModal);
break;
case "advanced":
setShowModalAdvanced(true);
break;
@ -154,7 +296,7 @@ export default function NodeToolbarComponent({
edges,
setNodes,
setEdges,
data.node?.outputs,
data.node?.outputs
);
break;
case "override":
@ -178,51 +320,56 @@ export default function NodeToolbarComponent({
y: 10,
paneX: nodes.find((node) => node.id === data.id)?.position.x,
paneY: nodes.find((node) => node.id === data.id)?.position.y,
},
}
);
break;
case "update":
takeSnapshot();
// to update we must get the code from the templates in useTypesStore
const thisNodeTemplate = templates[data.type]?.template;
// if the template does not have a code key
// return
if (!thisNodeTemplate?.code) return;
const currentCode = thisNodeTemplate.code.value;
if (data.node) {
postCustomComponent(currentCode, data.node)
.then((apiReturn) => {
const { data } = apiReturn;
if (data && updateNodeCode) {
updateNodeCode(data, currentCode, "code");
}
})
.catch((err) => {
console.log(err);
});
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...data,
};
newNode.data.node.template.code.value = currentCode;
return newNode;
});
}
break;
}
};
const isSaved = flows.some((flow) =>
Object.values(flow).includes(data.node?.display_name!),
Object.values(flow).includes(data.node?.display_name!)
);
function displayShortcut({
name,
shortcut,
}: {
name: string;
shortcut: string;
}): JSX.Element {
let hasShift: boolean = false;
const fixedShortcut = shortcut?.split("+");
fixedShortcut.forEach((key) => {
if (key.toLowerCase().includes("shift")) {
hasShift = true;
}
});
const filteredShortcut = fixedShortcut.filter(
(key) => !key.toLowerCase().includes("shift")
);
let shortcutWPlus: string[] = [];
if (!hasShift) shortcutWPlus = filteredShortcut.join("+").split(" ");
return (
<div className="flex justify-center">
<span> {name} </span>
<span
className={`ml-3 flex items-center rounded-sm bg-muted px-1 py-[0.2] text-muted-foreground`}
>
<RenderIcons
isMac={isMac}
hasShift={hasShift}
filteredShortcut={filteredShortcut}
shortcutWPlus={shortcutWPlus}
/>
</span>
</div>
);
}
const setNode = useFlowStore((state) => state.setNode);
const handleOnNewValue = (
newValue: string | string[] | boolean | Object[],
newValue: string | string[] | boolean | Object[]
): void => {
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
@ -269,131 +416,21 @@ export default function NodeToolbarComponent({
const [openModal, setOpenModal] = useState(false);
const hasCode = Object.keys(data.node!.template).includes("code");
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (
selected &&
(hasApiKey || hasStore) &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "U"
) {
event.preventDefault();
handleSelectChange("update");
}
if (selected && event.key.toUpperCase() === "ESCAPE") {
event.preventDefault();
handleSelectChange("unselect");
}
if (
selected &&
isGroup &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "G"
) {
event.preventDefault();
handleSelectChange("ungroup");
}
if (
selected &&
(hasApiKey || hasStore) &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key.toUpperCase() === "S"
) {
event.preventDefault();
setShowconfirmShare((state) => !state);
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key.toUpperCase() === "Q"
) {
event.preventDefault();
if (isMinimal) {
setShowState((show) => !show);
setShowNode(data.showNode ?? true ? false : true);
return;
}
setNoticeData({
title:
"Minimization are only available for nodes with one handle or fewer.",
});
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key.toUpperCase() === "U"
) {
event.preventDefault();
if (hasCode) return setOpenModal((state) => !state);
setNoticeData({ title: `You can not access ${data.id} code` });
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key.toUpperCase() === "A"
) {
event.preventDefault();
setShowModalAdvanced((state) => !state);
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "S"
) {
if (isSaved) {
event.preventDefault();
return setShowOverrideModal((state) => !state);
}
if (hasCode) {
event.preventDefault();
saveComponent(cloneDeep(data), false);
setSuccessData({ title: `${data.id} saved successfully` });
}
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key.toUpperCase() === "D"
) {
event.preventDefault();
if (data.node?.documentation) {
return openInNewTab(data.node?.documentation);
}
setNoticeData({
title: `${data.id} docs is not available at the moment.`,
});
}
if (
selected &&
(event.ctrlKey || event.metaKey) &&
event.key.toUpperCase() === "J"
) {
event.preventDefault();
downloadNode(flowComponent!);
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isSaved, showNode, data.showNode, isMinimal]);
return (
<>
<div className="w-26 nocopy nowheel nopan nodelete nodrag noundo h-10">
<span className="isolate inline-flex rounded-md shadow-sm">
{hasCode && (
<ShadTooltip content="Code" side="top">
<ShadTooltip
content={displayShortcut(
shortcuts.find(
({ name }) => name.split(" ")[0].toLowerCase() === "code"
)!
)}
side="top"
>
<button
className="relative inline-flex items-center rounded-l-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
className="relative inline-flex items-center rounded-l-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
onClick={() => {
setOpenModal(!openModal);
}}
@ -403,8 +440,30 @@ export default function NodeToolbarComponent({
</button>
</ShadTooltip>
)}
{nodeLength > 0 && (
<ShadTooltip
content={displayShortcut(
shortcuts.find(
({ name }) => name.split(" ")[0].toLowerCase() === "advanced"
)!
)}
side="top"
>
<button
className={`${
isGroup ? "rounded-l-md" : ""
} relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10`}
onClick={() => {
setShowModalAdvanced(true);
}}
data-testid="code-button-modal"
>
<IconComponent name="Settings2" className="h-4 w-4" />
</button>
</ShadTooltip>
)}
<ShadTooltip content={"Save"} side="top">
{/*<ShadTooltip content={"Save"} side="top">
<button
data-testid="save-button-modal"
className={classNames(
@ -421,27 +480,18 @@ export default function NodeToolbarComponent({
>
<IconComponent name="SaveAll" className="h-4 w-4" />
</button>
</ShadTooltip>
<ShadTooltip content={"Duplicate"} side="top">
<button
data-testid="duplicate-button-modal"
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
)}
onClick={(event) => {
event.preventDefault();
handleSelectChange("duplicate");
}}
>
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>
<ShadTooltip content="Freeze" side="top">
</ShadTooltip>*/}
<ShadTooltip
content={displayShortcut(
shortcuts.find(
({ name }) => name.split(" ")[0].toLowerCase() === "freeze"
)!
)}
side="top"
>
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
onClick={(event) => {
event.preventDefault();
@ -462,20 +512,35 @@ export default function NodeToolbarComponent({
className={cn(
"h-4 w-4 transition-all",
// TODO UPDATE THIS COLOR TO BE A VARIABLE
frozen ? "animate-wiggle text-ice" : "",
frozen ? "animate-wiggle text-ice" : ""
)}
/>
</button>
</ShadTooltip>
{/*<ShadTooltip content={"Duplicate"} side="top">
<button
data-testid="duplicate-button-modal"
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
)}
onClick={(event) => {
event.preventDefault();
handleSelectChange("duplicate");
}}
>
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>*/}
<Select onValueChange={handleSelectChange} value="">
<ShadTooltip content="More" side="top">
<ShadTooltip content="All" side="top">
<SelectTrigger>
<div>
<div
data-testid="more-options-modal"
className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
>
<IconComponent
@ -487,55 +552,72 @@ export default function NodeToolbarComponent({
</SelectTrigger>
</ShadTooltip>
<SelectContent>
{hasCode && (
<SelectItem value={"code"}>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Code")?.shortcut!
}
value={"Code"}
icon={"Code"}
dataTestId="code-button-modal"
/>
</SelectItem>
)}
{nodeLength > 0 && (
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
<ToolbarSelectItem
keyboardKey="A"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
shortcut={
shortcuts.find((obj) => obj.name === "Advanced Settings")
?.shortcut!
}
value={"Advanced"}
icon={"Settings2"}
dataTestId="edit-button-modal"
/>
</SelectItem>
)}
<SelectItem value={"save"}>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Save")?.shortcut!
}
value={"Save"}
icon={"SaveAll"}
dataTestId="save-button-modal"
/>
</SelectItem>
<SelectItem value={"duplicate"}>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut!
}
value={"Duplicate"}
icon={"Copy"}
dataTestId="copy-button-modal"
/>
</SelectItem>
<SelectItem value={"copy"}>
<ToolbarSelectItem
keyboardKey="C"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
shortcut={
shortcuts.find((obj) => obj.name === "Copy")?.shortcut!
}
value={"Copy"}
icon={"Clipboard"}
dataTestId="copy-button-modal"
/>
</SelectItem>
{isOutdated && (
<SelectItem value={"update"}>
<ToolbarSelectItem
keyboardKey="U"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Update"}
icon={"Code"}
dataTestId="update-button-modal"
ping={isOutdated}
/>
</SelectItem>
)}
{hasStore && (
<SelectItem
value={"Share"}
disabled={!hasApiKey || !validApiKey}
>
<ToolbarSelectItem
keyboardKey="S"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
shortcut={
shortcuts.find((obj) => obj.name === "Share")?.shortcut!
}
value={"Share"}
icon={"Share3"}
styleObj={{
iconClasses: "relative top-0.5 -m-1 mr-[0.25rem] h-6 w-6",
}}
dataTestId="share-button-modal"
/>
</SelectItem>
@ -543,13 +625,13 @@ export default function NodeToolbarComponent({
{(!hasStore || !hasApiKey || !validApiKey) && (
<SelectItem value={"Download"}>
<ToolbarSelectItem
value="Download"
shift={false}
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
icon="Download"
styleObj={{ iconClasses: "relative top-0.5 mr-2 h-4 w-4" }}
keyboardKey={"J"}
dataTestId={"Dowload-button-nodeToolbar"}
shortcut={
shortcuts.find((obj) => obj.name === "Download")
?.shortcut!
}
value={"Download"}
icon={"Download"}
dataTestId="Download-button-modal"
/>
</SelectItem>
)}
@ -558,9 +640,9 @@ export default function NodeToolbarComponent({
disabled={data.node?.documentation === ""}
>
<ToolbarSelectItem
keyboardKey="D"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
shortcut={
shortcuts.find((obj) => obj.name === "Docs")?.shortcut!
}
value={"Docs"}
icon={"FileText"}
dataTestId="docs-button-modal"
@ -569,38 +651,49 @@ export default function NodeToolbarComponent({
{isMinimal && (
<SelectItem value={"show"}>
<ToolbarSelectItem
icon={showNode ? "Minimize2" : "Maximize2"}
shortcut={
shortcuts.find((obj) => obj.name === "Minimize")
?.shortcut!
}
value={showNode ? "Minimize" : "Expand"}
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
keyboardKey={"Q"}
dataTestId={"minimize-button-nodeToolbar"}
icon={showNode ? "Minimize2" : "Maximize2"}
dataTestId="minimize-button-modal"
/>
</SelectItem>
)}
{isGroup && (
<SelectItem value="ungroup">
<div className="flex">
<IconComponent
name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
<span className="">Ungroup</span>{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.30rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.43em]">G</span>
</div>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Group")?.shortcut!
}
value={"Ungroup"}
icon={"Ungroup"}
dataTestId="group-button-modal"
/>
</SelectItem>
)}
<SelectItem value="freeze">
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Freeze")?.shortcut!
}
value={"Freeze"}
icon={"Snowflake"}
dataTestId="group-button-modal"
style={`${frozen ? " text-ice" : ""} transition-all`}
/>
</SelectItem>
<SelectItem value="Download">
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Download")?.shortcut!
}
value={"Download"}
icon={"Download"}
dataTestId="download-button-modal"
/>
</SelectItem>
<SelectItem value={"delete"} className="focus:bg-red-400/[.20]">
<div className="font-red flex text-status-red">
<IconComponent
@ -608,10 +701,10 @@ export default function NodeToolbarComponent({
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
<span className="">Delete</span>{" "}
<span>
<span className="justify absolute right-2 top-2 flex items-center rounded-sm bg-muted px-1 py-[0.2]">
<IconComponent
name="Delete"
className="absolute right-2 top-2 h-4 w-4 stroke-2 text-red-400"
className="h-4 w-4 stroke-2 text-red-400"
></IconComponent>
</span>
</div>
@ -644,12 +737,15 @@ export default function NodeToolbarComponent({
</span>
</ConfirmationModal.Content>
</ConfirmationModal>
<EditNodeModal
data={data}
nodeLength={nodeLength}
open={showModalAdvanced}
setOpen={setShowModalAdvanced}
/>
{showModalAdvanced && (
<EditNodeModal
// setOpenWDoubleClick={setOpenWDoubleClick}
data={data}
nodeLength={nodeLength}
open={showModalAdvanced}
setOpen={setShowModalAdvanced}
/>
)}
{showconfirmShare && (
<ShareModal
open={showconfirmShare}

View file

@ -1,50 +1,51 @@
import ForwardedIconComponent from "../../../../../components/genericIconComponent";
import RenderIcons from "../../../../../components/renderIconComponent";
import { toolbarSelectItemProps } from "../../../../../types/components";
export default function ToolbarSelectItem({
shift,
isMac,
keyboardKey,
value,
icon,
styleObj,
style,
dataTestId,
ping,
shortcut,
}: toolbarSelectItemProps) {
const isMac = navigator.platform.toUpperCase().includes("MAC");
let hasShift = false;
const fixedShortcut = shortcut?.split("+");
fixedShortcut.forEach((key) => {
if (key.toLowerCase().includes("shift")) {
hasShift = true;
}
});
const filteredShortcut = fixedShortcut.filter(
(key) => !key.toLowerCase().includes("shift")
);
let shortcutWPlus: string[] = [];
if (!hasShift) shortcutWPlus = filteredShortcut.join("+").split(" ");
return (
<div className="flex" data-testid={dataTestId}>
<div className={`flex ${style}`} data-testid={dataTestId}>
<ForwardedIconComponent
name={icon}
className={`relative top-0.5 mr-2 h-4 w-4 ${styleObj?.iconClasses} ${
ping && "animate-pulse text-green-500"
}`}
className={` mr-2 ${
icon === "Share3"
? "absolute left-2 top-[0.25em] h-6 w-6"
: "mt-[0.15em] h-4 w-4"
} ${ping && "animate-pulse text-green-500"}`}
/>
<span className={styleObj?.valueClasses}>{value}</span>
{isMac ? (
<ForwardedIconComponent
name="Command"
className={`absolute
${shift ? " right-[2rem] " : "right-[1.15rem]"}
top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.commandClasses}`}
></ForwardedIconComponent>
) : (
<span
className={`absolute ${
shift ? " right-[2.15rem] " : "right-[1.15rem]"
} top-[0.43em] stroke-2 `}
>
{shift ? "Ctrl" : "Ctrl +"}
</span>
)}
{shift && (
<ForwardedIconComponent
name="ArrowBigUp"
className={`absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.shiftClasses}`}
<span className={`${icon === "Share3" ? "ml-[1.8em]" : " "}`}>
{value}
</span>
<span
className={`absolute right-2 top-[0.43em] flex items-center rounded-sm bg-muted px-1 py-[0.2] text-muted-foreground `}
>
<RenderIcons
isMac={isMac}
hasShift={hasShift}
filteredShortcut={filteredShortcut}
shortcutWPlus={shortcutWPlus}
/>
)}
<span className={`absolute right-2 top-[0.43em] ${styleObj?.keyClasses}`}>
{keyboardKey}
</span>
</div>
);

View file

@ -0,0 +1,47 @@
import { useCallback } from "react";
const useDeleteMultipleFlows = (
selectedFlowsComponentsCards,
removeFlow,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setErrorData
) => {
const handleDeleteMultiple = useCallback(() => {
removeFlow(selectedFlowsComponentsCards)
.then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSuccessData({
title: "Selected items deleted successfully",
});
})
.catch(() => {
setErrorData({
title: "Error deleting items",
list: ["Please try again"],
});
});
}, [
selectedFlowsComponentsCards,
removeFlow,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setErrorData,
]);
return { handleDeleteMultiple };
};
export default useDeleteMultipleFlows;

View file

@ -0,0 +1,32 @@
import { useMemo } from "react";
const useDescriptionModal = (selectedFlowsComponentsCards, type) => {
const getDescriptionModal = useMemo(() => {
const getTypeLabel = (type) => {
const labels = {
all: "item",
component: "component",
flow: "flow",
};
return labels[type] || "";
};
const getPluralizedLabel = (type) => {
const labels = {
all: "items",
component: "components",
flow: "flows",
};
return labels[type] || "";
};
if (selectedFlowsComponentsCards?.length === 1) {
return getTypeLabel(type);
}
return getPluralizedLabel(type);
}, [selectedFlowsComponentsCards, type]);
return getDescriptionModal;
};
export default useDescriptionModal;

View file

@ -0,0 +1,27 @@
import cloneDeep from "lodash/cloneDeep";
import { useEffect } from "react";
const useFilteredFlows = (
flowsFromFolder,
searchFlowsComponents,
setAllFlows
) => {
useEffect(() => {
const newFlows = cloneDeep(flowsFromFolder || []);
const filteredFlows = newFlows.filter(
(f) =>
f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) ||
f.description
.toLowerCase()
.includes(searchFlowsComponents.toLowerCase())
);
if (searchFlowsComponents === "") {
setAllFlows(flowsFromFolder);
} else {
setAllFlows(filteredFlows);
}
}, [flowsFromFolder, searchFlowsComponents, setAllFlows]);
};
export default useFilteredFlows;

View file

@ -0,0 +1,53 @@
import { useCallback } from "react";
const useDuplicateFlows = (
selectedFlowsComponentsCards,
addFlow,
allFlows,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes
) => {
const handleDuplicate = useCallback(() => {
Promise.all(
selectedFlowsComponentsCards.map((selectedFlow) =>
addFlow(
true,
allFlows.find((flow) => flow.id === selectedFlow)
)
)
).then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSuccessData({ title: `${cardTypes} duplicated successfully` });
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
});
}, [
selectedFlowsComponentsCards,
addFlow,
allFlows,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
]);
return { handleDuplicate };
};
export default useDuplicateFlows;

View file

@ -0,0 +1,50 @@
import { useCallback } from "react";
const useExportFlows = (
selectedFlowsComponentsCards,
allFlows,
downloadFlow,
removeApiKeys,
version,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes
) => {
const handleExport = useCallback(() => {
selectedFlowsComponentsCards.forEach((selectedFlowId) => {
const selectedFlow = allFlows.find((flow) => flow.id === selectedFlowId);
if (selectedFlow) {
downloadFlow(
removeApiKeys({
id: selectedFlow.id,
data: selectedFlow.data,
description: selectedFlow.description,
name: selectedFlow.name,
last_tested_version: version,
is_component: false,
}),
selectedFlow.name,
selectedFlow.description
);
}
});
setSuccessData({ title: `${cardTypes} exported successfully` });
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
}, [
selectedFlowsComponentsCards,
allFlows,
downloadFlow,
removeApiKeys,
version,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
]);
return { handleExport };
};
export default useExportFlows;

View file

@ -0,0 +1,25 @@
import { useCallback } from "react";
const useSelectAll = (flowsFromFolder, getValues, setValue) => {
const handleSelectAll = useCallback(
(select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);
if (select) {
Object.keys(getValues()).forEach((key) => {
if (!flowsFromFolderIds?.includes(key)) return;
setValue(key, true);
});
return;
}
Object.keys(getValues()).forEach((key) => {
setValue(key, false);
});
},
[flowsFromFolder, getValues, setValue]
);
return { handleSelectAll };
};
export default useSelectAll;

View file

@ -0,0 +1,40 @@
import { useCallback } from "react";
const useSelectOptionsChange = (
selectedFlowsComponentsCards,
setErrorData,
setOpenDelete,
handleDuplicate,
handleExport
) => {
const handleSelectOptionsChange = useCallback(
(action) => {
const hasSelected = selectedFlowsComponentsCards?.length > 0;
if (!hasSelected) {
setErrorData({
title: "No items selected",
list: ["Please select items to delete"],
});
return;
}
if (action === "delete") {
setOpenDelete(true);
} else if (action === "duplicate") {
handleDuplicate();
} else if (action === "export") {
handleExport();
}
},
[
selectedFlowsComponentsCards,
setErrorData,
setOpenDelete,
handleDuplicate,
handleExport,
]
);
return { handleSelectOptionsChange };
};
export default useSelectOptionsChange;

View file

@ -0,0 +1,18 @@
import { useEffect } from "react";
const useSelectedFlows = (
entireFormValues,
setSelectedFlowsComponentsCards
) => {
useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;
const selectedFlows = Object.keys(entireFormValues).filter((key) => {
return entireFormValues[key] === true;
});
setSelectedFlowsComponentsCards(selectedFlows);
}, [entireFormValues, setSelectedFlowsComponentsCards]);
};
export default useSelectedFlows;

View file

@ -1,28 +1,33 @@
import { cloneDeep } from "lodash";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Link, useLocation, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
import useAlertStore from "../../../../stores/alertStore";
import { useDarkStore } from "../../../../stores/darkStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { FlowType } from "../../../../types/flow";
import { downloadFlow, removeApiKeys } from "../../../../utils/reactflowUtils";
import useFileDrop from "../../hooks/use-on-file-drop";
import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows";
import EmptyComponent from "../emptyComponent";
import HeaderComponent from "../headerComponent";
import { downloadFlow, removeApiKeys } from "../../../../utils/reactflowUtils";
import { useDarkStore } from "../../../../stores/darkStore";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import useDeleteMultipleFlows from "./hooks/use-delete-multiple";
import useDescriptionModal from "./hooks/use-description-modal";
import useFilteredFlows from "./hooks/use-filtered-flows";
import useDuplicateFlows from "./hooks/use-handle-duplicate";
import useExportFlows from "./hooks/use-handle-export";
import useSelectAll from "./hooks/use-handle-select-all";
import useSelectOptionsChange from "./hooks/use-select-options-change";
import useSelectedFlows from "./hooks/use-selected-flows";
export default function ComponentsComponent({
type = "all",
@ -36,22 +41,22 @@ export default function ComponentsComponent({
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const flowsFromFolder = useFolderStore(
(state) => state.selectedFolder?.flows,
(state) => state.selectedFolder?.flows
);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [openDelete, setOpenDelete] = useState(false);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
(state) => state.searchFlowsComponents
);
const setSelectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.setSelectedFlowsComponentsCards,
(state) => state.setSelectedFlowsComponentsCards
);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
(state) => state.selectedFlowsComponentsCards
);
const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
@ -73,6 +78,16 @@ export default function ComponentsComponent({
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const cardTypes = useMemo(() => {
if (window.location.pathname.includes("components")) {
return "Components";
}
if (window.location.pathname.includes("flows")) {
return "Flows";
}
return "Items";
}, [window.location]);
useEffect(() => {
setFolderUrl(folderId ?? "");
setSelectedFlowsComponentsCards([]);
@ -80,22 +95,7 @@ export default function ComponentsComponent({
getFolderById(folderId ? folderId : myCollectionId);
}, [location]);
useEffect(() => {
const newFlows = cloneDeep(flowsFromFolder!);
const filteredFlows = newFlows?.filter(
(f) =>
f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) ||
f.description
.toLowerCase()
.includes(searchFlowsComponents.toLowerCase()),
);
if (searchFlowsComponents === "") {
setAllFlows(flowsFromFolder!);
}
setAllFlows(filteredFlows);
}, [searchFlowsComponents]);
useFilteredFlows(flowsFromFolder, searchFlowsComponents, setAllFlows);
const resetFilter = () => {
setPageIndex(1);
@ -106,20 +106,12 @@ export default function ComponentsComponent({
const entireFormValues = useWatch({ control });
const methods = useForm();
const handleSelectAll = (select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);
if (select) {
Object.keys(getValues()).forEach((key) => {
if (!flowsFromFolderIds?.includes(key)) return;
setValue(key, true);
});
return;
}
Object.keys(getValues()).forEach((key) => {
setValue(key, false);
});
};
const { handleSelectAll } = useSelectAll(
flowsFromFolder,
getValues,
setValue
);
const handleSelectOptionsChange = (action: string) => {
const hasSelected = selectedFlowsComponentsCards?.length > 0;
@ -144,9 +136,9 @@ export default function ComponentsComponent({
selectedFlowsComponentsCards.map((selectedFlow) =>
addFlow(
true,
allFlows.find((flow) => flow.id === selectedFlow),
),
),
allFlows.find((flow) => flow.id === selectedFlow)
)
)
).then(() => {
resetFilter();
getFoldersApi(true);
@ -194,7 +186,7 @@ export default function ComponentsComponent({
is_component: false,
}),
selectedFlow!.name,
selectedFlow!.description,
selectedFlow!.description
);
});
setSuccessData({ title: "Flows exported successfully" });
@ -219,51 +211,67 @@ export default function ComponentsComponent({
});
});
};
const { handleDuplicate } = useDuplicateFlows(
selectedFlowsComponentsCards,
addFlow,
allFlows,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes
);
useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;
const selectedFlows: string[] = Object.keys(entireFormValues).filter(
(key) => {
if (entireFormValues[key] === true) {
return true;
}
return false;
},
);
const version = useDarkStore((state) => state.version);
setSelectedFlowsComponentsCards(selectedFlows);
}, [entireFormValues]);
const { handleExport } = useExportFlows(
selectedFlowsComponentsCards,
allFlows,
downloadFlow,
removeApiKeys,
version,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes
);
const getDescriptionModal = useMemo(() => {
const getTypeLabel = (type) => {
const labels = {
all: "item",
component: "component",
flow: "flow",
};
return labels[type] || "";
};
const { handleSelectOptionsChange } = useSelectOptionsChange(
selectedFlowsComponentsCards,
setErrorData,
setOpenDelete,
handleDuplicate,
handleExport
);
const getPluralizedLabel = (type) => {
const labels = {
all: "items",
component: "components",
flow: "flows",
};
return labels[type] || "";
};
const { handleDeleteMultiple } = useDeleteMultipleFlows(
selectedFlowsComponentsCards,
removeFlow,
resetFilter,
getFoldersApi,
folderId,
myCollectionId,
getFolderById,
setSuccessData,
setErrorData
);
if (selectedFlowsComponentsCards?.length === 1) {
return getTypeLabel(type);
}
return getPluralizedLabel(type);
}, [selectedFlowsComponentsCards, type]);
useSelectedFlows(entireFormValues, setSelectedFlowsComponentsCards);
const descriptionModal = useDescriptionModal(
selectedFlowsComponentsCards,
type
);
const getTotalRowsCount = () => {
if (type === "all") return allFlows?.length;
return allFlows?.filter(
(f) => (f.is_component ?? false) === (type === "component"),
(f) => (f.is_component ?? false) === (type === "component")
)?.length;
};
@ -370,7 +378,7 @@ export default function ComponentsComponent({
open={openDelete}
setOpen={setOpenDelete}
onConfirm={handleDeleteMultiple}
description={getDescriptionModal}
description={descriptionModal}
>
<></>
</DeleteConfirmationModal>

View file

@ -1,11 +1,9 @@
import { useState } from "react";
import IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import IconComponent from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Button } from "../../../../components/ui/button";
import { Checkbox } from "../../../../components/ui/checkbox";
import { cn } from "../../../../utils/utils";
import { Button } from "../../../../components/ui/button";
type HeaderComponentProps = {
handleSelectAll: (select) => void;
@ -109,7 +107,7 @@ const HeaderComponent = ({
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
disableFunctions ? "" : "hover:text-destructive",
disableFunctions ? "" : "hover:text-destructive"
)}
/>
</Button>

View file

@ -16,7 +16,7 @@ import useDropdownOptions from "../../hooks/use-dropdown-options";
export default function HomePage(): JSX.Element {
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
(state) => state.setCurrentFlowId
);
const location = useLocation();

View file

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

View file

@ -2,7 +2,6 @@ import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import GradientChooserComponent from "../../components/gradientChooserComponent";
import Header from "../../components/headerComponent";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
@ -22,6 +21,7 @@ import {
patchUserInputStateType,
} from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import GradientChooserComponent from "../SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent";
export default function ProfileSettingsPage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId

View file

@ -8,7 +8,7 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore";
export default function SettingsPage(): JSX.Element {
const pathname = location.pathname;
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
(state) => state.setCurrentFlowId
);
useEffect(() => {
setCurrentFlowId("");

View file

@ -2,17 +2,14 @@ import ForwardedIconComponent from "../../../../../../components/genericIconComp
import { Button } from "../../../../../../components/ui/button";
import { API_PAGE_PARAGRAPH } from "../../../../../../constants/constants";
import SecretKeyModal from "../../../../../../modals/secretKeyModal";
import { cn } from "../../../../../../utils/utils";
type ApiKeyHeaderComponentProps = {
selectedRows: string[];
handleDeleteKey: () => void;
fetchApiKeys: () => void;
userId: string;
};
const ApiKeyHeaderComponent = ({
selectedRows,
handleDeleteKey,
fetchApiKeys,
userId,
}: ApiKeyHeaderComponentProps) => {
@ -30,20 +27,6 @@ const ApiKeyHeaderComponent = ({
<p className="text-sm text-muted-foreground">{API_PAGE_PARAGRAPH}</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleDeleteKey}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
<SecretKeyModal data={userId} onCloseModal={fetchApiKeys}>
<Button data-testid="api-key-button-store" variant="primary">
<ForwardedIconComponent name="Plus" className="mr-2 w-4" />

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