diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index f45b9973ce..a7ad34bfc7 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1627,7 +1627,7 @@
},
"workflows": {
"workflows": "Workflows",
- "workflowLibrary": "Workflow Library",
+ "workflowLibrary": "Library",
"userWorkflows": "My Workflows",
"defaultWorkflows": "Default Workflows",
"openWorkflow": "Open Workflow",
@@ -1637,6 +1637,7 @@
"downloadWorkflow": "Download Workflow",
"saveWorkflow": "Save Workflow",
"saveWorkflowAs": "Save Workflow As",
+ "savingWorkflow": "Saving Workflow...",
"problemSavingWorkflow": "Problem Saving Workflow",
"workflowSaved": "Workflow Saved",
"noRecentWorkflows": "No Recent Workflows",
@@ -1648,7 +1649,8 @@
"searchWorkflows": "Search Workflows",
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
"workflowName": "Workflow Name",
- "workflowEditorReset": "Workflow Editor Reset"
+ "workflowEditorReset": "Workflow Editor Reset",
+ "workflowEditorMenu": "Workflow Editor Menu"
},
"app": {
"storeNotInitialized": "Store is not initialized"
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index 3675dd9af9..8d3ad1a16f 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -7,12 +7,10 @@ import { MdDeviceHub } from 'react-icons/md';
import 'reactflow/dist/style.css';
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
import { Flow } from './flow/Flow';
-import TopLeftPanel from './flow/panels/TopLeftPanel/TopLeftPanel';
-import TopCenterPanel from './flow/panels/TopCenterPanel/TopCenterPanel';
-import TopRightPanel from './flow/panels/TopRightPanel/TopRightPanel';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
import { useTranslation } from 'react-i18next';
+import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
const NodeEditor = () => {
const isReady = useAppSelector((state) => state.nodes.isReady);
@@ -47,9 +45,7 @@ const NodeEditor = () => {
>
-
-
-
+
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
index 2f0695f03a..5ba0015c60 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
@@ -3,6 +3,25 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
+import {
+ connectionEnded,
+ connectionMade,
+ connectionStarted,
+ edgeAdded,
+ edgeChangeStarted,
+ edgeDeleted,
+ edgesChanged,
+ edgesDeleted,
+ nodesChanged,
+ nodesDeleted,
+ selectedAll,
+ selectedEdgesChanged,
+ selectedNodesChanged,
+ selectionCopied,
+ selectionPasted,
+ viewportChanged,
+} from 'features/nodes/store/nodesSlice';
import { $flow } from 'features/nodes/store/reactFlowInstance';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import { MouseEvent, useCallback, useRef } from 'react';
@@ -25,25 +44,6 @@ import {
ReactFlowProps,
XYPosition,
} from 'reactflow';
-import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
-import {
- connectionEnded,
- connectionMade,
- connectionStarted,
- edgeAdded,
- edgeChangeStarted,
- edgeDeleted,
- edgesChanged,
- edgesDeleted,
- nodesChanged,
- nodesDeleted,
- selectedAll,
- selectedEdgesChanged,
- selectedNodesChanged,
- selectionCopied,
- selectionPasted,
- viewportChanged,
-} from 'features/nodes/store/nodesSlice';
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx
index 4b515bcd0a..e6d366b0f3 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx
@@ -1,4 +1,4 @@
-import { Flex } from '@chakra-ui/layout';
+import { Flex, Heading } from '@chakra-ui/layout';
import { memo } from 'react';
import DownloadWorkflowButton from 'features/workflowLibrary/components/DownloadWorkflowButton';
import UploadWorkflowButton from 'features/workflowLibrary/components/LoadWorkflowFromFileButton';
@@ -6,10 +6,14 @@ import ResetWorkflowEditorButton from 'features/workflowLibrary/components/Reset
import SaveWorkflowButton from 'features/workflowLibrary/components/SaveWorkflowButton';
import SaveWorkflowAsButton from 'features/workflowLibrary/components/SaveWorkflowAsButton';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
+import { useAppSelector } from 'app/store/storeHooks';
const TopCenterPanel = () => {
const isWorkflowLibraryEnabled =
useFeatureStatus('workflowLibrary').isFeatureEnabled;
+ const name = useAppSelector(
+ (state) => state.workflow.name || 'Untitled Workflow'
+ );
return (
{
top: 2,
insetInlineStart: '50%',
transform: 'translate(-50%)',
+ alignItems: 'center',
}}
>
-
+
+ {name}
+
+ {/*
{isWorkflowLibraryEnabled && (
<>
@@ -29,7 +45,7 @@ const TopCenterPanel = () => {
>
)}
-
+ */}
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx
new file mode 100644
index 0000000000..266d711337
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx
@@ -0,0 +1,26 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import IAIIconButton from 'common/components/IAIIconButton';
+import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaPlus } from 'react-icons/fa';
+
+const AddNodeButton = () => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const handleOpenAddNodePopover = useCallback(() => {
+ dispatch(addNodePopoverOpened());
+ }, [dispatch]);
+
+ return (
+ }
+ onClick={handleOpenAddNodePopover}
+ pointerEvents="auto"
+ />
+ );
+};
+
+export default memo(AddNodeButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
new file mode 100644
index 0000000000..2e79ea9763
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
@@ -0,0 +1,37 @@
+import { Flex, Spacer } from '@chakra-ui/layout';
+import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
+import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
+import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName';
+import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
+import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
+import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
+import { memo } from 'react';
+
+const TopCenterPanel = () => {
+ const isWorkflowLibraryEnabled =
+ useFeatureStatus('workflowLibrary').isFeatureEnabled;
+
+ return (
+
+
+
+
+
+
+ {isWorkflowLibraryEnabled && }
+
+
+ );
+};
+
+export default memo(TopCenterPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx
new file mode 100644
index 0000000000..6d82cc5b9d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx
@@ -0,0 +1,32 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import IAIButton from 'common/components/IAIButton';
+import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
+import { updateAllNodesRequested } from 'features/nodes/store/actions';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaExclamationTriangle } from 'react-icons/fa';
+
+const UpdateNodesButton = () => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const nodesNeedUpdate = useGetNodesNeedUpdate();
+ const handleClickUpdateNodes = useCallback(() => {
+ dispatch(updateAllNodesRequested());
+ }, [dispatch]);
+
+ if (!nodesNeedUpdate) {
+ return null;
+ }
+
+ return (
+ }
+ onClick={handleClickUpdateNodes}
+ pointerEvents="auto"
+ >
+ {t('nodes.updateAllNodes')}
+
+ );
+};
+
+export default memo(UpdateNodesButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx
new file mode 100644
index 0000000000..58fc3bb5b8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx
@@ -0,0 +1,25 @@
+import { Text } from '@chakra-ui/layout';
+import { useAppSelector } from 'app/store/storeHooks';
+import { memo } from 'react';
+
+const TopCenterPanel = () => {
+ const name = useAppSelector(
+ (state) => state.workflow.name || 'Untitled Workflow'
+ );
+
+ return (
+
+ {name}
+
+ );
+};
+
+export default memo(TopCenterPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
index a06b4a7656..944ec15c42 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
@@ -1,8 +1,8 @@
import { Flex } from '@chakra-ui/react';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import { memo } from 'react';
-import WorkflowEditorSettings from './WorkflowEditorSettings';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
+import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
const TopRightPanel = () => {
const isWorkflowLibraryEnabled =
@@ -11,7 +11,7 @@ const TopRightPanel = () => {
return (
{isWorkflowLibraryEnabled && }
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
index 1ca671a222..66a55b20c7 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx
@@ -9,15 +9,14 @@ import {
ModalContent,
ModalHeader,
ModalOverlay,
- forwardRef,
useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
-import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch';
+import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton';
import {
selectionModeChanged,
shouldAnimateEdgesChanged,
@@ -25,11 +24,9 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/nodesSlice';
-import { ChangeEvent, memo, useCallback } from 'react';
-import { FaCog } from 'react-icons/fa';
-import { SelectionMode } from 'reactflow';
-import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton';
+import { ChangeEvent, ReactNode, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
+import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = {
fontWeight: 600,
@@ -56,7 +53,11 @@ const selector = createSelector(
defaultSelectorOptions
);
-const WorkflowEditorSettings = forwardRef((_, ref) => {
+type Props = {
+ children: (props: { onOpen: () => void }) => ReactNode;
+};
+
+const WorkflowEditorSettings = ({ children }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const {
@@ -106,13 +107,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
return (
<>
- }
- onClick={onOpen}
- />
+ {children({ onOpen })}
@@ -151,6 +146,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
label={t('nodes.colorCodeEdges')}
helperText={t('nodes.colorCodeEdgesHelp')}
/>
+
{
>
);
-});
+};
export default memo(WorkflowEditorSettings);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
index ee0a8314a0..2848aea49d 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
@@ -1,10 +1,10 @@
import { useDisclosure } from '@chakra-ui/react';
-import IAIIconButton from 'common/components/IAIIconButton';
+import IAIButton from 'common/components/IAIButton';
+import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFolderOpen } from 'react-icons/fa';
import WorkflowLibraryModal from './WorkflowLibraryModal';
-import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
const WorkflowLibraryButton = () => {
const { t } = useTranslation();
@@ -12,12 +12,13 @@ const WorkflowLibraryButton = () => {
return (
- }
+ }
onClick={disclosure.onOpen}
- tooltip={t('workflows.workflowLibrary')}
- aria-label={t('workflows.workflowLibrary')}
- />
+ pointerEvents="auto"
+ >
+ {t('workflows.workflowLibrary')}
+
);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx
new file mode 100644
index 0000000000..1a04b11594
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx
@@ -0,0 +1,18 @@
+import { MenuItem } from '@chakra-ui/react';
+import { useDownloadWorkflow } from 'features/nodes/hooks/useDownloadWorkflow';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaDownload } from 'react-icons/fa';
+
+const DownloadWorkflowMenuItem = () => {
+ const { t } = useTranslation();
+ const downloadWorkflow = useDownloadWorkflow();
+
+ return (
+ } onClick={downloadWorkflow}>
+ {t('workflows.downloadWorkflow')}
+
+ );
+};
+
+export default memo(DownloadWorkflowMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx
new file mode 100644
index 0000000000..5d7e58e198
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx
@@ -0,0 +1,88 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Button,
+ Flex,
+ MenuItem,
+ Text,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
+import { addToast } from 'features/system/store/systemSlice';
+import { makeToast } from 'features/system/util/makeToast';
+import { memo, useCallback, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaTrash } from 'react-icons/fa';
+
+const ResetWorkflowEditorMenuItem = () => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const cancelRef = useRef(null);
+
+ const handleConfirmClear = useCallback(() => {
+ dispatch(nodeEditorReset());
+
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('workflows.workflowEditorReset'),
+ status: 'success',
+ })
+ )
+ );
+
+ onClose();
+ }, [dispatch, t, onClose]);
+
+ return (
+ <>
+ }
+ sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
+ onClick={onOpen}
+ >
+ {t('nodes.resetWorkflow')}
+
+
+
+
+
+
+
+ {t('nodes.resetWorkflow')}
+
+
+
+
+ {t('nodes.resetWorkflowDesc')}
+ {t('nodes.resetWorkflowDesc2')}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default memo(ResetWorkflowEditorMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx
new file mode 100644
index 0000000000..dbcf7edd9d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx
@@ -0,0 +1,85 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ FormControl,
+ FormLabel,
+ Input,
+ MenuItem,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import IAIButton from 'common/components/IAIButton';
+import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useSaveWorkflowAs';
+import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName';
+import { ChangeEvent, memo, useCallback, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaClone } from 'react-icons/fa';
+
+const SaveWorkflowAsButton = () => {
+ const currentName = useAppSelector((state) => state.workflow.name);
+ const { t } = useTranslation();
+ const { saveWorkflowAs } = useSaveWorkflowAs();
+ const [name, setName] = useState(getWorkflowCopyName(currentName));
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const inputRef = useRef(null);
+
+ const onOpenCallback = useCallback(() => {
+ setName(getWorkflowCopyName(currentName));
+ onOpen();
+ }, [currentName, onOpen]);
+
+ const onSave = useCallback(async () => {
+ saveWorkflowAs({ name, onSuccess: onClose, onError: onClose });
+ }, [name, onClose, saveWorkflowAs]);
+
+ const onChange = useCallback((e: ChangeEvent) => {
+ setName(e.target.value);
+ }, []);
+
+ return (
+ <>
+ } onClick={onOpenCallback}>
+ {t('workflows.saveWorkflowAs')}
+
+
+
+
+
+ {t('workflows.saveWorkflowAs')}
+
+
+
+
+ {t('workflows.workflowName')}
+
+
+
+
+
+ {t('common.cancel')}
+
+ {t('common.saveAs')}
+
+
+
+
+
+ >
+ );
+};
+
+export default memo(SaveWorkflowAsButton);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
new file mode 100644
index 0000000000..15ca98b69e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
@@ -0,0 +1,17 @@
+import { MenuItem } from '@chakra-ui/react';
+import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaSave } from 'react-icons/fa';
+
+const SaveLibraryWorkflowMenuItem = () => {
+ const { t } = useTranslation();
+ const { saveWorkflow } = useSaveLibraryWorkflow();
+ return (
+ } onClick={saveWorkflow}>
+ {t('workflows.saveWorkflow')}
+
+ );
+};
+
+export default memo(SaveLibraryWorkflowMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx
new file mode 100644
index 0000000000..0a9bbdeb2c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx
@@ -0,0 +1,21 @@
+import { MenuItem } from '@chakra-ui/react';
+import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaCog } from 'react-icons/fa';
+
+const DownloadWorkflowMenuItem = () => {
+ const { t } = useTranslation();
+
+ return (
+
+ {({ onOpen }) => (
+ } onClick={onOpen}>
+ {t('nodes.workflowSettings')}
+
+ )}
+
+ );
+};
+
+export default memo(DownloadWorkflowMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx
new file mode 100644
index 0000000000..d217a652bc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx
@@ -0,0 +1,27 @@
+import { MenuItem } from '@chakra-ui/react';
+import { FileButton } from '@mantine/core';
+import { useLoadWorkflowFromFile } from 'features/workflowLibrary/hooks/useLoadWorkflowFromFile';
+import { memo, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaUpload } from 'react-icons/fa';
+
+const UploadWorkflowMenuItem = () => {
+ const { t } = useTranslation();
+ const resetRef = useRef<() => void>(null);
+ const loadWorkflowFromFile = useLoadWorkflowFromFile({ resetRef });
+ return (
+
+ {(props) => (
+ } {...props}>
+ {t('workflows.uploadWorkflow')}
+
+ )}
+
+ );
+};
+
+export default memo(UploadWorkflowMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx
new file mode 100644
index 0000000000..a19b9cd6b5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx
@@ -0,0 +1,50 @@
+import {
+ Menu,
+ MenuButton,
+ MenuDivider,
+ MenuList,
+ useDisclosure,
+} from '@chakra-ui/react';
+import IAIIconButton from 'common/components/IAIIconButton';
+import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
+import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
+import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
+import ResetWorkflowEditorMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem';
+import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
+import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
+import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
+import UploadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaEllipsis } from 'react-icons/fa6';
+import { menuListMotionProps } from 'theme/components/menu';
+
+const WorkflowLibraryMenu = () => {
+ const { t } = useTranslation();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ useGlobalMenuCloseTrigger(onClose);
+ const isWorkflowLibraryEnabled =
+ useFeatureStatus('workflowLibrary').isFeatureEnabled;
+
+ return (
+
+ );
+};
+
+export default memo(WorkflowLibraryMenu);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts
new file mode 100644
index 0000000000..8b0a640ef9
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts
@@ -0,0 +1,17 @@
+import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
+import { useCallback } from 'react';
+
+export const useDownloadWorkflow = () => {
+ const workflow = useWorkflow();
+ const downloadWorkflow = useCallback(() => {
+ const blob = new Blob([JSON.stringify(workflow, null, 2)]);
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = `${workflow.name || 'My Workflow'}.json`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ }, [workflow]);
+
+ return downloadWorkflow;
+};
diff --git a/invokeai/frontend/web/src/theme/components/menu.ts b/invokeai/frontend/web/src/theme/components/menu.ts
index 4ab323bdb5..04a456daa1 100644
--- a/invokeai/frontend/web/src/theme/components/menu.ts
+++ b/invokeai/frontend/web/src/theme/components/menu.ts
@@ -45,6 +45,9 @@ const invokeAI = definePartsStyle((props) => ({
fontSize: 14,
},
},
+ divider: {
+ borderColor: mode('base.400', 'base.700')(props),
+ },
}));
export const menuTheme = defineMultiStyleConfig({