merge conflict

This commit is contained in:
Jennifer Player 2024-02-14 17:28:59 -05:00
commit 5ed7972e5f
34 changed files with 693 additions and 202 deletions

View File

@ -540,7 +540,7 @@ class Graph(BaseModel):
except NodeNotFoundError: except NodeNotFoundError:
return False return False
def get_node(self, node_path: str) -> InvocationsUnion: def get_node(self, node_path: str) -> BaseInvocation:
"""Gets a node from the graph using a node path.""" """Gets a node from the graph using a node path."""
# Materialized graphs may have nodes at the top level # Materialized graphs may have nodes at the top level
graph, node_id = self._get_graph_and_node(node_path) graph, node_id = self._get_graph_and_node(node_path)
@ -891,7 +891,7 @@ class GraphExecutionState(BaseModel):
# If next is still none, there's no next node, return None # If next is still none, there's no next node, return None
return next_node return next_node
def complete(self, node_id: str, output: InvocationOutputsUnion): def complete(self, node_id: str, output: BaseInvocationOutput) -> None:
"""Marks a node as complete""" """Marks a node as complete"""
if node_id not in self.execution_graph.nodes: if node_id not in self.execution_graph.nodes:

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager from contextlib import contextmanager
from typing import List, Union from typing import Callable, List, Union
import torch.nn as nn import torch.nn as nn
from diffusers.models import AutoencoderKL, UNet2DConditionModel from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
def _conv_forward_asymmetric(self, input, weight, bias): def _conv_forward_asymmetric(self, input, weight, bias):
@ -26,51 +27,31 @@ def _conv_forward_asymmetric(self, input, weight, bias):
@contextmanager @contextmanager
def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]):
# Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor
to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = []
try: try:
to_restore = []
for m_name, m in model.named_modules(): for m_name, m in model.named_modules():
if isinstance(model, UNet2DConditionModel): if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
if ".attentions." in m_name:
continue continue
if ".resnets." in m_name: if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name:
if ".conv2" in m_name: # down_blocks.1.resnets.1.conv1
continue _, block_num, _, resnet_num, submodule_name = m_name.split(".")
if ".conv_shortcut" in m_name: block_num = int(block_num)
resnet_num = int(resnet_num)
# Could be configurable to allow skipping arbitrary numbers of down blocks
if block_num >= len(model.down_blocks):
continue continue
""" # Skip the second resnet (could be configurable)
if isinstance(model, UNet2DConditionModel): if resnet_num > 0:
if False and ".upsamplers." in m_name:
continue continue
if False and ".downsamplers." in m_name: # Skip Conv2d layers (could be configurable)
if submodule_name == "conv2":
continue continue
if True and ".resnets." in m_name:
if True and ".conv1" in m_name:
if False and "down_blocks" in m_name:
continue
if False and "mid_block" in m_name:
continue
if False and "up_blocks" in m_name:
continue
if True and ".conv2" in m_name:
continue
if True and ".conv_shortcut" in m_name:
continue
if True and ".attentions." in m_name:
continue
if False and m_name in ["conv_in", "conv_out"]:
continue
"""
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
m.asymmetric_padding_mode = {} m.asymmetric_padding_mode = {}
m.asymmetric_padding = {} m.asymmetric_padding = {}
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"

View File

@ -175,6 +175,7 @@
"statusUpscaling": "Upscaling", "statusUpscaling": "Upscaling",
"statusUpscalingESRGAN": "Upscaling (ESRGAN)", "statusUpscalingESRGAN": "Upscaling (ESRGAN)",
"template": "Template", "template": "Template",
"toResolve": "To resolve",
"training": "Training", "training": "Training",
"trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.", "trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.",
"trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.", "trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.",
@ -900,6 +901,7 @@
"doesNotExist": "does not exist", "doesNotExist": "does not exist",
"downloadWorkflow": "Download Workflow JSON", "downloadWorkflow": "Download Workflow JSON",
"edge": "Edge", "edge": "Edge",
"editMode": "Edit in Workflow Editor",
"enum": "Enum", "enum": "Enum",
"enumDescription": "Enums are values that may be one of a number of options.", "enumDescription": "Enums are values that may be one of a number of options.",
"executionStateCompleted": "Completed", "executionStateCompleted": "Completed",
@ -995,6 +997,7 @@
"problemReadingMetadata": "Problem reading metadata from image", "problemReadingMetadata": "Problem reading metadata from image",
"problemReadingWorkflow": "Problem reading workflow from image", "problemReadingWorkflow": "Problem reading workflow from image",
"problemSettingTitle": "Problem Setting Title", "problemSettingTitle": "Problem Setting Title",
"resetToDefaultValue": "Reset to default value",
"reloadNodeTemplates": "Reload Node Templates", "reloadNodeTemplates": "Reload Node Templates",
"removeLinearView": "Remove from Linear View", "removeLinearView": "Remove from Linear View",
"reorderLinearView": "Reorder Linear View", "reorderLinearView": "Reorder Linear View",
@ -1068,6 +1071,7 @@
"vaeModelFieldDescription": "TODO", "vaeModelFieldDescription": "TODO",
"validateConnections": "Validate Connections and Graph", "validateConnections": "Validate Connections and Graph",
"validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked", "validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked",
"viewMode": "Use in Linear View",
"unableToGetWorkflowVersion": "Unable to get workflow schema version", "unableToGetWorkflowVersion": "Unable to get workflow schema version",
"unrecognizedWorkflowVersion": "Unrecognized workflow schema version {{version}}", "unrecognizedWorkflowVersion": "Unrecognized workflow schema version {{version}}",
"version": "Version", "version": "Version",

