This commit is contained in:
psychedelicious
2025-03-26 15:55:24 +10:00
parent 60f0c4bf99
commit 0b8f88e554
8 changed files with 96 additions and 5 deletions

View File

@@ -1783,7 +1783,8 @@
"textPlaceholder": "Empty Text",
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.",
"minimum": "Minimum",
"maximum": "Maximum"
"maximum": "Maximum",
"deploy": "Deploy"
}
},
"controlLayers": {

View File

@@ -28,7 +28,8 @@ export type AppFeature =
| 'starterModels'
| 'hfToken'
| 'retryQueueItem'
| 'cancelAndClearAll';
| 'cancelAndClearAll'
| 'deployWorkflow';
/**
* A disable-able Stable Diffusion feature
*/

View File

@@ -1,4 +1,7 @@
import { Box } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isDeploying } from 'features/nodes/components/sidePanel/builder/deploy';
import { DeployWorkflowPanelContent } from 'features/nodes/components/sidePanel/workflow/DeployWorkflowPanelContent';
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
import type { CSSProperties } from 'react';
import { memo, useCallback, useRef } from 'react';
@@ -20,6 +23,8 @@ export const EditModeLeftPanelContent = memo(() => {
panelGroupRef.current.setLayout([50, 50]);
}, []);
const isDeploying = useStore($isDeploying);
return (
<Box position="relative" w="full" h="full">
<PanelGroup
@@ -30,7 +35,8 @@ export const EditModeLeftPanelContent = memo(() => {
style={panelGroupStyles}
>
<Panel id="workflow" collapsible minSize={25}>
<WorkflowFieldsLinearViewPanel />
{!isDeploying && <WorkflowFieldsLinearViewPanel />}
{isDeploying && <DeployWorkflowPanelContent />}
</Panel>
<HorizontalResizeHandle onDoubleClick={handleDoubleClickHandle} />
<Panel id="inspector" collapsible minSize={25}>

View File

@@ -0,0 +1,4 @@
import { atom } from 'nanostores';
export const $isDeploying = atom(false);
export const $outputNodeId = atom<string | null>(null);

View File

@@ -0,0 +1,32 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { $isDeploying } 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(() => {
$isDeploying.set(true);
}, []);
return (
<Button
onClick={onClick}
leftIcon={<PiLightningFill />}
variant="ghost"
size="sm"
isDisabled={!deployWorkflowIsEnabled || !isWorkflowSaved}
>
{t('workflows.builder.deploy')}
</Button>
);
});
DeployWorkflowButton.displayName = 'DeployWorkflowButton';

View File

@@ -0,0 +1,36 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useInputFieldTemplateTitleOrThrow } from 'features/nodes/hooks/useInputFieldTemplateTitleOrThrow';
import { useInputFieldUserTitleOrThrow } from 'features/nodes/hooks/useInputFieldUserTitleOrThrow';
import { useNodeTemplateTitleOrThrow } from 'features/nodes/hooks/useNodeTemplateTitleOrThrow';
import { useNodeUserTitleOrThrow } from 'features/nodes/hooks/useNodeUserTitleOrThrow';
import { selectNodeFieldElementsDeduped } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
export const DeployWorkflowPanelContent = memo(() => {
const nodeFieldElements = useAppSelector(selectNodeFieldElementsDeduped);
return (
<Flex flexDir="column">
{nodeFieldElements.map((el) => {
const { nodeId, fieldName } = el.data.fieldIdentifier;
return <NodeFieldPreview key={`${nodeId}-${fieldName}`} nodeId={nodeId} fieldName={fieldName} />;
})}
</Flex>
);
});
DeployWorkflowPanelContent.displayName = 'DeployWorkflowPanelContent';
const NodeFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => {
const nodeUserTitle = useNodeUserTitleOrThrow(nodeId);
const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId);
const fieldUserTitle = useInputFieldUserTitleOrThrow(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName);
return (
<Flex>
<Text>{`${nodeUserTitle || nodeTemplateTitle} -> ${fieldUserTitle || fieldTemplateTitle}`}</Text>
<Text>{`${nodeId} -> ${fieldName}`}</Text>
<Text></Text>
</Flex>
);
});
NodeFieldPreview.displayName = 'NodeFieldPreview';

View File

@@ -1,5 +1,7 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
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 { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,12 +10,15 @@ import WorkflowJSONTab from './WorkflowJSONTab';
const WorkflowFieldsLinearViewPanel = () => {
const { t } = useTranslation();
const deployWorkflowIsEnabled = useFeatureStatus('deployWorkflow');
return (
<Tabs variant="enclosed" display="flex" w="full" h="full" flexDir="column">
<TabList>
<Tab>{t('workflows.builder.builder')}</Tab>
<Tab>{t('common.details')}</Tab>
<Tab>JSON</Tab>
<Spacer />
{deployWorkflowIsEnabled && <DeployWorkflowButton />}
</TabList>
<TabPanels h="full" pt={2}>

View File

@@ -33,7 +33,7 @@ import {
isNodeFieldElement,
isTextElement,
} from 'features/nodes/types/workflow';
import { isEqual } from 'lodash-es';
import { isEqual, uniqBy } from 'lodash-es';
import { useMemo } from 'react';
import { selectNodesSlice } from './selectors';
@@ -350,6 +350,9 @@ export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.
export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched);
export const selectWorkflowDescription = createWorkflowSelector((workflow) => workflow.description);
export const selectWorkflowForm = createWorkflowSelector((workflow) => workflow.form);
export const selectIsWorkflowSaved = createSelector(selectWorkflowId, selectWorkflowIsTouched, (id, isTouched) => {
return id !== undefined && !isTouched;
});
export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflowSlice], (nodes, workflow) => {
const noNodes = !nodes.nodes.length;
@@ -375,6 +378,9 @@ export const selectFormInitialValues = createWorkflowSelector((workflow) => work
export const selectNodeFieldElements = createWorkflowSelector((workflow) =>
Object.values(workflow.form.elements).filter(isNodeFieldElement)
);
export const selectNodeFieldElementsDeduped = createSelector(selectNodeFieldElements, (nodeFieldElements) =>
uniqBy(nodeFieldElements, (el) => `${el.data.fieldIdentifier.nodeId}-${el.data.fieldIdentifier.fieldName}`)
);
const buildSelectElement = (id: string) => createWorkflowSelector((workflow) => workflow.form?.elements[id]);
export const useElement = (id: string): FormElement | undefined => {
const selector = useMemo(() => buildSelectElement(id), [id]);