Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal';
import { CanvasWorkflowIntegrationModal } from 'features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { CropImageModal } from 'features/cropper/components/CropImageModal';
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
Expand Down Expand Up @@ -51,6 +52,7 @@ export const GlobalModalIsolator = memo(() => {
<SaveWorkflowAsDialog />
<CanvasManagerProviderGate>
<CanvasPasteModal />
<CanvasWorkflowIntegrationModal />
</CanvasManagerProviderGate>
<LoadWorkflowFromGraphModal />
<CropImageModal />
Expand Down
1 change: 1 addition & 0 deletions invokeai/frontend/web/src/app/logging/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));

export const zLogNamespace = z.enum([
'canvas',
'canvas-workflow-integration',
'config',
'dnd',
'events',
Expand Down
3 changes: 3 additions & 0 deletions invokeai/frontend/web/src/app/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/sli
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasWorkflowIntegrationSliceConfig } from 'features/controlLayers/store/canvasWorkflowIntegrationSlice';
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice';
Expand Down Expand Up @@ -62,6 +63,7 @@ const SLICE_CONFIGS = {
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig,
[canvasSliceConfig.slice.reducerPath]: canvasSliceConfig,
[canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig,
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig,
Expand Down Expand Up @@ -91,6 +93,7 @@ const ALL_REDUCERS = {
canvasSliceConfig.slice.reducer,
canvasSliceConfig.undoableConfig?.reduxUndoOptions
),
[canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig.slice.reducer,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Button,
ButtonGroup,
Flex,
Heading,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spacer,
Spinner,
Text,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
canvasWorkflowIntegrationClosed,
selectCanvasWorkflowIntegrationIsOpen,
selectCanvasWorkflowIntegrationIsProcessing,
selectCanvasWorkflowIntegrationSelectedWorkflowId,
} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

import { CanvasWorkflowIntegrationParameterPanel } from './CanvasWorkflowIntegrationParameterPanel';
import { CanvasWorkflowIntegrationWorkflowSelector } from './CanvasWorkflowIntegrationWorkflowSelector';
import { useCanvasWorkflowIntegrationExecute } from './useCanvasWorkflowIntegrationExecute';

export const CanvasWorkflowIntegrationModal = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const isOpen = useAppSelector(selectCanvasWorkflowIntegrationIsOpen);
const isProcessing = useAppSelector(selectCanvasWorkflowIntegrationIsProcessing);
const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId);

const { execute, canExecute } = useCanvasWorkflowIntegrationExecute();

const onClose = useCallback(() => {
if (!isProcessing) {
dispatch(canvasWorkflowIntegrationClosed());
}
}, [dispatch, isProcessing]);

const onExecute = useCallback(() => {
execute();
}, [execute]);

return (
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Heading size="md">{t('controlLayers.workflowIntegration.title', 'Run Workflow on Canvas')}</Heading>
</ModalHeader>
<ModalCloseButton isDisabled={isProcessing} />

<ModalBody>
<Flex direction="column" gap={4}>
<Text fontSize="sm" color="base.400">
{t(
'controlLayers.workflowIntegration.description',
'Select a workflow with Form Builder and an image parameter to run on the current canvas layer. You can adjust parameters before executing. The result will be added back to the canvas.'
)}
</Text>

<CanvasWorkflowIntegrationWorkflowSelector />

{selectedWorkflowId && <CanvasWorkflowIntegrationParameterPanel />}
</Flex>
</ModalBody>

<ModalFooter>
<ButtonGroup>
<Button variant="ghost" onClick={onClose} isDisabled={isProcessing}>
{t('common.cancel')}
</Button>
<Spacer />
<Button
onClick={onExecute}
isDisabled={!canExecute || isProcessing}
loadingText={t('controlLayers.workflowIntegration.executing', 'Executing...')}
>
{isProcessing && <Spinner size="sm" mr={2} />}
{t('controlLayers.workflowIntegration.execute', 'Execute Workflow')}
</Button>
</ButtonGroup>
</ModalFooter>
</ModalContent>
</Modal>
);
});

CanvasWorkflowIntegrationModal.displayName = 'CanvasWorkflowIntegrationModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Box } from '@invoke-ai/ui-library';
import { WorkflowFormPreview } from 'features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFormPreview';
import { memo } from 'react';

export const CanvasWorkflowIntegrationParameterPanel = memo(() => {
return (
<Box w="full">
<WorkflowFormPreview />
</Box>
);
});

CanvasWorkflowIntegrationParameterPanel.displayName = 'CanvasWorkflowIntegrationParameterPanel';
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Flex, FormControl, FormLabel, Select, Spinner, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
canvasWorkflowIntegrationWorkflowSelected,
selectCanvasWorkflowIntegrationSelectedWorkflowId,
} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows';

import { useFilteredWorkflows } from './useFilteredWorkflows';

export const CanvasWorkflowIntegrationWorkflowSelector = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId);
const { data: workflowsData, isLoading } = useListWorkflowsInfiniteInfiniteQuery(
{
per_page: 100, // Get a reasonable number of workflows
page: 0,
},
{
selectFromResult: ({ data, isLoading }) => ({
data,
isLoading,
}),
}
);

const workflows = useMemo(() => {
if (!workflowsData) {
return [];
}
// Flatten all pages into a single list
return workflowsData.pages.flatMap((page) => page.items);
}, [workflowsData]);

// Filter workflows to only show those with ImageFields
const { filteredWorkflows, isFiltering } = useFilteredWorkflows(workflows);

const onChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const workflowId = e.target.value || null;
dispatch(canvasWorkflowIntegrationWorkflowSelected({ workflowId }));
},
[dispatch]
);

if (isLoading || isFiltering) {
return (
<Flex alignItems="center" gap={2}>
<Spinner size="sm" />
<Text>
{isFiltering
? t('controlLayers.workflowIntegration.filteringWorkflows', 'Filtering workflows...')
: t('controlLayers.workflowIntegration.loadingWorkflows', 'Loading workflows...')}
</Text>
</Flex>
);
}

if (filteredWorkflows.length === 0) {
return (
<Text color="warning.400" fontSize="sm">
{workflows.length === 0
? t('controlLayers.workflowIntegration.noWorkflowsFound', 'No workflows found.')
: t(
'controlLayers.workflowIntegration.noWorkflowsWithImageField',
'No workflows with Form Builder and image input fields found. Create a workflow with the Form Builder and add an image field.'
)}
</Text>
);
}

return (
<FormControl>
<FormLabel>{t('controlLayers.workflowIntegration.selectWorkflow', 'Select Workflow')}</FormLabel>
<Select
placeholder={t('controlLayers.workflowIntegration.selectPlaceholder', 'Choose a workflow...')}
value={selectedWorkflowId || ''}
onChange={onChange}
>
{filteredWorkflows.map((workflow) => (
<option key={workflow.workflow_id} value={workflow.workflow_id}>
{workflow.name || t('controlLayers.workflowIntegration.unnamedWorkflow', 'Unnamed Workflow')}
</option>
))}
</Select>
</FormControl>
);
});

CanvasWorkflowIntegrationWorkflowSelector.displayName = 'CanvasWorkflowIntegrationWorkflowSelector';
Loading
Loading