View File

@ -6,7 +6,6 @@ import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/typ
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { z } from 'zod'; import { z } from 'zod';
import { fromZodError } from 'zod-validation-error'; import { fromZodError } from 'zod-validation-error';
@ -53,7 +52,6 @@ export const addWorkflowLoadRequestedListener = () => {
}); });
} }
dispatch(setActiveTab('nodes'));
requestAnimationFrame(() => { requestAnimationFrame(() => {
$flow.get()?.fitView(); $flow.get()?.fitView();
}); });

View File

@ -1,22 +1,28 @@
import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { $authToken } from 'app/store/nanostores/authToken';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { imageDownloaded } from 'features/gallery/store/actions'; import { imageDownloaded } from 'features/gallery/store/actions';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useImageUrlToBlob } from './useImageUrlToBlob';
export const useDownloadImage = () => { export const useDownloadImage = () => {
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const imageUrlToBlob = useImageUrlToBlob();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const authToken = useStore($authToken);
const downloadImage = useCallback( const downloadImage = useCallback(
async (image_url: string, image_name: string) => { async (image_url: string, image_name: string) => {
try { try {
const blob = await imageUrlToBlob(image_url); const requestOpts = authToken
? {
headers: {
Authorization: `Bearer ${authToken}`,
},
}
: {};
const blob = await fetch(image_url, requestOpts).then((resp) => resp.blob());
if (!blob) { if (!blob) {
throw new Error('Unable to create Blob'); throw new Error('Unable to create Blob');
} }
@ -40,7 +46,7 @@ export const useDownloadImage = () => {
}); });
} }
}, },
[t, toaster, imageUrlToBlob, dispatch] [t, toaster, dispatch, authToken]
); );
return { downloadImage }; return { downloadImage };

View File

