mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
merge conflict
This commit is contained in:
commit
5ed7972e5f
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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 };
|
||||||
|
@ -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 }));
|
||||||
|
@ -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';
|
||||||
|
@ -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 />
|
||||||
|
@ -25,6 +25,7 @@ const UpdateNodesButton = () => {
|
|||||||
icon={<PiWarningBold />}
|
icon={<PiWarningBold />}
|
||||||
onClick={handleClickUpdateNodes}
|
onClick={handleClickUpdateNodes}
|
||||||
pointerEvents="auto"
|
pointerEvents="auto"
|
||||||
|
colorScheme="warning"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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);
|
|
@ -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);
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
@ -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);
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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')} >> {t('nodes.updateAllNodes')} >> {t('common.save')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -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 />
|
||||||
|
@ -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 };
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
@ -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';
|
@ -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';
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user