mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): wip publish flow
This commit is contained in:
@@ -1785,7 +1785,16 @@
|
||||
"minimum": "Minimum",
|
||||
"maximum": "Maximum",
|
||||
"publish": "Publish",
|
||||
"selectOutputNode": "Select Output Node"
|
||||
"selectOutputNode": "Select Output Node",
|
||||
"cannotPublish": "Cannot publish workflow",
|
||||
"cannotPublishUnsavedWorkflow": "Workflow has unsaved changes",
|
||||
"cannotPublishWorkflowWithBatchOrGeneratorNodes": "Workflow contains batch and/or generator nodes",
|
||||
"cannotPublishInvalidWorkflow": "Workflow graph invalid (hover Invoke button for details)",
|
||||
"cannotPublishWorkflowWithoutOutputNode": "No output node selected",
|
||||
"publishFailed": "Publish failed",
|
||||
"publishFailedDesc": "There was a problem publishing the workflow. Please try again.",
|
||||
"publishSuccess": "Workflow publish started",
|
||||
"publishSuccessDesc": "Your workflow is being published. Check your Project Dashboard to see its progress."
|
||||
}
|
||||
},
|
||||
"controlLayers": {
|
||||
|
||||
@@ -65,8 +65,6 @@ const containerSx: SystemStyleObject = {
|
||||
shadow: '0 0 0 3px var(--invoke-colors-blue-300)',
|
||||
},
|
||||
'&[data-is-in-deploy-flow="true"]': {
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none',
|
||||
'& *': {
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none',
|
||||
@@ -108,7 +106,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
const { nodeId, width, children, selected } = props;
|
||||
const mouseOverNode = useMouseOverNode(nodeId);
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const zoomToNode = useZoomToNode();
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
const isInDeployFlow = useStore($isInDeployFlow);
|
||||
|
||||
const executionState = useNodeExecutionState(nodeId);
|
||||
@@ -137,9 +135,9 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
// This target is marked as not fitting the view on double click
|
||||
return;
|
||||
}
|
||||
zoomToNode(nodeId);
|
||||
zoomToNode();
|
||||
},
|
||||
[nodeId, zoomToNode]
|
||||
[zoomToNode]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $isInDeployFlow } from 'features/nodes/components/sidePanel/builder/deploy';
|
||||
import { EditModeLeftPanelContent } from 'features/nodes/components/sidePanel/EditModeLeftPanelContent';
|
||||
import { DeployWorkflowPanelContent } from 'features/nodes/components/sidePanel/workflow/DeployWorkflowPanelContent';
|
||||
import { PublishWorkflowPanelContent } from 'features/nodes/components/sidePanel/workflow/DeployWorkflowPanelContent';
|
||||
import { ActiveWorkflowDescription } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription';
|
||||
import { ActiveWorkflowNameAndActions } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowNameAndActions';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
@@ -17,7 +17,7 @@ const WorkflowsTabLeftPanel = () => {
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{isInDeployFlow && <DeployWorkflowPanelContent />}
|
||||
{isInDeployFlow && <PublishWorkflowPanelContent />}
|
||||
{!isInDeployFlow && <ActiveWorkflowNameAndActions />}
|
||||
{!isInDeployFlow && mode === 'view' && <ActiveWorkflowDescription />}
|
||||
{!isInDeployFlow && mode === 'view' && <ViewModeLeftPanelContent />}
|
||||
|
||||
@@ -67,11 +67,8 @@ FormElementEditModeHeader.displayName = 'FormElementEditModeHeader';
|
||||
const ZoomToNodeButton = memo(({ element }: { element: NodeFieldElement }) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId } = element.data.fieldIdentifier;
|
||||
const zoomToNode = useZoomToNode();
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const onClick = useCallback(() => {
|
||||
zoomToNode(nodeId);
|
||||
}, [nodeId, zoomToNode]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
@@ -79,7 +76,7 @@ const ZoomToNodeButton = memo(({ element }: { element: NodeFieldElement }) => {
|
||||
onMouseOut={mouseOverFormField.handleMouseOut}
|
||||
tooltip={t('workflows.builder.zoomToNode')}
|
||||
aria-label={t('workflows.builder.zoomToNode')}
|
||||
onClick={onClick}
|
||||
onClick={zoomToNode}
|
||||
icon={<PiGpsFixBold />}
|
||||
variant="link"
|
||||
size="sm"
|
||||
|
||||
@@ -39,7 +39,7 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
|
||||
return (
|
||||
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
|
||||
<NodeFieldElementEditModeContent dragHandleRef={dragHandleRef} el={el} isDragging={isDragging} />
|
||||
<NodeFieldElementOverlay element={el} />
|
||||
<NodeFieldElementOverlay nodeId={el.data.fieldIdentifier.nodeId} />
|
||||
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
|
||||
</Flex>
|
||||
);
|
||||
@@ -105,9 +105,9 @@ const nodeFieldOverlaySx: SystemStyleObject = {
|
||||
},
|
||||
};
|
||||
|
||||
const NodeFieldElementOverlay = memo(({ element }: { element: NodeFieldElement }) => {
|
||||
const mouseOverNode = useMouseOverNode(element.data.fieldIdentifier.nodeId);
|
||||
const mouseOverFormField = useMouseOverFormField(element.data.fieldIdentifier.nodeId);
|
||||
export const NodeFieldElementOverlay = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const mouseOverNode = useMouseOverNode(nodeId);
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -9,3 +9,8 @@ export const $isReadyToDoValidationRun = computed(
|
||||
return isInDeployFlow && outputNodeId !== null && !isSelectingOutputNode;
|
||||
}
|
||||
);
|
||||
export const resetPublishState = () => {
|
||||
$isInDeployFlow.set(false);
|
||||
$outputNodeId.set(null);
|
||||
$isSelectingOutputNode.set(false);
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $isInDeployFlow } from 'features/nodes/components/sidePanel/builder/deploy';
|
||||
import { selectIsWorkflowSaved } from 'features/nodes/store/workflowSlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLightningFill } from 'react-icons/pi';
|
||||
|
||||
export const DeployWorkflowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const deployWorkflowIsEnabled = useFeatureStatus('deployWorkflow');
|
||||
const isWorkflowSaved = useAppSelector(selectIsWorkflowSaved);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
$isInDeployFlow.set(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
leftIcon={<PiLightningFill />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={!deployWorkflowIsEnabled || !isWorkflowSaved}
|
||||
>
|
||||
{t('workflows.builder.publish')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
DeployWorkflowButton.displayName = 'DeployWorkflowButton';
|
||||
@@ -1,32 +1,48 @@
|
||||
import { Button, ButtonGroup, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import {
|
||||
$isInDeployFlow,
|
||||
$isReadyToDoValidationRun,
|
||||
$isSelectingOutputNode,
|
||||
$outputNodeId,
|
||||
resetPublishState,
|
||||
} from 'features/nodes/components/sidePanel/builder/deploy';
|
||||
import { NodeFieldElementOverlay } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
|
||||
import { useInputFieldTemplateTitleOrThrow } from 'features/nodes/hooks/useInputFieldTemplateTitleOrThrow';
|
||||
import { useInputFieldUserTitleOrThrow } from 'features/nodes/hooks/useInputFieldUserTitleOrThrow';
|
||||
import { useMouseOverFormField } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useNodeTemplateTitleOrThrow } from 'features/nodes/hooks/useNodeTemplateTitleOrThrow';
|
||||
import { useNodeUserTitleOrThrow } from 'features/nodes/hooks/useNodeUserTitleOrThrow';
|
||||
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
|
||||
import { selectNodeFieldElementsDeduped } from 'features/nodes/store/workflowSlice';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
|
||||
import { selectHasBatchOrGeneratorNodes } from 'features/nodes/store/selectors';
|
||||
import { selectIsWorkflowSaved, selectNodeFieldElementsDeduped } from 'features/nodes/store/workflowSlice';
|
||||
import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows';
|
||||
import { $isReadyToEnqueue } from 'features/queue/store/readiness';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiLightningFill } from 'react-icons/pi';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
export const DeployWorkflowPanelContent = memo(() => {
|
||||
const log = logger('generation');
|
||||
|
||||
export const PublishWorkflowPanelContent = memo(() => {
|
||||
const nodeFieldElements = useAppSelector(selectNodeFieldElementsDeduped);
|
||||
const outputNodeId = useStore($outputNodeId);
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<ButtonGroup isAttached={false}>
|
||||
<SelectOutputNodeButton />
|
||||
<DoValidationRunButton />
|
||||
<CancelDeployButton />
|
||||
<PublishWorkflowButton />
|
||||
<CancelPublishButton />
|
||||
</ButtonGroup>
|
||||
{outputNodeId !== null && <OutputNode outputNodeId={outputNodeId} />}
|
||||
<Flex flexDir="column" borderWidth={1}>
|
||||
@@ -39,7 +55,7 @@ export const DeployWorkflowPanelContent = memo(() => {
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
DeployWorkflowPanelContent.displayName = 'DeployWorkflowPanelContent';
|
||||
PublishWorkflowPanelContent.displayName = 'DeployWorkflowPanelContent';
|
||||
|
||||
const OutputNode = memo(({ outputNodeId }: { outputNodeId: string }) => {
|
||||
const resetOutputNode = useCallback(() => {
|
||||
@@ -75,7 +91,7 @@ const SelectOutputNodeButton = memo(() => {
|
||||
});
|
||||
SelectOutputNodeButton.displayName = 'SelectOutputNodeButton';
|
||||
|
||||
const CancelDeployButton = memo(() => {
|
||||
const CancelPublishButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const onClick = useCallback(() => {
|
||||
$isInDeployFlow.set(false);
|
||||
@@ -84,46 +100,189 @@ const CancelDeployButton = memo(() => {
|
||||
}, []);
|
||||
return <Button onClick={onClick}>{t('common.cancel')}</Button>;
|
||||
});
|
||||
CancelDeployButton.displayName = 'CancelDeployButton';
|
||||
CancelPublishButton.displayName = 'CancelDeployButton';
|
||||
|
||||
const DoValidationRunButton = memo(() => {
|
||||
const PublishWorkflowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isReadyToDoValidationRun = useStore($isReadyToDoValidationRun);
|
||||
const invoke = useInvoke();
|
||||
const onClick = useCallback(() => {
|
||||
invoke.enqueue(true, true);
|
||||
}, [invoke]);
|
||||
const isReadyToEnqueue = useStore($isReadyToEnqueue);
|
||||
const isWorkflowSaved = useAppSelector(selectIsWorkflowSaved);
|
||||
const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes);
|
||||
const outputNodeId = useStore($outputNodeId);
|
||||
const isSelectingOutputNode = useStore($isSelectingOutputNode);
|
||||
|
||||
const enqueue = useEnqueueWorkflows();
|
||||
const onClick = useCallback(async () => {
|
||||
const result = await withResultAsync(() => enqueue(true, true));
|
||||
if (result.isErr()) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('workflows.builder.publishFailed'),
|
||||
description: t('workflows.builder.publishFailedDesc'),
|
||||
});
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to enqueue batch');
|
||||
} else {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('workflows.builder.publishSuccess'),
|
||||
description: t('workflows.builder.publishSuccessDesc'),
|
||||
});
|
||||
resetPublishState();
|
||||
log.debug(parseify(result.value), 'Enqueued batch');
|
||||
}
|
||||
}, [enqueue, t]);
|
||||
|
||||
return (
|
||||
<Button isDisabled={!isReadyToDoValidationRun || invoke.isDisabled} onClick={onClick}>
|
||||
{t('workflows.builder.publish')}
|
||||
</Button>
|
||||
<PublishTooltip
|
||||
isWorkflowSaved={isWorkflowSaved}
|
||||
hasBatchOrGeneratorNodes={hasBatchOrGeneratorNodes}
|
||||
isReadyToEnqueue={isReadyToEnqueue}
|
||||
hasOutputNode={outputNodeId !== null && !isSelectingOutputNode}
|
||||
>
|
||||
<Button isDisabled={!isReadyToDoValidationRun || !isReadyToEnqueue} onClick={onClick}>
|
||||
{t('workflows.builder.publish')}
|
||||
</Button>
|
||||
</PublishTooltip>
|
||||
);
|
||||
});
|
||||
DoValidationRunButton.displayName = 'DoValidationRunButton';
|
||||
PublishWorkflowButton.displayName = 'DoValidationRunButton';
|
||||
|
||||
const NodeInputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => {
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow(nodeId);
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId);
|
||||
const fieldUserTitle = useInputFieldUserTitleOrThrow(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName);
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Flex
|
||||
flexDir="column"
|
||||
position="relative"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
onMouseOver={mouseOverFormField.handleMouseOver}
|
||||
onMouseOut={mouseOverFormField.handleMouseOut}
|
||||
onClick={zoomToNode}
|
||||
>
|
||||
<Text fontWeight="semibold">{`${nodeUserTitle || nodeTemplateTitle} -> ${fieldUserTitle || fieldTemplateTitle}`}</Text>
|
||||
<Text variant="subtext">{`${nodeId} -> ${fieldName}`}</Text>
|
||||
<NodeFieldElementOverlay nodeId={nodeId} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NodeInputFieldPreview.displayName = 'NodeInputFieldPreview';
|
||||
|
||||
const NodeOutputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => {
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow(nodeId);
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId);
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Flex
|
||||
flexDir="column"
|
||||
position="relative"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
onMouseOver={mouseOverFormField.handleMouseOver}
|
||||
onMouseOut={mouseOverFormField.handleMouseOut}
|
||||
onClick={zoomToNode}
|
||||
>
|
||||
<Text fontWeight="semibold">{`${nodeUserTitle || nodeTemplateTitle} -> ${fieldTemplate.title}`}</Text>
|
||||
<Text variant="subtext">{`${nodeId} -> ${fieldName}`}</Text>
|
||||
<NodeFieldElementOverlay nodeId={nodeId} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NodeOutputFieldPreview.displayName = 'NodeOutputFieldPreview';
|
||||
|
||||
export const StartPublishFlowButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const deployWorkflowIsEnabled = useFeatureStatus('deployWorkflow');
|
||||
const isReadyToEnqueue = useStore($isReadyToEnqueue);
|
||||
const isWorkflowSaved = useAppSelector(selectIsWorkflowSaved);
|
||||
const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
$isInDeployFlow.set(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PublishTooltip
|
||||
isWorkflowSaved={isWorkflowSaved}
|
||||
hasBatchOrGeneratorNodes={hasBatchOrGeneratorNodes}
|
||||
isReadyToEnqueue={isReadyToEnqueue}
|
||||
hasOutputNode={true}
|
||||
>
|
||||
<Button
|
||||
onClick={onClick}
|
||||
leftIcon={<PiLightningFill />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={!deployWorkflowIsEnabled || !isWorkflowSaved || hasBatchOrGeneratorNodes}
|
||||
>
|
||||
{t('workflows.builder.publish')}
|
||||
</Button>
|
||||
</PublishTooltip>
|
||||
);
|
||||
});
|
||||
|
||||
StartPublishFlowButton.displayName = 'StartPublishFlowButton';
|
||||
|
||||
const PublishTooltip = memo(
|
||||
({
|
||||
isWorkflowSaved,
|
||||
hasBatchOrGeneratorNodes,
|
||||
isReadyToEnqueue,
|
||||
hasOutputNode,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
isWorkflowSaved: boolean;
|
||||
hasBatchOrGeneratorNodes: boolean;
|
||||
isReadyToEnqueue: boolean;
|
||||
hasOutputNode: boolean;
|
||||
}>) => {
|
||||
const { t } = useTranslation();
|
||||
const problems = useMemo(() => {
|
||||
const _problems: string[] = [];
|
||||
if (!isWorkflowSaved) {
|
||||
_problems.push(t('workflows.builder.cannotPublishUnsavedWorkflow'));
|
||||
}
|
||||
if (hasBatchOrGeneratorNodes) {
|
||||
_problems.push(t('workflows.builder.cannotPublishWorkflowWithBatchOrGeneratorNodes'));
|
||||
}
|
||||
if (!isReadyToEnqueue) {
|
||||
_problems.push(t('workflows.builder.cannotPublishInvalidWorkflow'));
|
||||
}
|
||||
if (!hasOutputNode) {
|
||||
_problems.push(t('workflows.builder.cannotPublishWorkflowWithoutOutputNode'));
|
||||
}
|
||||
return _problems;
|
||||
}, [hasBatchOrGeneratorNodes, hasOutputNode, isReadyToEnqueue, isWorkflowSaved, t]);
|
||||
|
||||
if (problems.length === 0) {
|
||||
return children;
|
||||
// return t('workflows.builder.publish');
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
<Flex flexDir="column">
|
||||
<Text>{t('workflows.builder.cannotPublish')}:</Text>
|
||||
<UnorderedList>
|
||||
{problems.map((problem, index) => (
|
||||
<ListItem key={index}>{problem}</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
PublishTooltip.displayName = 'PublishTooltip';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { WorkflowBuilder } from 'features/nodes/components/sidePanel/builder/WorkflowBuilder';
|
||||
import { DeployWorkflowButton } from 'features/nodes/components/sidePanel/workflow/DeployWorkflowButton';
|
||||
import { StartPublishFlowButton } from 'features/nodes/components/sidePanel/workflow/DeployWorkflowPanelContent';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -18,7 +18,7 @@ const WorkflowFieldsLinearViewPanel = () => {
|
||||
<Tab>{t('common.details')}</Tab>
|
||||
<Tab>JSON</Tab>
|
||||
<Spacer />
|
||||
{deployWorkflowIsEnabled && <DeployWorkflowButton />}
|
||||
{deployWorkflowIsEnabled && <StartPublishFlowButton />}
|
||||
</TabList>
|
||||
|
||||
<TabPanels h="full" pt={2}>
|
||||
|
||||
@@ -4,14 +4,14 @@ import { useCallback } from 'react';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
export const useZoomToNode = () => {
|
||||
const zoomToNode = useCallback((nodeId: string) => {
|
||||
export const useZoomToNode = (nodeId: string) => {
|
||||
const zoomToNode = useCallback(() => {
|
||||
const flow = $flow.get();
|
||||
if (!flow) {
|
||||
log.warn('No flow instance found, cannot zoom to node');
|
||||
return;
|
||||
}
|
||||
flow.fitView({ duration: 300, maxZoom: 1.5, nodes: [{ id: nodeId }] });
|
||||
}, []);
|
||||
}, [nodeId]);
|
||||
return zoomToNode;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { RootState } from 'app/store/store';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import type { AnyNode, InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { isBatchNode, isGeneratorNode, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const selectNode = (nodesSlice: NodesState, nodeId: string): AnyNode => {
|
||||
@@ -81,3 +81,7 @@ export const selectMayRedo = createSelector(
|
||||
(state: RootState) => state.nodes,
|
||||
(nodes) => nodes.future.length > 0
|
||||
);
|
||||
|
||||
export const selectHasBatchOrGeneratorNodes = createSelector(selectNodes, (nodes) =>
|
||||
nodes.filter(isInvocationNode).some((node) => isBatchNode(node) || isGeneratorNode(node))
|
||||
);
|
||||
|
||||
@@ -100,7 +100,7 @@ export const isGeneratorNodeType = (type: string) =>
|
||||
|
||||
export const isBatchNode = (node: InvocationNode) => isBatchNodeType(node.data.type);
|
||||
|
||||
const isGeneratorNode = (node: InvocationNode) => isGeneratorNodeType(node.data.type);
|
||||
export const isGeneratorNode = (node: InvocationNode) => isGeneratorNodeType(node.data.type);
|
||||
|
||||
export const isExecutableNode = (node: InvocationNode) => {
|
||||
return !isBatchNode(node) && !isGeneratorNode(node);
|
||||
|
||||
@@ -37,7 +37,7 @@ const getBoardField = (field: BoardFieldInputInstance, state: RootState): BoardF
|
||||
/**
|
||||
* Builds a graph from the node editor state.
|
||||
*/
|
||||
export const buildNodesGraph = (state: RootState, templates: Templates): Graph => {
|
||||
export const buildNodesGraph = (state: RootState, templates: Templates): Required<Graph> => {
|
||||
const { nodes, edges } = selectNodesSlice(state);
|
||||
|
||||
// Exclude all batch nodes - we will handle these in the batch setup in a diff function
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { $outputNodeId } from 'features/nodes/components/sidePanel/builder/deploy';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectNodeFieldElementsDeduped } from 'features/nodes/store/workflowSlice';
|
||||
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
|
||||
import { resolveBatchValue } from 'features/nodes/util/node/resolveBatchValue';
|
||||
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { useCallback } from 'react';
|
||||
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
|
||||
import type { Batch, EnqueueBatchArg } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useEnqueueWorkflows = () => {
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const enqueue = useCallback(
|
||||
async (prepend: boolean, isApiValidationRun: boolean) => {
|
||||
const state = getState();
|
||||
const nodesState = selectNodesSlice(state);
|
||||
const workflow = state.workflow;
|
||||
const templates = $templates.get();
|
||||
const graph = buildNodesGraph(state, templates);
|
||||
const builtWorkflow = buildWorkflowWithValidation({
|
||||
nodes: nodesState.nodes,
|
||||
edges: nodesState.edges,
|
||||
workflow,
|
||||
});
|
||||
|
||||
if (builtWorkflow) {
|
||||
// embedded workflows don't have an id
|
||||
delete builtWorkflow.id;
|
||||
}
|
||||
|
||||
const data: Batch['data'] = [];
|
||||
|
||||
const invocationNodes = nodesState.nodes.filter(isInvocationNode);
|
||||
const batchNodes = invocationNodes.filter(isBatchNode);
|
||||
|
||||
// Handle zipping batch nodes. First group the batch nodes by their batch_group_id
|
||||
const groupedBatchNodes = groupBy(batchNodes, (node) => node.data.inputs['batch_group_id']?.value);
|
||||
|
||||
// Then, we will create a batch data collection item for each group
|
||||
for (const [batchGroupId, batchNodes] of Object.entries(groupedBatchNodes)) {
|
||||
const zippedBatchDataCollectionItems: NonNullable<Batch['data']>[number] = [];
|
||||
|
||||
for (const node of batchNodes) {
|
||||
const value = await resolveBatchValue({ nodesState, node, dispatch });
|
||||
const sourceHandle = node.data.type === 'image_batch' ? 'image' : 'value';
|
||||
const edgesFromBatch = nodesState.edges.filter(
|
||||
(e) => e.source === node.id && e.sourceHandle === sourceHandle
|
||||
);
|
||||
if (batchGroupId !== 'None') {
|
||||
// If this batch node has a batch_group_id, we will zip the data collection items
|
||||
for (const edge of edgesFromBatch) {
|
||||
if (!edge.targetHandle) {
|
||||
break;
|
||||
}
|
||||
zippedBatchDataCollectionItems.push({
|
||||
node_path: edge.target,
|
||||
field_name: edge.targetHandle,
|
||||
items: value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Otherwise add the data collection items to root of the batch so they are not zipped
|
||||
const productBatchDataCollectionItems: NonNullable<Batch['data']>[number] = [];
|
||||
for (const edge of edgesFromBatch) {
|
||||
if (!edge.targetHandle) {
|
||||
break;
|
||||
}
|
||||
productBatchDataCollectionItems.push({
|
||||
node_path: edge.target,
|
||||
field_name: edge.targetHandle,
|
||||
items: value,
|
||||
});
|
||||
}
|
||||
if (productBatchDataCollectionItems.length > 0) {
|
||||
data.push(productBatchDataCollectionItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if this batch data collection item has any items, add it to the data array
|
||||
if (batchGroupId !== 'None' && zippedBatchDataCollectionItems.length > 0) {
|
||||
data.push(zippedBatchDataCollectionItems);
|
||||
}
|
||||
}
|
||||
|
||||
const batchConfig: EnqueueBatchArg = {
|
||||
batch: {
|
||||
graph,
|
||||
workflow: builtWorkflow,
|
||||
runs: state.params.iterations,
|
||||
origin: 'workflows',
|
||||
destination: 'gallery',
|
||||
data,
|
||||
},
|
||||
prepend,
|
||||
};
|
||||
|
||||
if (isApiValidationRun) {
|
||||
// Derive the input fields from the builder's selected node field elements
|
||||
const nodeFieldElements = selectNodeFieldElementsDeduped(state);
|
||||
const api_input_fields = nodeFieldElements.map((el) => {
|
||||
const { nodeId, fieldName } = el.data.fieldIdentifier;
|
||||
return {
|
||||
kind: 'input',
|
||||
node_id: nodeId,
|
||||
field_name: fieldName,
|
||||
} as const;
|
||||
});
|
||||
|
||||
// Derive the output fields from the builder's selected output node
|
||||
const outputNodeId = $outputNodeId.get();
|
||||
assert(outputNodeId !== null, 'Output node not selected');
|
||||
const outputNodeType = selectNodeData(selectNodesSlice(state), outputNodeId).type;
|
||||
const outputNodeTemplate = templates[outputNodeType];
|
||||
assert(outputNodeTemplate, `Template for node type ${outputNodeType} not found`);
|
||||
const outputFieldNames = Object.keys(outputNodeTemplate.outputs);
|
||||
const api_output_fields = outputFieldNames.map((fieldName) => {
|
||||
return {
|
||||
kind: 'output',
|
||||
node_id: outputNodeId,
|
||||
field_name: fieldName,
|
||||
} as const;
|
||||
});
|
||||
|
||||
batchConfig.is_api_validation_run = true;
|
||||
batchConfig.api_input_fields = api_input_fields;
|
||||
batchConfig.api_output_fields = api_output_fields;
|
||||
|
||||
// If the batch is an API validation run, we only want to run it once
|
||||
batchConfig.batch.runs = 1;
|
||||
}
|
||||
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, { ...enqueueMutationFixedCacheKeyOptions, track: false })
|
||||
);
|
||||
|
||||
const enqueueResult = await req.unwrap();
|
||||
return { batchConfig, enqueueResult };
|
||||
},
|
||||
[dispatch, getState]
|
||||
);
|
||||
|
||||
return enqueue;
|
||||
};
|
||||
@@ -1,29 +1,40 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { enqueueRequestedCanvas } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
||||
import { enqueueRequestedWorkflows } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
|
||||
import { enqueueRequestedUpscaling } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows';
|
||||
import { $isReadyToEnqueue } from 'features/queue/store/readiness';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback } from 'react';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue';
|
||||
|
||||
const log = logger('generation');
|
||||
|
||||
export const useInvoke = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const tabName = useAppSelector(selectActiveTab);
|
||||
const isReady = useStore($isReadyToEnqueue);
|
||||
const enqueueWorkflows = useEnqueueWorkflows();
|
||||
|
||||
const [_, { isLoading }] = useEnqueueBatchMutation(enqueueMutationFixedCacheKeyOptions);
|
||||
|
||||
const enqueue = useCallback(
|
||||
(prepend: boolean, isApiValidationRun: boolean) => {
|
||||
async (prepend: boolean, isApiValidationRun: boolean) => {
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabName === 'workflows') {
|
||||
dispatch(enqueueRequestedWorkflows({ prepend, isApiValidationRun }));
|
||||
return;
|
||||
const result = await withResultAsync(() => enqueueWorkflows(prepend, isApiValidationRun));
|
||||
if (result.isErr()) {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to enqueue batch');
|
||||
} else {
|
||||
log.debug(parseify(result.value), 'Enqueued batch');
|
||||
}
|
||||
}
|
||||
|
||||
if (tabName === 'upscaling') {
|
||||
@@ -38,7 +49,7 @@ export const useInvoke = () => {
|
||||
|
||||
// Else we are not on a generation tab and should not queue
|
||||
},
|
||||
[dispatch, isReady, tabName]
|
||||
[dispatch, enqueueWorkflows, isReady, tabName]
|
||||
);
|
||||
|
||||
const enqueueBack = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user