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:
|
||||
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."""
|
||||
# Materialized graphs may have nodes at the top level
|
||||
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
|
||||
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"""
|
||||
|
||||
if node_id not in self.execution_graph.nodes:
|
||||
|
@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Union
|
||||
from typing import Callable, List, Union
|
||||
|
||||
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):
|
||||
@ -26,70 +27,50 @@ def _conv_forward_asymmetric(self, input, weight, bias):
|
||||
|
||||
@contextmanager
|
||||
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:
|
||||
to_restore = []
|
||||
|
||||
for m_name, m in model.named_modules():
|
||||
if isinstance(model, UNet2DConditionModel):
|
||||
if ".attentions." in m_name:
|
||||
if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
||||
continue
|
||||
|
||||
if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name:
|
||||
# down_blocks.1.resnets.1.conv1
|
||||
_, block_num, _, resnet_num, submodule_name = m_name.split(".")
|
||||
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
|
||||
|
||||
if ".resnets." in m_name:
|
||||
if ".conv2" in m_name:
|
||||
continue
|
||||
if ".conv_shortcut" in m_name:
|
||||
continue
|
||||
|
||||
"""
|
||||
if isinstance(model, UNet2DConditionModel):
|
||||
if False and ".upsamplers." in m_name:
|
||||
# Skip the second resnet (could be configurable)
|
||||
if resnet_num > 0:
|
||||
continue
|
||||
|
||||
if False and ".downsamplers." in m_name:
|
||||
# Skip Conv2d layers (could be configurable)
|
||||
if submodule_name == "conv2":
|
||||
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
|
||||
m.asymmetric_padding_mode = {}
|
||||
m.asymmetric_padding = {}
|
||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["x"] = (
|
||||
m._reversed_padding_repeated_twice[0],
|
||||
m._reversed_padding_repeated_twice[1],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["y"] = (
|
||||
0,
|
||||
0,
|
||||
m._reversed_padding_repeated_twice[2],
|
||||
m._reversed_padding_repeated_twice[3],
|
||||
)
|
||||
|
||||
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 = {}
|
||||
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["x"] = (
|
||||
m._reversed_padding_repeated_twice[0],
|
||||
m._reversed_padding_repeated_twice[1],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
|
||||
m.asymmetric_padding["y"] = (
|
||||
0,
|
||||
0,
|
||||
m._reversed_padding_repeated_twice[2],
|
||||
m._reversed_padding_repeated_twice[3],
|
||||
)
|
||||
|
||||
to_restore.append((m, m._conv_forward))
|
||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
||||
to_restore.append((m, m._conv_forward))
|
||||
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
|
||||
|
||||
yield
|
||||
|
||||
|
@ -175,6 +175,7 @@
|
||||
"statusUpscaling": "Upscaling",
|
||||
"statusUpscalingESRGAN": "Upscaling (ESRGAN)",
|
||||
"template": "Template",
|
||||
"toResolve": "To resolve",
|
||||
"training": "Training",
|
||||
"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.",
|
||||
@ -900,6 +901,7 @@
|
||||
"doesNotExist": "does not exist",
|
||||
"downloadWorkflow": "Download Workflow JSON",
|
||||
"edge": "Edge",
|
||||
"editMode": "Edit in Workflow Editor",
|
||||
"enum": "Enum",
|
||||
"enumDescription": "Enums are values that may be one of a number of options.",
|
||||
"executionStateCompleted": "Completed",
|
||||
@ -995,6 +997,7 @@
|
||||
"problemReadingMetadata": "Problem reading metadata from image",
|
||||
"problemReadingWorkflow": "Problem reading workflow from image",
|
||||
"problemSettingTitle": "Problem Setting Title",
|
||||
"resetToDefaultValue": "Reset to default value",
|
||||
"reloadNodeTemplates": "Reload Node Templates",
|
||||
"removeLinearView": "Remove from Linear View",
|
||||
"reorderLinearView": "Reorder Linear View",
|
||||
@ -1068,6 +1071,7 @@
|
||||
"vaeModelFieldDescription": "TODO",
|
||||
"validateConnections": "Validate Connections and Graph",
|
||||
"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",
|
||||
"unrecognizedWorkflowVersion": "Unrecognized workflow schema version {{version}}",
|
||||
"version": "Version",
|
||||
|
@ -6,7 +6,6 @@ import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/typ
|
||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { t } from 'i18next';
|
||||
import { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
@ -53,7 +52,6 @@ export const addWorkflowLoadRequestedListener = () => {
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setActiveTab('nodes'));
|
||||
requestAnimationFrame(() => {
|
||||
$flow.get()?.fitView();
|
||||
});
|
||||
|
@ -1,22 +1,28 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imageDownloaded } from 'features/gallery/store/actions';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useImageUrlToBlob } from './useImageUrlToBlob';
|
||||
|
||||
export const useDownloadImage = () => {
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const imageUrlToBlob = useImageUrlToBlob();
|
||||
const dispatch = useAppDispatch();
|
||||
const authToken = useStore($authToken);
|
||||
|
||||
const downloadImage = useCallback(
|
||||
async (image_url: string, image_name: string) => {
|
||||
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) {
|
||||
throw new Error('Unable to create Blob');
|
||||
}
|
||||
@ -40,7 +46,7 @@ export const useDownloadImage = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, toaster, imageUrlToBlob, dispatch]
|
||||
[t, toaster, dispatch, authToken]
|
||||
);
|
||||
|
||||
return { downloadImage };
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import {
|
||||
selectWorkflowSlice,
|
||||
workflowExposedFieldAdded,
|
||||
@ -18,7 +19,7 @@ type Props = {
|
||||
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const selectIsExposed = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
@ -30,8 +31,8 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
|
||||
const isExposed = useAppSelector(selectIsExposed);
|
||||
|
||||
const handleExposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
|
||||
}, [dispatch, fieldName, nodeId, value]);
|
||||
|
||||
const handleUnexposeField = useCallback(() => {
|
||||
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 { memo, useCallback } from 'react';
|
||||
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 FieldTooltipContent from './FieldTooltipContent';
|
||||
|
@ -1,25 +1,23 @@
|
||||
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 ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
||||
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
|
||||
import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName';
|
||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const name = useAppSelector((s) => s.workflow.name);
|
||||
return (
|
||||
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
|
||||
<Flex flexDir="column" gap="2">
|
||||
<Flex gap="2">
|
||||
<AddNodeButton />
|
||||
<WorkflowLibraryButton />
|
||||
</Flex>
|
||||
<Flex gap="2">
|
||||
<AddNodeButton />
|
||||
<UpdateNodesButton />
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<WorkflowName />
|
||||
{!!name.length && <WorkflowName />}
|
||||
<Spacer />
|
||||
<ClearFlowButton />
|
||||
<SaveWorkflowButton />
|
||||
|
@ -25,6 +25,7 @@ const UpdateNodesButton = () => {
|
||||
icon={<PiWarningBold />}
|
||||
onClick={handleClickUpdateNodes}
|
||||
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 { 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 ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import InspectorPanel from './inspector/InspectorPanel';
|
||||
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
|
||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||
import { WorkflowMenu } from './WorkflowMenu';
|
||||
import { WorkflowName } from './WorkflowName';
|
||||
|
||||
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
mode: workflow.mode,
|
||||
};
|
||||
});
|
||||
|
||||
const NodeEditorPanelGroup = () => {
|
||||
const { mode } = useAppSelector(selector);
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const panelStorage = usePanelStorage();
|
||||
|
||||
const handleDoubleClickHandle = useCallback(() => {
|
||||
if (!panelGroupRef.current) {
|
||||
return;
|
||||
@ -27,22 +42,33 @@ const NodeEditorPanelGroup = () => {
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<QueueControls />
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
storage={panelStorage}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
</Panel>
|
||||
<ResizeHandle orientation="horizontal" onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
<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
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
autoSaveId="workflow-panel-group"
|
||||
direction="vertical"
|
||||
style={panelGroupStyles}
|
||||
storage={panelStorage}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
</Panel>
|
||||
<ResizeHandle orientation="horizontal" onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
)}
|
||||
</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}>
|
||||
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
|
||||
<TabList>
|
||||
<Tab>{t('common.linear')}</Tab>
|
||||
<Tab>{t('common.details')}</Tab>
|
||||
<Tab>{t('common.linear')}</Tab>
|
||||
<Tab>JSON</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<WorkflowLinearTab />
|
||||
<WorkflowGeneralTab />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<WorkflowGeneralTab />
|
||||
<WorkflowLinearTab />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<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,
|
||||
SchedulerFieldValue,
|
||||
SDXLRefinerModelFieldValue,
|
||||
StatefulFieldValue,
|
||||
StringFieldValue,
|
||||
T2IAdapterModelFieldValue,
|
||||
VAEModelFieldValue,
|
||||
@ -36,6 +37,7 @@ import {
|
||||
zMainModelFieldValue,
|
||||
zSchedulerFieldValue,
|
||||
zSDXLRefinerModelFieldValue,
|
||||
zStatefulFieldValue,
|
||||
zStringFieldValue,
|
||||
zT2IAdapterModelFieldValue,
|
||||
zVAEModelFieldValue,
|
||||
@ -478,6 +480,9 @@ export const nodesSlice = createSlice({
|
||||
selectedEdgesChanged: (state, action: PayloadAction<string[]>) => {
|
||||
state.selectedEdges = action.payload;
|
||||
},
|
||||
fieldValueReset: (state, action: FieldValueAction<StatefulFieldValue>) => {
|
||||
fieldValueReducer(state, action, zStatefulFieldValue);
|
||||
},
|
||||
fieldStringValueChanged: (state, action: FieldValueAction<StringFieldValue>) => {
|
||||
fieldValueReducer(state, action, zStringFieldValue);
|
||||
},
|
||||
@ -760,6 +765,7 @@ export const {
|
||||
edgesChanged,
|
||||
edgesDeleted,
|
||||
edgeUpdated,
|
||||
fieldValueReset,
|
||||
fieldBoardValueChanged,
|
||||
fieldBooleanValueChanged,
|
||||
fieldColorValueChanged,
|
||||
@ -834,7 +840,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
|
||||
nodeIsOpenChanged,
|
||||
nodeLabelChanged,
|
||||
nodeNotesChanged,
|
||||
nodesChanged,
|
||||
nodesDeleted,
|
||||
nodeUseCacheChanged,
|
||||
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 {
|
||||
AnyNode,
|
||||
InvocationNodeEdge,
|
||||
@ -33,9 +33,16 @@ export type NodesState = {
|
||||
selectionMode: SelectionMode;
|
||||
};
|
||||
|
||||
export type WorkflowMode = 'edit' | 'view';
|
||||
export type FieldIdentifierWithValue = FieldIdentifier & {
|
||||
value: StatefulFieldValue;
|
||||
};
|
||||
|
||||
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'> & {
|
||||
_version: 1;
|
||||
isTouched: boolean;
|
||||
mode: WorkflowMode;
|
||||
originalExposedFieldValues: FieldIdentifierWithValue[];
|
||||
};
|
||||
|
||||
export type NodeTemplatesState = {
|
||||
|
@ -2,11 +2,16 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||
import type {
|
||||
FieldIdentifierWithValue,
|
||||
WorkflowMode,
|
||||
WorkflowsState as WorkflowState,
|
||||
} from 'features/nodes/store/types';
|
||||
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 { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
||||
import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
|
||||
|
||||
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
||||
name: '',
|
||||
@ -23,7 +28,9 @@ export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
||||
|
||||
export const initialWorkflowState: WorkflowState = {
|
||||
_version: 1,
|
||||
isTouched: true,
|
||||
isTouched: false,
|
||||
mode: 'view',
|
||||
originalExposedFieldValues: [],
|
||||
...blankWorkflow,
|
||||
};
|
||||
|
||||
@ -31,15 +38,25 @@ export const workflowSlice = createSlice({
|
||||
name: 'workflow',
|
||||
initialState: initialWorkflowState,
|
||||
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.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}`
|
||||
);
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowExposedFieldRemoved: (state, action: PayloadAction<FieldIdentifier>) => {
|
||||
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;
|
||||
},
|
||||
workflowExposedFieldsReordered: (state, action: PayloadAction<FieldIdentifier[]>) => {
|
||||
@ -82,15 +99,43 @@ export const workflowSlice = createSlice({
|
||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||
state.id = action.payload;
|
||||
},
|
||||
workflowReset: () => cloneDeep(initialWorkflowState),
|
||||
workflowSaved: (state) => {
|
||||
state.isTouched = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(workflowLoaded, (state, action) => {
|
||||
const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
return { ...initialWorkflowState, ...cloneDeep(workflowExtra) };
|
||||
const { nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
|
||||
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) => {
|
||||
@ -101,6 +146,29 @@ export const workflowSlice = createSlice({
|
||||
|
||||
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) => {
|
||||
state.isTouched = true;
|
||||
});
|
||||
@ -108,6 +176,7 @@ export const workflowSlice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
workflowModeChanged,
|
||||
workflowExposedFieldAdded,
|
||||
workflowExposedFieldRemoved,
|
||||
workflowExposedFieldsReordered,
|
||||
@ -120,7 +189,6 @@ export const {
|
||||
workflowVersionChanged,
|
||||
workflowContactChanged,
|
||||
workflowIDChanged,
|
||||
workflowReset,
|
||||
workflowSaved,
|
||||
} = workflowSlice.actions;
|
||||
|
||||
|
@ -101,9 +101,11 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
|
||||
const clearStorage = useClearStorage();
|
||||
|
||||
const handleOpenSettingsModel = useCallback(() => {
|
||||
refetchIntermediatesCount();
|
||||
if (shouldShowClearIntermediates) {
|
||||
refetchIntermediatesCount();
|
||||
}
|
||||
_onSettingsModalOpen();
|
||||
}, [_onSettingsModalOpen, refetchIntermediatesCount]);
|
||||
}, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]);
|
||||
|
||||
const handleClickResetWebUI = useCallback(() => {
|
||||
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 { memo } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
const NodesTab = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
const mode = useAppSelector((s) => s.workflow.mode);
|
||||
|
||||
if (mode === 'edit') {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<NodeEditor />
|
||||
</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);
|
||||
|
@ -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 { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBooksBold } from 'react-icons/pi';
|
||||
import { PiFolderOpenBold } from 'react-icons/pi';
|
||||
|
||||
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
|
||||
@ -15,7 +15,7 @@ const WorkflowLibraryButton = () => {
|
||||
<IconButton
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
tooltip={t('workflows.workflowLibrary')}
|
||||
icon={<PiBooksBold />}
|
||||
icon={<PiFolderOpenBold />}
|
||||
onClick={disclosure.onOpen}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
|
@ -1,60 +1,22 @@
|
||||
import { ConfirmationAlertDialog, Flex, MenuItem, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
|
||||
import { memo, useCallback } from 'react';
|
||||
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 dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const isTouched = useAppSelector((s) => s.workflow.isTouched);
|
||||
|
||||
const handleNewWorkflow = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
|
||||
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}>
|
||||
const renderButton = useCallback(
|
||||
(onClick: () => void) => (
|
||||
<MenuItem as="button" icon={<PiFilePlusBold />} onClick={onClick}>
|
||||
{t('nodes.newWorkflow')}
|
||||
</MenuItem>
|
||||
|
||||
<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>
|
||||
</>
|
||||
),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewWorkflowMenuItem);
|
||||
return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
|
||||
});
|
||||
|
||||
NewWorkflowMenuItem.displayName = 'NewWorkflowMenuItem';
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
useGlobalMenuClose,
|
||||
} from '@invoke-ai/ui-library';
|
||||
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 SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
|
||||
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
|
||||
|
@ -33,7 +33,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
# 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",
|
||||
"compel==2.0.2",
|
||||
"controlnet-aux==0.0.7",
|
||||
|
Loading…
Reference in New Issue
Block a user