diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index f4bfa282fa..7d426efbb6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -718,12 +718,12 @@
"swapSizes": "Swap Sizes"
},
"nodes": {
- "reloadSchema": "Reload Node Templates",
- "saveGraph": "Save Workflow",
- "loadGraph": "Load Workflow",
- "resetGraph": "Reset Workflow",
- "clearGraph": "Reset Graph",
- "clearGraphDesc": "Are you sure you want to clear all nodes?",
+ "reloadNodeTemplates": "Reload Node Templates",
+ "saveWorkflow": "Save Workflow",
+ "loadWorkflow": "Load Workflow",
+ "resetWorkflow": "Reset Workflow",
+ "resetWorkflowDesc": "Are you sure you want to reset this workflow?",
+ "resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out",
"fitViewportNodes": "Fit View",
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
index 1b48b3b48a..885a2c6e87 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx
@@ -24,34 +24,40 @@ type NodeWrapperProps = PropsWithChildren & {
const NodeWrapper = (props: NodeWrapperProps) => {
const { nodeId, width, children, selected } = props;
- const selectNodeExecutionState = useMemo(
+ const selectIsInProgress = useMemo(
() =>
createSelector(
stateSelector,
- ({ nodes }) => nodes.nodeExecutionStates[nodeId]
+ ({ nodes }) =>
+ nodes.nodeExecutionStates[nodeId]?.status === NodeStatus.IN_PROGRESS
),
[nodeId]
);
- const nodeExecutionState = useAppSelector(selectNodeExecutionState);
+ const isInProgress = useAppSelector(selectIsInProgress);
const [
- nodeSelectedOutlineLight,
- nodeSelectedOutlineDark,
+ nodeSelectedLight,
+ nodeSelectedDark,
+ nodeInProgressLight,
+ nodeInProgressDark,
shadowsXl,
shadowsBase,
] = useToken('shadows', [
- 'nodeSelectedOutline.light',
- 'nodeSelectedOutline.dark',
+ 'nodeSelected.light',
+ 'nodeSelected.dark',
+ 'nodeInProgress.light',
+ 'nodeInProgress.dark',
'shadows.xl',
'shadows.base',
]);
const dispatch = useAppDispatch();
- const shadow = useColorModeValue(
- nodeSelectedOutlineLight,
- nodeSelectedOutlineDark
+ const selectedShadow = useColorModeValue(nodeSelectedLight, nodeSelectedDark);
+ const inProgressShadow = useColorModeValue(
+ nodeInProgressLight,
+ nodeInProgressDark
);
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
@@ -71,24 +77,9 @@ const NodeWrapper = (props: NodeWrapperProps) => {
w: width ?? NODE_WIDTH,
transitionProperty: 'common',
transitionDuration: '0.1s',
- shadow: selected
- ? nodeExecutionState?.status === NodeStatus.IN_PROGRESS
- ? undefined
- : shadow
- : undefined,
+ shadow: selected ? selectedShadow : undefined,
cursor: 'grab',
opacity,
- borderWidth: 2,
- borderColor:
- nodeExecutionState?.status === NodeStatus.IN_PROGRESS
- ? 'warning.300'
- : 'base.200',
- _dark: {
- borderColor:
- nodeExecutionState?.status === NodeStatus.IN_PROGRESS
- ? 'warning.500'
- : 'base.900',
- },
}}
>
{
zIndex: -1,
}}
/>
+
{children}
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
index 260655723e..15d8d58d7b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx
@@ -1,20 +1,19 @@
-import { ButtonGroup, Tooltip } from '@chakra-ui/react';
+import { ButtonGroup } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
+import {
+ // shouldShowFieldTypeLegendChanged,
+ shouldShowMinimapPanelChanged,
+} from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
FaExpand,
// FaInfo,
FaMapMarkerAlt,
- FaMinus,
- FaPlus,
} from 'react-icons/fa';
+import { FaMagnifyingGlassMinus, FaMagnifyingGlassPlus } from 'react-icons/fa6';
import { useReactFlow } from 'reactflow';
-import {
- // shouldShowFieldTypeLegendChanged,
- shouldShowMinimapPanelChanged,
-} from 'features/nodes/store/nodesSlice';
const ViewportControls = () => {
const { t } = useTranslation();
@@ -49,27 +48,24 @@ const ViewportControls = () => {
return (
-
- }
- />
-
-
- }
- />
-
-
- }
- />
-
+ }
+ />
+ }
+ />
+ }
+ />
{/* {
icon={}
/>
*/}
-
- }
- />
-
+ aria-label={
+ shouldShowMinimapPanel
+ ? t('nodes.hideMinimapnodes')
+ : t('nodes.showMinimapnodes')
+ }
+ isChecked={shouldShowMinimapPanel}
+ onClick={handleClickedToggleMiniMapPanel}
+ icon={}
+ />
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx
index 6a413cbf04..8454f5539f 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx
@@ -2,9 +2,11 @@ import { FileButton } from '@mantine/core';
import IAIIconButton from 'common/components/IAIIconButton';
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
import { memo, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
const LoadWorkflowButton = () => {
+ const { t } = useTranslation();
const resetRef = useRef<() => void>(null);
const loadWorkflowFromFile = useLoadWorkflowFromFile();
return (
@@ -16,8 +18,8 @@ const LoadWorkflowButton = () => {
{(props) => (
}
- tooltip="Load Workflow"
- aria-label="Load Workflow"
+ tooltip={t('nodes.loadWorkflow')}
+ aria-label={t('nodes.loadWorkflow')}
{...props}
/>
)}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx
index ea51a57edd..905b0b74a2 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { FaSyncAlt } from 'react-icons/fa';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
-const ReloadSchemaButton = () => {
+const ReloadNodeTemplatesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -16,13 +16,13 @@ const ReloadSchemaButton = () => {
return (
}
- tooltip={t('nodes.reloadSchema')}
- aria-label={t('nodes.reloadSchema')}
+ tooltip={t('nodes.reloadNodeTemplates')}
+ aria-label={t('nodes.reloadNodeTemplates')}
onClick={handleReloadSchema}
>
- {t('nodes.reloadSchema')}
+ {t('nodes.reloadNodeTemplates')}
);
};
-export default memo(ReloadSchemaButton);
+export default memo(ReloadNodeTemplatesButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ClearGraphButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ResetWorkflowButton.tsx
similarity index 78%
rename from invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ClearGraphButton.tsx
rename to invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ResetWorkflowButton.tsx
index 1501d0270b..62e8bf7b4b 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ClearGraphButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/ResetWorkflowButton.tsx
@@ -6,7 +6,10 @@ import {
AlertDialogHeader,
AlertDialogOverlay,
Button,
+ Divider,
+ Flex,
Text,
+ VStack,
useDisclosure,
} from '@chakra-ui/react';
import { RootState } from 'app/store/store';
@@ -19,7 +22,7 @@ import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
-const ClearGraphButton = () => {
+const ResetWorkflowButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -48,8 +51,8 @@ const ClearGraphButton = () => {
<>
}
- tooltip={t('nodes.clearGraph')}
- aria-label={t('nodes.clearGraph')}
+ tooltip={t('nodes.resetWorkflow')}
+ aria-label={t('nodes.resetWorkflow')}
onClick={onOpen}
isDisabled={!nodesCount}
/>
@@ -64,18 +67,21 @@ const ClearGraphButton = () => {
- {t('nodes.clearGraph')}
+ {t('nodes.resetWorkflow')}
-
- {t('nodes.clearGraphDesc')}
+
+
+ {t('nodes.resetWorkflowDesc')}
+ {t('nodes.resetWorkflowDesc2')}
+
-
@@ -85,4 +91,4 @@ const ClearGraphButton = () => {
);
};
-export default memo(ClearGraphButton);
+export default memo(ResetWorkflowButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/SaveWorkflowButton.tsx
new file mode 100644
index 0000000000..45764307a3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/SaveWorkflowButton.tsx
@@ -0,0 +1,29 @@
+import IAIIconButton from 'common/components/IAIIconButton';
+import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaSave } from 'react-icons/fa';
+
+const SaveWorkflowButton = () => {
+ const { t } = useTranslation();
+ const workflow = useWorkflow();
+ const handleSave = 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 (
+ }
+ tooltip={t('nodes.saveWorkflow')}
+ aria-label={t('nodes.saveWorkflow')}
+ onClick={handleSave}
+ />
+ );
+};
+
+export default memo(SaveWorkflowButton);
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 38fbbca397..29e21acd03 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,16 +1,8 @@
-import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { Panel } from 'reactflow';
-import WorkflowEditorControls from './WorkflowEditorControls';
const TopCenterPanel = () => {
- return (
-
-
-
-
-
- );
+ return {null};
};
export default memo(TopCenterPanel);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/WorkflowEditorControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/WorkflowEditorControls.tsx
index 66815deb5a..537d21902e 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/WorkflowEditorControls.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/WorkflowEditorControls.tsx
@@ -1,15 +1,17 @@
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
import { memo } from 'react';
-import ClearGraphButton from './ClearGraphButton';
+import ResetWorkflowButton from './ResetWorkflowButton';
import LoadWorkflowButton from './LoadWorkflowButton';
+import SaveWorkflowButton from './SaveWorkflowButton';
const WorkflowEditorControls = () => {
return (
<>
-
+
+
>
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
index e407eab955..cee6de70a6 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
@@ -1,8 +1,8 @@
-import { Tooltip } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
-import IAIButton from 'common/components/IAIButton';
+import IAIIconButton from 'common/components/IAIIconButton';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
+import { FaPlus } from 'react-icons/fa';
import { Panel } from 'reactflow';
const TopLeftPanel = () => {
@@ -14,15 +14,12 @@ const TopLeftPanel = () => {
return (
-
-
- Add Node
-
-
+ }
+ onClick={handleOpenAddNodePopover}
+ />
);
};
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 20f33aba27..06a57bd875 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
@@ -27,7 +27,7 @@ import {
import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa';
import { SelectionMode } from 'reactflow';
-import ReloadSchemaButton from '../TopCenterPanel/ReloadSchemaButton';
+import ReloadNodeTemplatesButton from '../TopCenterPanel/ReloadSchemaButton';
const formLabelProps: FormLabelProps = {
fontWeight: 600,
@@ -163,7 +163,7 @@ const WorkflowEditorSettings = () => {
label="Validate Connections and Graph"
helperText="Prevent invalid connections from being made, and invalid graphs from being invoked"
/>
-
+
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx
index ed5386938f..aa3b1ad1be 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx
@@ -1,26 +1,10 @@
import { Flex } from '@chakra-ui/react';
-import { RootState } from 'app/store/store';
-import { useAppSelector } from 'app/store/storeHooks';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
-import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
-import { memo, useMemo } from 'react';
-import { useDebounce } from 'use-debounce';
-
-const useWatchWorkflow = () => {
- const nodes = useAppSelector((state: RootState) => state.nodes);
- const [debouncedNodes] = useDebounce(nodes, 300);
- const workflow = useMemo(
- () => buildWorkflow(debouncedNodes),
- [debouncedNodes]
- );
-
- return {
- workflow,
- };
-};
+import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
+import { memo } from 'react';
const WorkflowJSONTab = () => {
- const { workflow } = useWatchWorkflow();
+ const workflow = useWorkflow();
return (
{
return (
@@ -21,8 +22,12 @@ const WorkflowPanel = () => {
h: 'full',
borderRadius: 'base',
p: 4,
+ gap: 2,
}}
>
+
+
+
{
+ const nodes = useAppSelector((state: RootState) => state.nodes);
+ const [debouncedNodes] = useDebounce(nodes, 300);
+ const workflow = useMemo(
+ () => buildWorkflow(debouncedNodes),
+ [debouncedNodes]
+ );
+
+ return workflow;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 3b699ca435..dec0daabe9 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -600,9 +600,9 @@ const nodesSlice = createSlice({
state.workflow.contact = action.payload;
},
workflowLoaded: (state, action: PayloadAction) => {
- // TODO: validation
const { nodes, edges, ...workflow } = action.payload;
state.workflow = workflow;
+
state.nodes = applyNodeChanges(
nodes.map((node) => ({
item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` },
@@ -614,6 +614,16 @@ const nodesSlice = createSlice({
edges.map((edge) => ({ item: edge, type: 'add' })),
[]
);
+
+ state.nodeExecutionStates = nodes.reduce<
+ Record
+ >((acc, node) => {
+ acc[node.id] = {
+ nodeId: node.id,
+ ...initialNodeExecutionState,
+ };
+ return acc;
+ }, {});
},
workflowReset: (state) => {
state.workflow = cloneDeep(initialWorkflow);
diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts
index 33ab1991b8..26aa19bd9d 100644
--- a/invokeai/frontend/web/src/features/nodes/types/types.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/types.ts
@@ -807,7 +807,7 @@ export const zSemVer = z.string().refine((val) => {
export type SemVer = z.infer;
export const zWorkflow = z.object({
- name: z.string().trim().min(1),
+ name: z.string(),
author: z.string(),
description: z.string(),
version: z.string(),
diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx
index bb6e7e862d..e7bd36b931 100644
--- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx
@@ -27,6 +27,7 @@ import { MdCancel, MdCancelScheduleSend } from 'react-icons/md';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { sessionCanceled } from 'services/api/thunks/session';
+import IAIButton from 'common/components/IAIButton';
const cancelButtonSelector = createSelector(
systemSelector,
@@ -49,15 +50,14 @@ const cancelButtonSelector = createSelector(
}
);
-interface CancelButtonProps {
+type Props = Omit & {
btnGroupWidth?: string | number;
-}
+ asIconButton?: boolean;
+};
-const CancelButton = (
- props: CancelButtonProps & Omit
-) => {
+const CancelButton = (props: Props) => {
const dispatch = useAppDispatch();
- const { btnGroupWidth = 'auto', ...rest } = props;
+ const { btnGroupWidth = 'auto', asIconButton = false, ...rest } = props;
const {
isProcessing,
isConnected,
@@ -124,16 +124,31 @@ const CancelButton = (
return (
-
+ {asIconButton ? (
+
+ ) : (
+
+ Cancel
+
+ )}
);
diff --git a/invokeai/frontend/web/src/theme/theme.ts b/invokeai/frontend/web/src/theme/theme.ts
index 248e9d0a83..05ef152502 100644
--- a/invokeai/frontend/web/src/theme/theme.ts
+++ b/invokeai/frontend/web/src/theme/theme.ts
@@ -107,10 +107,15 @@ export const theme: ThemeOverride = {
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 3px var(--invokeai-colors-accent-500)',
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
},
- nodeSelectedOutline: {
+ nodeSelected: {
light: `0 0 0 2px var(--invokeai-colors-accent-400)`,
dark: `0 0 0 2px var(--invokeai-colors-accent-500)`,
},
+ nodeInProgress: {
+ light:
+ '0 0 4px 2px var(--invokeai-colors-accent-500), 0 0 15px 4px var(--invokeai-colors-accent-600)',
+ dark: '0 0 4px 2px var(--invokeai-colors-accent-400), 0 0 15px 4px var(--invokeai-colors-accent-400)',
+ },
},
colors: InvokeAIColors,
components: {