@ -1,6 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { import {
selectWorkflowSlice, selectWorkflowSlice,
workflowExposedFieldAdded, workflowExposedFieldAdded,
@ -18,7 +19,7 @@ type Props = {
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => { const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const value = useFieldValue(nodeId, fieldName);
const selectIsExposed = useMemo( const selectIsExposed = useMemo(
() => () =>
createSelector(selectWorkflowSlice, (workflow) => { createSelector(selectWorkflowSlice, (workflow) => {
@ -30,8 +31,8 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
const isExposed = useAppSelector(selectIsExposed); const isExposed = useAppSelector(selectIsExposed);
const handleExposeField = useCallback(() => { const handleExposeField = useCallback(() => {
dispatch(workflowExposedFieldAdded({ nodeId, fieldName })); dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
}, [dispatch, fieldName, nodeId]); }, [dispatch, fieldName, nodeId, value]);
const handleUnexposeField = useCallback(() => { const handleUnexposeField = useCallback(() => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));

View File

@ -8,7 +8,7 @@ import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiInfoBold, PiTrashSimpleBold, PiDotsSixVerticalBold } from 'react-icons/pi'; import { PiDotsSixVerticalBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
import EditableFieldTitle from './EditableFieldTitle'; import EditableFieldTitle from './EditableFieldTitle';
import FieldTooltipContent from './FieldTooltipContent'; import FieldTooltipContent from './FieldTooltipContent';

View File

@ -1,25 +1,23 @@
import { Flex, Spacer } from '@invoke-ai/ui-library'; import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton'; import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName'; import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react'; import { memo } from 'react';
const TopCenterPanel = () => { const TopCenterPanel = () => {
const name = useAppSelector((s) => s.workflow.name);
return ( return (
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none"> <Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
<Flex flexDir="column" gap="2">
<Flex gap="2"> <Flex gap="2">
<AddNodeButton /> <AddNodeButton />
<WorkflowLibraryButton />
</Flex>
<UpdateNodesButton /> <UpdateNodesButton />
</Flex> </Flex>
<Spacer /> <Spacer />
<WorkflowName /> {!!name.length && <WorkflowName />}
<Spacer /> <Spacer />
<ClearFlowButton /> <ClearFlowButton />
<SaveWorkflowButton /> <SaveWorkflowButton />

View File

@ -25,6 +25,7 @@ const UpdateNodesButton = () => {
icon={<PiWarningBold />} icon={<PiWarningBold />}
onClick={handleClickUpdateNodes} onClick={handleClickUpdateNodes}
pointerEvents="auto" pointerEvents="auto"
colorScheme="warning"
/> />
); );
}; };

View File

@ -1,15 +0,0 @@
import { Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react';
const TopCenterPanel = () => {
const name = useAppSelector((s) => s.workflow.name);
return (
<Text m={2} fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold" opacity={0.8}>
{name}
</Text>
);
};
export default memo(TopCenterPanel);

View File

@ -1,15 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
const TopRightPanel = () => {
return (
<Flex gap={2} position="absolute" top={2} insetInlineEnd={2}>
<WorkflowLibraryButton />
<WorkflowLibraryMenu />
</Flex>
);
};
export default memo(TopRightPanel);

View File

@ -0,0 +1,43 @@
import { Flex, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
export const ModeToggle = () => {
const dispatch = useAppDispatch();
const mode = useAppSelector((s) => s.workflow.mode);
const { t } = useTranslation();
const onClickEdit = useCallback(() => {
dispatch(workflowModeChanged('edit'));
}, [dispatch]);
const onClickView = useCallback(() => {
dispatch(workflowModeChanged('view'));
}, [dispatch]);
return (
<Flex justifyContent="flex-end">
{mode === 'view' && (
<IconButton
aria-label={t('nodes.editMode')}
tooltip={t('nodes.editMode')}
onClick={onClickEdit}
icon={<PiPencilBold />}
colorScheme="invokeBlue"
/>
)}
{mode === 'edit' && (
<IconButton
aria-label={t('nodes.viewMode')}
tooltip={t('nodes.viewMode')}
onClick={onClickView}
icon={<PiEyeBold />}
colorScheme="invokeBlue"
/>
)}
</Flex>
);
};

View File

@ -1,22 +1,37 @@
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { Flex } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import QueueControls from 'features/queue/components/QueueControls'; import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
import InspectorPanel from './inspector/InspectorPanel'; import InspectorPanel from './inspector/InspectorPanel';
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
import WorkflowPanel from './workflow/WorkflowPanel'; import WorkflowPanel from './workflow/WorkflowPanel';
import { WorkflowMenu } from './WorkflowMenu';
import { WorkflowName } from './WorkflowName';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' }; const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
return {
mode: workflow.mode,
};
});
const NodeEditorPanelGroup = () => { const NodeEditorPanelGroup = () => {
const { mode } = useAppSelector(selector);
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null); const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const panelStorage = usePanelStorage(); const panelStorage = usePanelStorage();
const handleDoubleClickHandle = useCallback(() => { const handleDoubleClickHandle = useCallback(() => {
if (!panelGroupRef.current) { if (!panelGroupRef.current) {
return; return;
@ -27,6 +42,16 @@ const NodeEditorPanelGroup = () => {
return ( return (
<Flex w="full" h="full" gap={2} flexDir="column"> <Flex w="full" h="full" gap={2} flexDir="column">
<QueueControls /> <QueueControls />
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
<Flex justifyContent="space-between" alignItems="center" gap="4">
<WorkflowLibraryButton />
<WorkflowName />
</Flex>
<WorkflowMenu />
</Flex>
{mode === 'view' && <WorkflowViewMode />}
{mode === 'edit' && (
<PanelGroup <PanelGroup
ref={panelGroupRef} ref={panelGroupRef}
id="workflow-panel-group" id="workflow-panel-group"
@ -43,6 +68,7 @@ const NodeEditorPanelGroup = () => {
<InspectorPanel /> <InspectorPanel />
</Panel> </Panel>
</PanelGroup> </PanelGroup>
)}
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,26 @@
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
import { ModeToggle } from './ModeToggle';
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
return {
mode: workflow.mode,
};
});
export const WorkflowMenu = () => {
const { mode } = useAppSelector(selector);
return (
<Flex gap="2" alignItems="center">
{mode === 'edit' && <SaveWorkflowButton />}
<NewWorkflowButton />
<ModeToggle />
</Flex>
);
};

View File

@ -0,0 +1,37 @@
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useTranslation } from 'react-i18next';
import { PiDotOutlineFill } from 'react-icons/pi';
import WorkflowInfoTooltipContent from './viewMode/WorkflowInfoTooltipContent';
import { WorkflowWarning } from './viewMode/WorkflowWarning';
export const WorkflowName = () => {
const { name, isTouched, mode } = useAppSelector((s) => s.workflow);
const { t } = useTranslation();
return (
<Flex gap="1" alignItems="center">
{name.length ? (
<Tooltip label={<WorkflowInfoTooltipContent />} placement="top">
<Text fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold">
{name}
</Text>
</Tooltip>
) : (
<Text fontSize="lg" fontStyle="italic" fontWeight="semibold">
{t('workflows.unnamedWorkflow')}
</Text>
)}
{isTouched && mode === 'edit' && (
<Tooltip label="Workflow has unsaved changes">
<Flex>
<Icon as={PiDotOutlineFill} boxSize="20px" sx={{ color: 'invokeYellow.500' }} />
</Flex>
</Tooltip>
)}
<WorkflowWarning />
</Flex>
);
};

View File

@ -0,0 +1,53 @@
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { t } from 'i18next';
import { memo } from 'react';
import { PiArrowCounterClockwiseBold, PiInfoBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
const WorkflowField = ({ nodeId, fieldName }: Props) => {
const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'input');
const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
return (
<Flex layerStyle="second" position="relative" borderRadius="base" w="full" p={4} gap="2" flexDir="column">
<Flex alignItems="center">
<FormLabel fontSize="sm">{label || fieldTemplateTitle}</FormLabel>
<Spacer />
{isValueChanged && (
<IconButton
aria-label={t('nodes.resetToDefaultValue')}
tooltip={t('nodes.resetToDefaultValue')}
variant="ghost"
size="sm"
onClick={onReset}
icon={<PiArrowCounterClockwiseBold />}
/>
)}
<Tooltip
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Flex h="24px" alignItems="center">
<Icon fontSize="md" color="base.300" as={PiInfoBold} />
</Flex>
</Tooltip>
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
);
};
export default memo(WorkflowField);

View File

@ -0,0 +1,68 @@
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
return {
name: workflow.name,
description: workflow.description,
notes: workflow.notes,
author: workflow.author,
tags: workflow.tags,
};
});
const WorkflowInfoTooltipContent = () => {
const { name, description, notes, author, tags } = useAppSelector(selector);
const { t } = useTranslation();
return (
<Flex flexDir="column" gap="2">
{!!name.length && (
<Box>
<Text fontWeight="semibold">{t('nodes.workflowName')}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{name}
</Text>
</Box>
)}
{!!author.length && (
<Box>
<Text fontWeight="semibold">{t('nodes.workflowAuthor')}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{author}
</Text>
</Box>
)}
{!!tags.length && (
<Box>
<Text fontWeight="semibold">{t('nodes.workflowTags')}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{tags}
</Text>
</Box>
)}
{!!description.length && (
<Box>
<Text fontWeight="semibold">{t('nodes.workflowDescription')}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{description}
</Text>
</Box>
)}
{!!notes.length && (
<Box>
<Text fontWeight="semibold">{t('nodes.workflowNotes')}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{notes}
</Text>
</Box>
)}
</Flex>
);
};
export default memo(WorkflowInfoTooltipContent);

View File

@ -0,0 +1,39 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { t } from 'i18next';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
import WorkflowField from './WorkflowField';
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
return {
fields: workflow.exposedFields,
name: workflow.name,
};
});
export const WorkflowViewMode = () => {
const { isLoading } = useGetOpenAPISchemaQuery();
const { fields } = useAppSelector(selector);
return (
<Box position="relative" w="full" h="full">
<ScrollableContent>
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
{isLoading ? (
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
) : fields.length ? (
fields.map(({ nodeId, fieldName }) => (
<WorkflowField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
))
) : (
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
)}
</Flex>
</ScrollableContent>
</Box>
);
};

View File

@ -0,0 +1,21 @@
import { Flex, Icon, Tooltip } from '@invoke-ai/ui-library';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { PiWarningBold } from 'react-icons/pi';
import { WorkflowWarningTooltip } from './WorkflowWarningTooltip';
export const WorkflowWarning = () => {
const nodesNeedUpdate = useGetNodesNeedUpdate();
if (!nodesNeedUpdate) {
return <></>;
}
return (
<Tooltip label={<WorkflowWarningTooltip />}>
<Flex h="full" alignItems="center" gap="2">
<Icon color="warning.400" as={PiWarningBold} />
</Flex>
</Tooltip>
);
};

View File

@ -0,0 +1,20 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useTranslation } from 'react-i18next';
export const WorkflowWarningTooltip = () => {
const { t } = useTranslation();
return (
<Flex flexDir="column" gap="2">
<Flex flexDir="column" gap="2">
<Text fontWeight="semibold">{t('toast.loadedWithWarnings')}</Text>
<Flex flexDir="column">
<Text>{t('common.toResolve')}:</Text>
<Text>
{t('nodes.editMode')} &gt;&gt; {t('nodes.updateAllNodes')} &gt;&gt; {t('common.save')}
</Text>
</Flex>
</Flex>
</Flex>
);
};

View File

@ -12,17 +12,17 @@ const WorkflowPanel = () => {
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}> <Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column"> <Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
<TabList> <TabList>
<Tab>{t('common.linear')}</Tab>
<Tab>{t('common.details')}</Tab> <Tab>{t('common.details')}</Tab>
<Tab>{t('common.linear')}</Tab>
<Tab>JSON</Tab> <Tab>JSON</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
<WorkflowLinearTab /> <WorkflowGeneralTab />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<WorkflowGeneralTab /> <WorkflowLinearTab />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<WorkflowJSONTab /> <WorkflowJSONTab />

View File

@ -0,0 +1,28 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { isEqual } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useFieldOriginalValue = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const selectOriginalExposedFieldValues = useMemo(
() =>
createSelector(
selectWorkflowSlice,
(workflow) =>
workflow.originalExposedFieldValues.find((v) => v.nodeId === nodeId && v.fieldName === fieldName)?.value
),
[nodeId, fieldName]
);
const originalValue = useAppSelector(selectOriginalExposedFieldValues);
const value = useFieldValue(nodeId, fieldName);
const isValueChanged = useMemo(() => !isEqual(value, originalValue), [value, originalValue]);
const onReset = useCallback(() => {
dispatch(fieldValueReset({ nodeId, fieldName, value: originalValue }));
}, [dispatch, fieldName, nodeId, originalValue]);
return { originalValue, isValueChanged, onReset };
};

View File

@ -0,0 +1,23 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldValue = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node?.data.inputs[fieldName]?.value;
}),
[fieldName, nodeId]
);
const value = useAppSelector(selector);
return value;
};

View File

@ -18,6 +18,7 @@ import type {
MainModelFieldValue, MainModelFieldValue,
SchedulerFieldValue, SchedulerFieldValue,
SDXLRefinerModelFieldValue, SDXLRefinerModelFieldValue,
StatefulFieldValue,
StringFieldValue, StringFieldValue,
T2IAdapterModelFieldValue, T2IAdapterModelFieldValue,
VAEModelFieldValue, VAEModelFieldValue,
@ -36,6 +37,7 @@ import {
zMainModelFieldValue, zMainModelFieldValue,
zSchedulerFieldValue, zSchedulerFieldValue,
zSDXLRefinerModelFieldValue, zSDXLRefinerModelFieldValue,
zStatefulFieldValue,
zStringFieldValue, zStringFieldValue,
zT2IAdapterModelFieldValue, zT2IAdapterModelFieldValue,
zVAEModelFieldValue, zVAEModelFieldValue,
@ -478,6 +480,9 @@ export const nodesSlice = createSlice({
selectedEdgesChanged: (state, action: PayloadAction<string[]>) => { selectedEdgesChanged: (state, action: PayloadAction<string[]>) => {
state.selectedEdges = action.payload; state.selectedEdges = action.payload;
}, },
fieldValueReset: (state, action: FieldValueAction<StatefulFieldValue>) => {
fieldValueReducer(state, action, zStatefulFieldValue);
},
fieldStringValueChanged: (state, action: FieldValueAction<StringFieldValue>) => { fieldStringValueChanged: (state, action: FieldValueAction<StringFieldValue>) => {
fieldValueReducer(state, action, zStringFieldValue); fieldValueReducer(state, action, zStringFieldValue);
}, },
@ -760,6 +765,7 @@ export const {
edgesChanged, edgesChanged,
edgesDeleted, edgesDeleted,
edgeUpdated, edgeUpdated,
fieldValueReset,
fieldBoardValueChanged, fieldBoardValueChanged,
fieldBooleanValueChanged, fieldBooleanValueChanged,
fieldColorValueChanged, fieldColorValueChanged,
@ -834,7 +840,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
nodeIsOpenChanged, nodeIsOpenChanged,
nodeLabelChanged, nodeLabelChanged,
nodeNotesChanged, nodeNotesChanged,
nodesChanged,
nodesDeleted, nodesDeleted,
nodeUseCacheChanged, nodeUseCacheChanged,
notesNodeValueChanged, notesNodeValueChanged,

View File

@ -1,4 +1,4 @@
import type { FieldType } from 'features/nodes/types/field'; import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/nodes/types/field';
import type { import type {
AnyNode, AnyNode,
InvocationNodeEdge, InvocationNodeEdge,
@ -33,9 +33,16 @@ export type NodesState = {
selectionMode: SelectionMode; selectionMode: SelectionMode;
}; };
export type WorkflowMode = 'edit' | 'view';
export type FieldIdentifierWithValue = FieldIdentifier & {
value: StatefulFieldValue;
};
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'> & { export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'> & {
_version: 1; _version: 1;
isTouched: boolean; isTouched: boolean;
mode: WorkflowMode;
originalExposedFieldValues: FieldIdentifierWithValue[];
}; };
export type NodeTemplatesState = { export type NodeTemplatesState = {

View File

@ -2,11 +2,16 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { workflowLoaded } from 'features/nodes/store/actions'; import { workflowLoaded } from 'features/nodes/store/actions';
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice'; import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types'; import type {
FieldIdentifierWithValue,
WorkflowMode,
WorkflowsState as WorkflowState,
} from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field'; import type { FieldIdentifier } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow'; import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, isEqual, uniqBy } from 'lodash-es'; import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = { export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
name: '', name: '',
@ -23,7 +28,9 @@ export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
export const initialWorkflowState: WorkflowState = { export const initialWorkflowState: WorkflowState = {
_version: 1, _version: 1,
isTouched: true, isTouched: false,
mode: 'view',
originalExposedFieldValues: [],
...blankWorkflow, ...blankWorkflow,
}; };
@ -31,15 +38,25 @@ export const workflowSlice = createSlice({
name: 'workflow', name: 'workflow',
initialState: initialWorkflowState, initialState: initialWorkflowState,
reducers: { reducers: {
workflowExposedFieldAdded: (state, action: PayloadAction<FieldIdentifier>) => { workflowModeChanged: (state, action: PayloadAction<WorkflowMode>) => {
state.mode = action.payload;
},
workflowExposedFieldAdded: (state, action: PayloadAction<FieldIdentifierWithValue>) => {
state.exposedFields = uniqBy( state.exposedFields = uniqBy(
state.exposedFields.concat(action.payload), state.exposedFields.concat(omit(action.payload, 'value')),
(field) => `${field.nodeId}-${field.fieldName}`
);
state.originalExposedFieldValues = uniqBy(
state.originalExposedFieldValues.concat(action.payload),
(field) => `${field.nodeId}-${field.fieldName}` (field) => `${field.nodeId}-${field.fieldName}`
); );
state.isTouched = true; state.isTouched = true;
}, },
workflowExposedFieldRemoved: (state, action: PayloadAction<FieldIdentifier>) => { workflowExposedFieldRemoved: (state, action: PayloadAction<FieldIdentifier>) => {
state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload)); state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload));
state.originalExposedFieldValues = state.originalExposedFieldValues.filter(
(field) => !isEqual(omit(field, 'value'), action.payload)
);
state.isTouched = true; state.isTouched = true;
}, },
workflowExposedFieldsReordered: (state, action: PayloadAction<FieldIdentifier[]>) => { workflowExposedFieldsReordered: (state, action: PayloadAction<FieldIdentifier[]>) => {
@ -82,15 +99,43 @@ export const workflowSlice = createSlice({
workflowIDChanged: (state, action: PayloadAction<string>) => { workflowIDChanged: (state, action: PayloadAction<string>) => {
state.id = action.payload; state.id = action.payload;
}, },
workflowReset: () => cloneDeep(initialWorkflowState),
workflowSaved: (state) => { workflowSaved: (state) => {
state.isTouched = false; state.isTouched = false;
}, },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(workflowLoaded, (state, action) => { builder.addCase(workflowLoaded, (state, action) => {
const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload; const { nodes, edges: _edges, ...workflowExtra } = action.payload;
return { ...initialWorkflowState, ...cloneDeep(workflowExtra) };
const originalExposedFieldValues: FieldIdentifierWithValue[] = [];
workflowExtra.exposedFields.forEach((field) => {
const node = nodes.find((n) => n.id === field.nodeId);
if (!isInvocationNode(node)) {
return;
}
const input = node.data.inputs[field.fieldName];
if (!input) {
return;
}
const originalExposedFieldValue = {
nodeId: field.nodeId,
fieldName: field.fieldName,
value: input.value,
};
originalExposedFieldValues.push(originalExposedFieldValue);
});
return {
...cloneDeep(initialWorkflowState),
...cloneDeep(workflowExtra),
originalExposedFieldValues,
mode: state.mode,
};
}); });
builder.addCase(nodesDeleted, (state, action) => { builder.addCase(nodesDeleted, (state, action) => {
@ -101,6 +146,29 @@ export const workflowSlice = createSlice({
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState)); builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
builder.addCase(nodesChanged, (state, action) => {
// Not all changes to nodes should result in the workflow being marked touched
const filteredChanges = action.payload.filter((change) => {
// We always want to mark the workflow as touched if a node is added, removed, or reset
if (['add', 'remove', 'reset'].includes(change.type)) {
return true;
}
// Position changes can change the position and the dragging status of the node - ignore if the change doesn't
// affect the position
if (change.type === 'position' && (change.position || change.positionAbsolute)) {
return true;
}
// This change isn't relevant
return false;
});
if (filteredChanges.length > 0) {
state.isTouched = true;
}
});
builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => { builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
state.isTouched = true; state.isTouched = true;
}); });
@ -108,6 +176,7 @@ export const workflowSlice = createSlice({
}); });
export const { export const {
workflowModeChanged,
workflowExposedFieldAdded, workflowExposedFieldAdded,
workflowExposedFieldRemoved, workflowExposedFieldRemoved,
workflowExposedFieldsReordered, workflowExposedFieldsReordered,
@ -120,7 +189,6 @@ export const {
workflowVersionChanged, workflowVersionChanged,
workflowContactChanged, workflowContactChanged,
workflowIDChanged, workflowIDChanged,
workflowReset,
workflowSaved, workflowSaved,
} = workflowSlice.actions; } = workflowSlice.actions;

View File

@ -101,9 +101,11 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
const clearStorage = useClearStorage(); const clearStorage = useClearStorage();
const handleOpenSettingsModel = useCallback(() => { const handleOpenSettingsModel = useCallback(() => {
if (shouldShowClearIntermediates) {
refetchIntermediatesCount(); refetchIntermediatesCount();
}
_onSettingsModalOpen(); _onSettingsModalOpen();
}, [_onSettingsModalOpen, refetchIntermediatesCount]); }, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]);
const handleClickResetWebUI = useCallback(() => { const handleClickResetWebUI = useCallback(() => {
clearStorage(); clearStorage();

View File

@ -1,13 +1,28 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
import NodeEditor from 'features/nodes/components/NodeEditor'; import NodeEditor from 'features/nodes/components/NodeEditor';
import { memo } from 'react'; import { memo } from 'react';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
const NodesTab = () => { const NodesTab = () => {
const mode = useAppSelector((s) => s.workflow.mode);
if (mode === 'edit') {
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<NodeEditor /> <NodeEditor />
</ReactFlowProvider> </ReactFlowProvider>
); );
} else {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<Flex w="full" h="full">
<CurrentImageDisplay />
</Flex>
</Box>
);
}
}; };
export default memo(NodesTab); export default memo(NodesTab);

View File

@ -0,0 +1,26 @@
import { IconButton } from '@invoke-ai/ui-library';
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFilePlusBold } from 'react-icons/pi';
export const NewWorkflowButton = memo(() => {
const { t } = useTranslation();
const renderButton = useCallback(
(onClick: () => void) => (
<IconButton
aria-label={t('nodes.newWorkflow')}
tooltip={t('nodes.newWorkflow')}
icon={<PiFilePlusBold />}
onClick={onClick}
pointerEvents="auto"
/>
),
[t]
);
return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
});
NewWorkflowButton.displayName = 'NewWorkflowButton';

View File

@ -0,0 +1,63 @@
import { ConfirmationAlertDialog, Flex, Text, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
renderButton: (onClick: () => void) => JSX.Element;
};
export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const isTouched = useAppSelector((s) => s.workflow.isTouched);
const handleNewWorkflow = useCallback(() => {
dispatch(nodeEditorReset());
dispatch(workflowModeChanged('edit'));
dispatch(
addToast(
makeToast({
title: t('workflows.newWorkflowCreated'),
status: 'success',
})
)
);
onClose();
}, [dispatch, onClose, t]);
const onClick = useCallback(() => {
if (!isTouched) {
handleNewWorkflow();
return;
}
onOpen();
}, [handleNewWorkflow, isTouched, onOpen]);
return (
<>
{props.renderButton(onClick)}
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('nodes.newWorkflow')}
acceptCallback={handleNewWorkflow}
>
<Flex flexDir="column" gap={2}>
<Text>{t('nodes.newWorkflowDesc')}</Text>
<Text variant="subtext">{t('nodes.newWorkflowDesc2')}</Text>
</Flex>
</ConfirmationAlertDialog>
</>
);
});
NewWorkflowConfirmationAlertDialog.displayName = 'NewWorkflowConfirmationAlertDialog';

View File

@ -2,7 +2,7 @@ import { IconButton, useDisclosure } from '@invoke-ai/ui-library';
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext'; import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiBooksBold } from 'react-icons/pi'; import { PiFolderOpenBold } from 'react-icons/pi';
import WorkflowLibraryModal from './WorkflowLibraryModal'; import WorkflowLibraryModal from './WorkflowLibraryModal';
@ -15,7 +15,7 @@ const WorkflowLibraryButton = () => {
<IconButton <IconButton
aria-label={t('workflows.workflowLibrary')} aria-label={t('workflows.workflowLibrary')}
tooltip={t('workflows.workflowLibrary')} tooltip={t('workflows.workflowLibrary')}
icon={<PiBooksBold />} icon={<PiFolderOpenBold />}
onClick={disclosure.onOpen} onClick={disclosure.onOpen}
pointerEvents="auto" pointerEvents="auto"
/> />

View File

@ -1,60 +1,22 @@
import { ConfirmationAlertDialog, Flex, MenuItem, Text, useDisclosure } from '@invoke-ai/ui-library'; import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi'; import { PiFilePlusBold } from 'react-icons/pi';
const NewWorkflowMenuItem = () => { export const NewWorkflowMenuItem = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const isTouched = useAppSelector((s) => s.workflow.isTouched);
const handleNewWorkflow = useCallback(() => { const renderButton = useCallback(
dispatch(nodeEditorReset()); (onClick: () => void) => (
<MenuItem as="button" icon={<PiFilePlusBold />} onClick={onClick}>
dispatch(
addToast(
makeToast({
title: t('workflows.newWorkflowCreated'),
status: 'success',
})
)
);
onClose();
}, [dispatch, onClose, t]);
const onClick = useCallback(() => {
if (!isTouched) {
handleNewWorkflow();
return;
}
onOpen();
}, [handleNewWorkflow, isTouched, onOpen]);
return (
<>
<MenuItem as="button" icon={<PiFlowArrowBold />} onClick={onClick}>
{t('nodes.newWorkflow')} {t('nodes.newWorkflow')}
</MenuItem> </MenuItem>
),
<ConfirmationAlertDialog [t]
isOpen={isOpen}
onClose={onClose}
title={t('nodes.newWorkflow')}
acceptCallback={handleNewWorkflow}
>
<Flex flexDir="column" gap={2}>
<Text>{t('nodes.newWorkflowDesc')}</Text>
<Text variant="subtext">{t('nodes.newWorkflowDesc2')}</Text>
</Flex>
</ConfirmationAlertDialog>
</>
); );
};
export default memo(NewWorkflowMenuItem); return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
});
NewWorkflowMenuItem.displayName = 'NewWorkflowMenuItem';

View File

@ -8,7 +8,7 @@ import {
useGlobalMenuClose, useGlobalMenuClose,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem'; import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem'; import { NewWorkflowMenuItem } from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem'; import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem'; import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem'; import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';

View File

@ -33,7 +33,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
# Core generation dependencies, pinned for reproducible builds. # Core generation dependencies, pinned for reproducible builds.
"accelerate==0.26.1", "accelerate==0.27.2",
"clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip",
"compel==2.0.2", "compel==2.0.2",
"controlnet-aux==0.0.7", "controlnet-aux==0.0.7",