Feat: Implement Customizable Shortcuts

This pull request improve user experience by enabling customization of
keyboard shortcuts.
This commit is contained in:
Igor Carvalho 2024-06-10 01:08:26 -03:00 • committed by GitHub
commit 20e7b82ba4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1587 additions and 684 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",
@ -11022,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

@ -36,7 +36,7 @@ import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
import ParameterComponent from "./components/parameterComponent";
import { postCustomComponent } from "../../controllers/API";
import { cloneDeep } from "lodash";
import { useShortcutsStore } from "../../stores/shortcuts";
export default function GenericNode({
data,
@ -137,6 +137,8 @@ export default function GenericNode({
return names;
};
// const [openWDoubleCLick, setOpenWDoubleCLick] = useState(false);
const getBaseBorderClass = (selected) => {
let className = selected ? "border border-ring" : "border";
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
@ -231,10 +233,14 @@ export default function GenericNode({
}
};
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const memoizedNodeToolbarComponent = useMemo(() => {
return (
<NodeToolbar>
<NodeToolbarComponent
// openWDoubleClick={openWDoubleCLick}
// setOpenWDoubleClick={setOpenWDoubleCLick}
data={data}
deleteNode={(id) => {
takeSnapshot();
@ -266,11 +272,18 @@ export default function GenericNode({
updateNodeCode,
isOutdated,
selected,
shortcuts,
// openWDoubleCLick,
// setOpenWDoubleCLick,
]);
return (
<>
{memoizedNodeToolbarComponent}
<div
// onDoubleClick={(event) => {
// if (!isWrappedWithClass(event, "nodoubleclick"))
// setOpenWDoubleCLick(true);
// }}
className={getNodeBorderClassName(
selected,
showNode,
@ -344,7 +357,7 @@ export default function GenericNode({
event.preventDefault();
}}
data-testid={"title-" + data.node?.display_name}
className="generic-node-tooltip-div cursor-text text-primary"
className="nodoubleclick generic-node-tooltip-div cursor-text text-primary"
>
{data.node?.display_name}
</div>
@ -592,7 +605,7 @@ export default function GenericNode({
) : (
<div
className={cn(
"generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
"nodoubleclick generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable

View file

@ -1,40 +1,53 @@
import { Transition } from "@headlessui/react";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect, useMemo, useRef, useState } from "react";
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

@ -9,7 +9,7 @@ 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";
@ -20,8 +20,10 @@ import { cn } from "../../../../utils/utils";
import IconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
import { useShortcutsStore } from "../../../../stores/shortcuts";
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);
@ -113,7 +115,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
title: UPLOAD_ERROR_ALERT,
list: [error],
});
},
}
);
}}
>
@ -137,17 +139,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={() => {
@ -157,17 +161,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>
</DropdownMenuContent>
</DropdownMenu>
@ -195,7 +201,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
)}
/>
{printByBuildStatus()}

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

@ -33,10 +33,10 @@ 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
className,
)}
position={position}
{...props}
@ -45,7 +45,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
@ -75,7 +75,7 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-3 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}
>

View file

@ -734,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";
@ -741,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

@ -4,5 +4,5 @@ import SvgGroqLogo from "./GroqLogo";
export const GroqIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgGroqLogo ref={ref} {...props} />;
}
},
);

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

@ -6,7 +6,7 @@
*/
export default function getPythonCode(
flowName: string,
tweaksBuildedObject
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];

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

@ -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,
@ -55,6 +57,12 @@ const EditNodeModal = forwardRef(
}
}, [gridApi, open]);
// useEffect(() => {
// return () => {
// setOpenWDoubleClick(false);
// };
// }, []);
return (
<BaseModal
key={data.id}

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

@ -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,
@ -21,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";
@ -50,6 +59,7 @@ 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,
@ -141,119 +151,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,6 +178,115 @@ 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,
@ -473,6 +486,7 @@ export default function Page({
zoomOnScroll={!view}
zoomOnPinch={!view}
panOnDrag={!view}
panActivationKeyCode={""}
proOptions={{ hideAttribution: true }}
onPaneClick={onPaneClick}
>

View file

@ -1,8 +1,11 @@
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 IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent";
import {
Select,
@ -10,7 +13,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,6 +20,7 @@ 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";
@ -29,8 +32,9 @@ import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, cn } from "../../../../utils/utils";
import { classNames, cn, isThereModal } from "../../../../utils/utils";
import ToolbarSelectItem from "./toolbarSelectItem";
import RenderIcons from "../../../../components/renderIconComponent";
export default function NodeToolbarComponent({
data,
@ -43,6 +47,15 @@ export default function NodeToolbarComponent({
setShowState,
onCloseAdvancedModal,
}: 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,10 +71,127 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "dict" ||
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;
@ -73,18 +203,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");
@ -118,6 +244,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;
@ -185,6 +332,42 @@ export default function NodeToolbarComponent({
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 = (
@ -235,131 +418,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);
}}
@ -369,8 +442,28 @@ 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(
@ -387,24 +480,15 @@ 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",
@ -434,8 +518,23 @@ export default function NodeToolbarComponent({
</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
@ -453,23 +552,56 @@ 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"
@ -481,14 +613,11 @@ export default function NodeToolbarComponent({
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>
@ -496,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>
)}
@ -511,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"
@ -522,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
@ -561,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>
@ -597,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,49 @@
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} ${
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

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

@ -0,0 +1,183 @@
import { useEffect, useState } from "react";
import useAlertStore from "../../../../../stores/alertStore";
import ForwardedIconComponent from "../../../../../components/genericIconComponent";
import { Button } from "../../../../../components/ui/button";
import BaseModal from "../../../../../modals/baseModal";
import { useShortcutsStore } from "../../../../../stores/shortcuts";
import { toTitleCase } from "../../../../../utils/utils";
export default function EditShortcutButton({
children,
shortcut,
defaultShortcuts,
defaultCombination,
open,
setOpen,
disable,
setSelected,
}: {
children: JSX.Element;
shortcut: string[];
defaultShortcuts: Array<{ name: string; shortcut: string }>;
defaultCombination: string;
open: boolean;
setOpen: (bool: boolean) => void;
disable?: boolean;
setSelected: (selected: string[]) => void;
}): JSX.Element {
let shortcutInitialValue =
defaultShortcuts.length > 0
? defaultShortcuts.find(
(s) =>
s.name.split(" ")[0].toLowerCase().toLowerCase() ===
shortcut[0]?.split(" ")[0].toLowerCase(),
)?.shortcut
: "";
const [key, setKey] = useState<string | null>(null);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setShortcuts = useShortcutsStore((state) => state.setShortcuts);
const setErrorData = useAlertStore((state) => state.setErrorData);
function canEditCombination(newCombination: string): boolean {
let canSave = true;
defaultShortcuts.forEach(({ shortcut }) => {
if (shortcut.toLowerCase() === newCombination.toLowerCase()) {
canSave = false;
}
});
return canSave;
}
const setUniqueShortcut = useShortcutsStore(
(state) => state.updateUniqueShortcut,
);
function editCombination(): void {
if (key) {
if (canEditCombination(key)) {
const newCombination = defaultShortcuts.map((s) => {
if (s.name === shortcut[0]) {
return { name: s.name, shortcut: key };
}
return { name: s.name, shortcut: s.shortcut };
});
const fixCombination = key.split(" ");
if (
fixCombination[0].toLowerCase().includes("ctrl") ||
fixCombination[0].toLowerCase().includes("cmd")
) {
fixCombination[0] = "mod";
}
const shortcutName = shortcut[0].split(" ")[0].toLowerCase();
setUniqueShortcut(shortcutName, fixCombination.join("").toLowerCase());
setShortcuts(newCombination);
localStorage.setItem(
"langflow-shortcuts",
JSON.stringify(newCombination),
);
setKey(null);
setOpen(false);
setSuccessData({
title: `${shortcut[0]} shortcut successfully changed`,
});
return;
}
}
setErrorData({
title: "Error saving key combination",
list: ["This combination already exists!"],
});
}
useEffect(() => {
if (!open) {
setKey(null);
setSelected([]);
}
}, [open, setOpen, key]);
function getFixedCombination({
oldKey,
key,
}: {
oldKey: string;
key: string;
}): string {
if (oldKey === null) {
return `${key.length > 0 ? toTitleCase(key) : toTitleCase(key)}`;
}
return `${oldKey.length > 0 ? toTitleCase(oldKey) : oldKey.toUpperCase()} + ${key.length > 0 ? toTitleCase(key) : key.toUpperCase()}`;
}
function checkForKeys(keys: string, keyToCompare: string): boolean {
const keysArr = keys.split(" ");
let hasNewKey = false;
return keysArr.some(
(k) => k.toLowerCase().trim() === keyToCompare.toLowerCase().trim(),
);
}
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
e.preventDefault();
let fixedKey = e.key;
if (e.key?.toLowerCase() === "control") {
fixedKey = "Ctrl";
}
if (e.key?.toLowerCase() === "meta") {
fixedKey = "Cmd";
}
if (e.key?.toLowerCase() === " ") {
fixedKey = "Space";
}
if (key) {
if (checkForKeys(key, fixedKey)) return;
}
setKey((oldKey) =>
getFixedCombination({ oldKey: oldKey!, key: fixedKey }),
);
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [key, setKey]);
return (
<BaseModal open={open} setOpen={setOpen} size="x-small" disable={disable}>
<BaseModal.Header description={"Recording your keyboard"}>
<span className="pr-2"> Key Combination </span>
<ForwardedIconComponent
name="Keyboard"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Content>
<div className="align-center flex h-full w-full justify-center gap-4 rounded-md border border-border py-2">
<div className="flex items-center justify-center text-center text-lg font-bold">
{key === null
? shortcutInitialValue?.toUpperCase()
: key.toUpperCase()}
</div>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button variant={"default"} onClick={editCombination}>
Apply
</Button>
<Button
className="mr-5"
variant={"destructive"}
onClick={() => setKey(null)}
>
Reset
</Button>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -1,20 +1,16 @@
import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community";
import { useEffect, useState } from "react";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import TableComponent from "../../../../components/tableComponent";
import { Button } from "../../../../components/ui/button";
import { defaultShortcuts } from "../../../../constants/constants";
import { useShortcutsStore } from "../../../../stores/shortcuts";
import EditShortcutButton from "./EditShortcutButton";
export default function ShortcutsPage() {
const isMac = navigator.userAgent.toUpperCase().includes("MAC");
const advancedShortcut = `${isMac ? "Cmd" : "Ctrl"} + Shift + A`;
const minizmizeShortcut = `${isMac ? "Cmd" : "Ctrl"} + Shift + Q`;
const codeShortcut = `${isMac ? "Cmd" : "Ctrl"} + Shift + C`;
const copyShortcut = `${isMac ? "Cmd" : "Ctrl"} + C`;
const duplicateShortcut = `${isMac ? "Cmd" : "Ctrl"} + D`;
const shareShortcut = `${isMac ? "Cmd" : "Ctrl"} + Shift + S`;
const docsShortcut = `${isMac ? "Cmd" : "Ctrl"} + Shift + D`;
const saveShortcut = `${isMac ? "Cmd" : "Ctrl"} + S`;
const deleteShortcut = `Backspace`;
const interactionShortcut = `${isMac ? "Cmd" : "Ctrl"} + K`;
const undoShortcut = `${isMac ? "Cmd" : "Ctrl"} + Z`;
const redoShortcut = `${isMac ? "Cmd" : "Ctrl"} + Y`;
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const setShortcuts = useShortcutsStore((state) => state.setShortcuts);
// Column Definitions: Defines the columns to be displayed.
const colDefs = [
@ -23,8 +19,10 @@ export default function ShortcutsPage() {
field: "name",
flex: 1,
editable: false,
resizable: false,
}, //This column will be twice as wide as the others
{
headerName: "Keyboard Shortcut",
field: "shortcut",
flex: 2,
editable: false,
@ -32,71 +30,31 @@ export default function ShortcutsPage() {
},
];
const nodesRowData = [
{
name: "Advanced Settings Component",
shortcut: advancedShortcut,
resizable: false,
},
{
name: "Minimize Component",
shortcut: minizmizeShortcut,
resizable: false,
},
{
name: "Code Component",
shortcut: codeShortcut,
resizable: false,
},
{
name: "Copy Component",
shortcut: copyShortcut,
resizable: false,
},
{
name: "Duplicate Component",
shortcut: duplicateShortcut,
resizable: false,
},
{
name: "Share Component",
shortcut: shareShortcut,
resizable: false,
},
{
name: "Docs Component",
shortcut: docsShortcut,
resizable: false,
},
{
name: "Save Component",
shortcut: saveShortcut,
resizable: false,
},
{
name: "Delete Component",
shortcut: deleteShortcut,
resizable: false,
},
{
name: "Open Playground",
shortcut: interactionShortcut,
resizable: false,
},
{
name: "Undo",
shortcut: undoShortcut,
resizable: false,
},
{
name: "Redo",
shortcut: redoShortcut,
resizable: false,
},
];
const [nodesRowData, setNodesRowData] = useState<
Array<{ name: string; shortcut: string }>
>([]);
useEffect(() => {
setNodesRowData(shortcuts);
}, [shortcuts]);
const combinationToEdit = shortcuts.filter((s) => s.name === selectedRows[0]);
const [open, setOpen] = useState(false);
const updateUniqueShortcut = useShortcutsStore(
(state) => state.updateUniqueShortcut,
);
function handleRestore() {
setShortcuts(defaultShortcuts);
defaultShortcuts.forEach(({ name, shortcut }) => {
const fixedName = name.split(" ")[0].toLowerCase();
updateUniqueShortcut(fixedName, shortcut);
});
localStorage.removeItem("langflow-shortcuts");
}
return (
<div className="flex h-full w-full flex-col gap-6">
<div className="flex h-full w-full flex-col gap-6 ">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
@ -110,14 +68,48 @@ export default function ShortcutsPage() {
Manage Shortcuts for quick access to frequently used actions.
</p>
</div>
<div>
<div className="align-end mb-4 flex w-full justify-end">
<div className="justify center flex items-center">
{open && (
<EditShortcutButton
disable={selectedRows.length === 0}
defaultCombination={combinationToEdit[0]?.shortcut}
shortcut={selectedRows}
defaultShortcuts={shortcuts}
open={open}
setOpen={setOpen}
setSelected={setSelectedRows}
>
<div style={{ display: "none" }} />
</EditShortcutButton>
)}
<Button
variant="primary"
className="ml-3"
onClick={handleRestore}
>
<ForwardedIconComponent name="RotateCcw" className="mr-2 w-4" />
Restore
</Button>
</div>
</div>
</div>
</div>
<div className="flex h-full w-full flex-col justify-between">
<TableComponent
key={"shortcuts"}
pagination={false}
columnDefs={colDefs}
rowData={nodesRowData}
/>
<div className="grid gap-6 pb-8">
<div>
<TableComponent
suppressRowClickSelection={true}
domLayout="autoHeight"
pagination={false}
columnDefs={colDefs}
rowData={nodesRowData}
onCellDoubleClicked={(e) => {
setSelectedRows([e.data.name]);
setOpen(true);
}}
/>
</div>
</div>
</div>
);

View file

@ -1,15 +1,50 @@
import { create } from "zustand";
import { defaultShortcuts } from "../constants/constants";
import { shortcutsStoreType } from "../types/store";
export const useShortcutsStore = create<shortcutsStoreType>((set, get) => ({
openCodeModalWShortcut: false,
handleModalWShortcut: (modal) => {
switch (modal) {
case "code":
shortcuts: defaultShortcuts,
setShortcuts: (newShortcuts) => {
set({ shortcuts: newShortcuts });
},
flow: "mod+b",
undo: "mod+z",
redo: "mod+y",
open: "mod+k",
advanced: "mod+shift+a",
minimize: "mod+shift+q",
code: "space",
copy: "mod+c",
duplicate: "mod+d",
share: "mod+shift+s",
docs: "mod+shift+d",
save: "mod+s",
delete: "backspace",
group: "mod+g",
cut: "mod+x",
paste: "mod+v",
api: "mod+r",
update: "mod+u",
download: "mod+j",
freeze: "mod+f",
updateUniqueShortcut: (name, combination) => {
set({
[name]: combination,
});
},
getShortcutsFromStorage: () => {
if (localStorage.getItem("langflow-shortcuts")) {
const savedShortcuts = localStorage.getItem("langflow-shortcuts");
const savedArr = JSON.parse(savedShortcuts!);
savedArr.forEach(({ name, shortcut }) => {
let shortcutName = name.split(" ")[0].toLowerCase();
set({
openCodeModalWShortcut: !get().openCodeModalWShortcut,
[shortcutName]: shortcut,
});
break;
});
get().setShortcuts(JSON.parse(savedShortcuts!));
}
},
}));
useShortcutsStore.getState().getShortcutsFromStorage();

View file

@ -9,7 +9,9 @@
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
}

View file

@ -19,6 +19,10 @@ pre {
display: none;
}
.ag-row {
cursor: pointer;
}
.react-flow__pane {
cursor: default;
}

View file

@ -22,7 +22,7 @@
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
--destructive: 0 60% 40%; /* hsl(0 60% 40%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
@ -97,7 +97,7 @@
--accent: 216 24% 20%; /* hsl(216 34% 17%) */
--accent-foreground: 210 30% 98%; /* hsl(210 40% 98%) */
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
--destructive: 0 60% 25%; /* hsl(0 60% 25%) */
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
--ring: 216 24% 30%; /* hsl(216 24% 30%) */

View file

@ -491,7 +491,7 @@ export type ChatInputType = {
isDragging: boolean;
files: FilePreviewType[];
setFiles: (
files: FilePreviewType[] | ((prev: FilePreviewType[]) => FilePreviewType[]),
files: FilePreviewType[] | ((prev: FilePreviewType[]) => FilePreviewType[])
) => void;
chatValue: string;
inputRef: {
@ -535,6 +535,8 @@ export type fileCardPropsType = {
};
export type nodeToolbarPropsType = {
// openWDoubleClick: boolean;
// setOpenWDoubleClick: (open: boolean) => void;
data: NodeDataType;
deleteNode: (idx: string) => void;
setShowNode: (boolean: any) => void;
@ -591,7 +593,7 @@ export type chatMessagePropsType = {
updateChat: (
chat: ChatMessageType,
message: string,
stream_url?: string,
stream_url?: string
) => void;
};
@ -683,12 +685,12 @@ export type codeTabsPropsType = {
value: string,
node: NodeType,
template: TemplateVariableType,
tweak: tweakType,
tweak: tweakType
) => string;
buildTweakObject?: (
tw: string,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType,
template: TemplateVariableType
) => Promise<string | void>;
};
activeTweaks?: boolean;
@ -770,21 +772,12 @@ export type IOFileInputProps = {
};
export type toolbarSelectItemProps = {
isMac: boolean;
shift: boolean;
keyboardKey: string;
value: string;
icon: string;
styleObj?: {
iconClasses?: string;
commandClasses?: string;
shiftClasses?: string;
ctrlClasses?: string;
keyClasses?: string;
valueClasses?: string;
};
style?: string;
dataTestId: string;
ping?: boolean;
shortcut: string;
};
export type clearChatPropsType = {

View file

@ -20,6 +20,33 @@ export type StoreComponentResponse = {
};
export type shortcutsStoreType = {
openCodeModalWShortcut: boolean;
handleModalWShortcut: (str: string) => void;
updateUniqueShortcut: (name: string, combination: string) => void;
flow: string;
group: string;
cut: string;
paste: string;
api: string;
open: string;
undo: string;
redo: string;
advanced: string;
minimize: string;
code: string;
copy: string;
duplicate: string;
share: string;
docs: string;
save: string;
delete: string;
update: string;
download: string;
freeze: string;
shortcuts: Array<{
name: string;
shortcut: string;
}>;
setShortcuts: (
newShortcuts: Array<{ name: string; shortcut: string }>,
) => void;
getShortcutsFromStorage: () => void;
};

View file

@ -11,6 +11,7 @@ import ShortUniqueId from "short-unique-id";
import getFieldTitle from "../CustomNodes/utils/get-field-title";
import {
INPUT_TYPES,
IS_MAC,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_TYPES,
SUCCESS_BUILD,
@ -99,18 +100,18 @@ export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
export function isValidConnection(
{ source, target, sourceHandle, targetHandle }: Connection,
nodes: Node[],
edges: Edge[],
edges: Edge[]
) {
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
if (
targetHandleObject.inputTypes?.some(
(n) => n === sourceHandleObject.dataType,
(n) => n === sourceHandleObject.dataType
) ||
sourceHandleObject.baseClasses.some(
(t) =>
targetHandleObject.inputTypes?.some((n) => n === t) ||
t === targetHandleObject.type,
t === targetHandleObject.type
)
) {
let targetNode = nodes.find((node) => node.id === target!)?.data?.node;
@ -143,7 +144,7 @@ export function removeApiKeys(flow: FlowType): FlowType {
export function updateTemplate(
reference: APITemplateType,
objectToUpdate: APITemplateType,
objectToUpdate: APITemplateType
): APITemplateType {
let clonedObject: APITemplateType = cloneDeep(reference);
@ -203,7 +204,7 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
export function updateIds(
{ edges, nodes }: { edges: Edge[]; nodes: Node[] },
selection?: { edges: Edge[]; nodes: Node[] },
selection?: { edges: Edge[]; nodes: Node[] }
) {
let idsMap = {};
const selectionIds = selection?.nodes.map((n) => n.id);
@ -231,7 +232,7 @@ export function updateIds(
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!,
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
@ -241,7 +242,7 @@ export function updateIds(
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!,
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
@ -287,11 +288,11 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
t &&
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
node.id,
node.id
)
) {
errors.push(
`${displayName || type} is missing ${getFieldTitle(template, t)}.`,
`${displayName || type} is missing ${getFieldTitle(template, t)}.`
);
} else if (
template[t].type === "dict" &&
@ -305,15 +306,15 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
errors.push(
`${displayName || type} (${getFieldTitle(
template,
t,
)}) contains duplicate keys with the same values.`,
t
)}) contains duplicate keys with the same values.`
);
if (hasEmptyKey(template[t].value))
errors.push(
`${displayName || type} (${getFieldTitle(
template,
t,
)}) field must not be empty.`,
t
)}) field must not be empty.`
);
}
return errors;
@ -322,7 +323,7 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
export function validateNodes(
nodes: Node[],
edges: Edge[],
edges: Edge[]
): // this returns an array of tuples with the node id and the errors
Array<{ id: string; errors: Array<string> }> {
if (nodes.length === 0) {
@ -343,7 +344,7 @@ export function updateEdges(edges: Edge[]) {
if (edges)
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!,
edge.targetHandle!
);
edge.className = "";
});
@ -410,7 +411,7 @@ export function handleKeyDown(
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>,
inputValue: string | string[] | null,
block: string,
block: string
) {
//condition to fix bug control+backspace on Windows/Linux
if (
@ -420,9 +421,7 @@ export function handleKeyDown(
(inputValue === block ||
inputValue?.charAt(inputValue?.length - 1) === " " ||
specialCharsRegex.test(inputValue?.charAt(inputValue?.length - 1)))) ||
(navigator.userAgent.toUpperCase().includes("MAC") &&
e.ctrlKey === true &&
e.key === "Backspace")
(IS_MAC && e.ctrlKey === true && e.key === "Backspace")
) {
e.preventDefault();
e.stopPropagation();
@ -435,7 +434,7 @@ export function handleKeyDown(
}
export function handleOnlyIntegerInput(
event: React.KeyboardEvent<HTMLInputElement>,
event: React.KeyboardEvent<HTMLInputElement>
) {
if (
event.key === "." ||
@ -451,7 +450,7 @@ export function handleOnlyIntegerInput(
export function getConnectedNodes(
edge: Edge,
nodes: Array<NodeType>,
nodes: Array<NodeType>
): Array<NodeType> {
const sourceId = edge.source;
const targetId = edge.target;
@ -552,7 +551,7 @@ export function checkOldEdgesHandles(edges: Edge[]): boolean {
!edge.sourceHandle ||
!edge.targetHandle ||
!edge.sourceHandle.includes("{") ||
!edge.targetHandle.includes("{"),
!edge.targetHandle.includes("{")
);
}
@ -575,7 +574,7 @@ export function customStringify(obj: any): string {
const keys = Object.keys(obj).sort();
const keyValuePairs = keys.map(
(key) => `"${key}":${customStringify(obj[key])}`,
(key) => `"${key}":${customStringify(obj[key])}`
);
return `{${keyValuePairs.join(",")}}`;
}
@ -604,7 +603,7 @@ export function getHandleId(
source: string,
sourceHandle: string,
target: string,
targetHandle: string,
targetHandle: string
) {
return (
"reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle
@ -615,7 +614,7 @@ export function generateFlow(
selection: OnSelectionChangeParams,
nodes: Node[],
edges: Edge[],
name: string,
name: string
): generateFlowType {
const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
const uid = new ShortUniqueId({ length: 5 });
@ -624,7 +623,7 @@ export function generateFlow(
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
selection.nodes.some((node) => node.id === edge.source),
selection.nodes.some((node) => node.id === edge.source)
);
newFlowData.nodes = selection.nodes;
@ -645,7 +644,7 @@ export function generateFlow(
(edge) =>
(selection.nodes.some((node) => node.id === edge.target) ||
selection.nodes.some((node) => node.id === edge.source)) &&
newFlowData.edges.every((e) => e.id !== edge.id),
newFlowData.edges.every((e) => e.id !== edge.id)
),
};
}
@ -656,13 +655,13 @@ export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
const { nodes, edges } = groupNode.data.node!.flow!.data!;
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
newEdges = newEdges.filter(
(e) => !(nodes.some((n) => n.id === e.source) && e.source !== lastNode?.id),
(e) => !(nodes.some((n) => n.id === e.source) && e.source !== lastNode?.id)
);
newEdges.forEach((edge) => {
if (lastNode && edge.source === lastNode.id) {
edge.source = groupNode.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
edge.sourceHandle!,
edge.sourceHandle!
);
newSourceHandle.id = groupNode.id;
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
@ -689,7 +688,7 @@ export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
export function filterFlow(
selection: OnSelectionChangeParams,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
setNodes((nodes) => nodes.filter((node) => !selection.nodes.includes(node)));
setEdges((edges) => edges.filter((edge) => !selection.edges.includes(edge)));
@ -727,7 +726,7 @@ export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
export function concatFlows(
flow: FlowType,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
const { nodes, edges } = flow.data!;
setNodes((old) => [...old, ...nodes]);
@ -736,7 +735,7 @@ export function concatFlows(
export function validateSelection(
selection: OnSelectionChangeParams,
edges: Edge[],
edges: Edge[]
): Array<string> {
const clonedSelection = cloneDeep(selection);
const clonedEdges = cloneDeep(edges);
@ -750,7 +749,7 @@ export function validateSelection(
let nodesSet = new Set(clonedSelection.nodes.map((n) => n.id));
// then filter the edges that are connected to the nodes in the set
let connectedEdges = clonedSelection.edges.filter(
(e) => nodesSet.has(e.source) && nodesSet.has(e.target),
(e) => nodesSet.has(e.source) && nodesSet.has(e.target)
);
// add the edges to the selection
clonedSelection.edges = connectedEdges;
@ -764,17 +763,17 @@ export function validateSelection(
clonedSelection.nodes.some(
(node) =>
isInputNode(node.data as NodeDataType) ||
isOutputNode(node.data as NodeDataType),
isOutputNode(node.data as NodeDataType)
)
) {
errorsArray.push(
"Please select only nodes that are not input or output nodes",
"Please select only nodes that are not input or output nodes"
);
}
//check if there are two or more nodes with free outputs
if (
clonedSelection.nodes.filter(
(n) => !clonedSelection.edges.some((e) => e.source === n.id),
(n) => !clonedSelection.edges.some((e) => e.source === n.id)
).length > 1
) {
errorsArray.push("Please select only one node with free outputs");
@ -785,7 +784,7 @@ export function validateSelection(
clonedSelection.nodes.some(
(node) =>
!clonedSelection.edges.some((edge) => edge.target === node.id) &&
!clonedSelection.edges.some((edge) => edge.source === node.id),
!clonedSelection.edges.some((edge) => edge.source === node.id)
)
) {
errorsArray.push("Please select only nodes that are connected");
@ -842,8 +841,8 @@ export function mergeNodeTemplates({
nodeTemplate[key].display_name
? nodeTemplate[key].display_name
: nodeTemplate[key].name
? toTitleCase(nodeTemplate[key].name)
: toTitleCase(key);
? toTitleCase(nodeTemplate[key].name)
: toTitleCase(key);
}
}
});
@ -854,7 +853,7 @@ function isHandleConnected(
edges: Edge[],
key: string,
field: TemplateVariableType,
nodeId: string,
nodeId: string
) {
/*
this function receives a flow and a handleId and check if there is a connection with this handle
@ -870,7 +869,7 @@ function isHandleConnected(
id: nodeId,
proxy: { id: field.proxy!.id, field: field.proxy!.field },
inputTypes: field.input_types,
} as targetHandleType),
} as targetHandleType)
)
) {
return true;
@ -885,7 +884,7 @@ function isHandleConnected(
fieldName: key,
id: nodeId,
inputTypes: field.input_types,
} as targetHandleType),
} as targetHandleType)
)
) {
return true;
@ -908,7 +907,7 @@ export function generateNodeTemplate(Flow: FlowType) {
export function generateNodeFromFlow(
flow: FlowType,
getNodeId: (type: string) => string,
getNodeId: (type: string) => string
): NodeType {
const { nodes } = flow.data!;
const outputNode = cloneDeep(findLastNode(flow.data!));
@ -939,7 +938,7 @@ export function generateNodeFromFlow(
export function connectedInputNodesOnHandle(
nodeId: string,
handleId: string,
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] },
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] }
) {
const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> =
[];
@ -976,7 +975,7 @@ export function connectedInputNodesOnHandle(
export function updateProxyIdsOnTemplate(
template: APITemplateType,
idsMap: { [key: string]: string },
idsMap: { [key: string]: string }
) {
Object.keys(template).forEach((key) => {
if (template[key].proxy && idsMap[template[key].proxy!.id]) {
@ -987,7 +986,7 @@ export function updateProxyIdsOnTemplate(
export function updateEdgesIds(
edges: Edge[],
idsMap: { [key: string]: string },
idsMap: { [key: string]: string }
) {
edges.forEach((edge) => {
let targetHandle: targetHandleType = edge.data.targetHandle;
@ -1014,7 +1013,7 @@ export function expandGroupNode(
nodes: Node[],
edges: Edge[],
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
const idsMap = updateIds(flow!.data!);
updateProxyIdsOnTemplate(template, idsMap);
@ -1057,7 +1056,7 @@ export function expandGroupNode(
const lastNode = cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!,
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
@ -1114,7 +1113,7 @@ export function expandGroupNode(
export function getGroupStatus(
flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } },
ssData: { [key: string]: { valid: boolean; params: string } }
) {
let status = { valid: true, params: SUCCESS_BUILD };
const { nodes } = flow.data!;
@ -1133,7 +1132,7 @@ export function getGroupStatus(
export function createFlowComponent(
nodeData: NodeDataType,
version: string,
version: string
): FlowType {
const flowNode: FlowType = {
data: {
@ -1169,7 +1168,7 @@ export function downloadNode(NodeFLow: FlowType) {
export function updateComponentNameAndType(
data: any,
component: NodeDataType,
component: NodeDataType
) {}
export function removeFileNameFromComponents(flow: FlowType) {
@ -1243,7 +1242,7 @@ export function extractFieldsFromComponenents(data: APIObjectType) {
export function downloadFlow(
flow: FlowType,
flowName: string,
flowDescription?: string,
flowDescription?: string
) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
@ -1253,7 +1252,7 @@ export function downloadFlow(
...clonedFlow,
name: flowName,
description: flowDescription,
}),
})
)}`;
// create a link element and set its properties
@ -1268,7 +1267,7 @@ export function downloadFlow(
export function downloadFlows() {
downloadFlowsFromDatabase().then((flows) => {
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flows),
JSON.stringify(flows)
)}`;
// create a link element and set its properties
@ -1292,7 +1291,7 @@ export function getRandomDescription(): string {
export const createNewFlow = (
flowData: ReactFlowJsonObject,
flow: FlowType,
folderId: string,
folderId: string
) => {
return {
description: flow?.description ?? getRandomDescription(),

View file

@ -149,6 +149,8 @@ import {
X,
XCircle,
Zap,
PlaySquare,
Wrench,
} from "lucide-react";
import { FaApple, FaDiscord, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS";
@ -533,11 +535,12 @@ export const nodeIconsLucide: iconsType = {
Command,
ArrowBigUp,
Dot,
RotateCcw,
Wrench,
FolderPlusIcon,
FolderIcon,
Discord: FaDiscord,
PaperclipIcon,
RotateCcw,
Settings,
Streamlit,
};

View file

@ -10,6 +10,7 @@ import {
import { NodeType } from "../types/flow";
import { FlowState } from "../types/tabs";
import TableAutoCellRender from "../components/tableComponent/components/tableAutoCellRender";
import { MODAL_CLASSES } from "../constants/constants";
export function classNames(...classes: Array<string>): string {
return classes.filter(Boolean).join(" ");
@ -406,3 +407,8 @@ export function extractColumnsFromRows(
return Object.values(columnsKeys);
}
export function isThereModal(): boolean {
const modal = document.body.getElementsByClassName(MODAL_CLASSES);
return modal.length > 0;
}