mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): updated workflow handling (WIP)
Clientside updates for the backend workflow changes. Includes roughed-out workflow library UI.
This commit is contained in:
parent
a514c9e28b
commit
0b079df4ae
@ -161,7 +161,9 @@
|
|||||||
"txt2img": "Text To Image",
|
"txt2img": "Text To Image",
|
||||||
"unifiedCanvas": "Unified Canvas",
|
"unifiedCanvas": "Unified Canvas",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"prevPage": "Previous Page",
|
||||||
|
"nextPage": "Next Page"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
@ -1599,5 +1601,12 @@
|
|||||||
"showIntermediates": "Show Intermediates",
|
"showIntermediates": "Show Intermediates",
|
||||||
"snapToGrid": "Snap to Grid",
|
"snapToGrid": "Snap to Grid",
|
||||||
"undo": "Undo"
|
"undo": "Undo"
|
||||||
|
},
|
||||||
|
"workflows": {
|
||||||
|
"workflowLibrary": "Workflow Library",
|
||||||
|
"userCategory": "User",
|
||||||
|
"systemCategory": "System",
|
||||||
|
"loadWorkflow": "$t(nodes.loadWorkflow)",
|
||||||
|
"deleteWorkflow": "Delete Workflow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
|
|||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import { BatchConfig } from 'services/api/types';
|
import { BatchConfig } from 'services/api/types';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
|
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
||||||
|
|
||||||
export const addEnqueueRequestedNodes = () => {
|
export const addEnqueueRequestedNodes = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -11,9 +12,11 @@ export const addEnqueueRequestedNodes = () => {
|
|||||||
effect: async (action, { getState, dispatch }) => {
|
effect: async (action, { getState, dispatch }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const graph = buildNodesGraph(state.nodes);
|
const graph = buildNodesGraph(state.nodes);
|
||||||
|
const workflow = buildWorkflow(state.nodes);
|
||||||
const batchConfig: BatchConfig = {
|
const batchConfig: BatchConfig = {
|
||||||
batch: {
|
batch: {
|
||||||
graph,
|
graph,
|
||||||
|
workflow,
|
||||||
runs: state.generation.iterations,
|
runs: state.generation.iterations,
|
||||||
},
|
},
|
||||||
prepend: action.payload.prepend,
|
prepend: action.payload.prepend,
|
||||||
|
@ -99,7 +99,6 @@ export const addWorkflowLoadRequestedListener = () => {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Some other error occurred
|
// Some other error occurred
|
||||||
console.log(e);
|
|
||||||
log.error(
|
log.error(
|
||||||
{ error: parseify(e) },
|
{ error: parseify(e) },
|
||||||
t('nodes.unknownErrorValidatingWorkflow')
|
t('nodes.unknownErrorValidatingWorkflow')
|
||||||
|
@ -16,6 +16,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
|
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||||
|
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||||
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
@ -39,12 +41,12 @@ import {
|
|||||||
FaSeedling,
|
FaSeedling,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6';
|
import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6';
|
||||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
import {
|
||||||
|
useGetImageDTOQuery,
|
||||||
|
useLazyGetImageWorkflowQuery,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
|
||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
|
||||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
@ -111,18 +113,17 @@ const CurrentImageButtons = () => {
|
|||||||
lastSelectedImage?.image_name
|
lastSelectedImage?.image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow(
|
const [getWorkflow, getWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||||
lastSelectedImage?.workflow_id
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleLoadWorkflow = useCallback(() => {
|
const handleLoadWorkflow = useCallback(() => {
|
||||||
if (!workflow) {
|
if (!lastSelectedImage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(workflowLoadRequested(workflow));
|
getWorkflow(lastSelectedImage?.image_name).then((workflow) => {
|
||||||
}, [dispatch, workflow]);
|
dispatch(workflowLoadRequested(workflow.data));
|
||||||
|
});
|
||||||
|
}, [dispatch, getWorkflow, lastSelectedImage]);
|
||||||
|
|
||||||
useHotkeys('w', handleLoadWorkflow, [workflow]);
|
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
||||||
|
|
||||||
const handleClickUseAllParameters = useCallback(() => {
|
const handleClickUseAllParameters = useCallback(() => {
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
@ -255,12 +256,12 @@ const CurrentImageButtons = () => {
|
|||||||
|
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
isLoading={isLoadingWorkflow}
|
|
||||||
icon={<FaCircleNodes />}
|
icon={<FaCircleNodes />}
|
||||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||||
isDisabled={!workflow}
|
isDisabled={!imageDTO?.has_workflow}
|
||||||
onClick={handleLoadWorkflow}
|
onClick={handleLoadWorkflow}
|
||||||
|
isLoading={getWorkflowResult.isLoading}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
isLoading={isLoadingMetadata}
|
isLoading={isLoadingMetadata}
|
||||||
|
@ -3,17 +3,21 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||||
import {
|
import {
|
||||||
imagesToChangeSelected,
|
imagesToChangeSelected,
|
||||||
isModalOpenChanged,
|
isModalOpenChanged,
|
||||||
} from 'features/changeBoardModal/store/slice';
|
} from 'features/changeBoardModal/store/slice';
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
|
import {
|
||||||
|
sentImageToCanvas,
|
||||||
|
sentImageToImg2Img,
|
||||||
|
} from 'features/gallery/store/actions';
|
||||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
@ -32,16 +36,12 @@ import {
|
|||||||
import { FaCircleNodes } from 'react-icons/fa6';
|
import { FaCircleNodes } from 'react-icons/fa6';
|
||||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||||
import {
|
import {
|
||||||
|
useLazyGetImageWorkflowQuery,
|
||||||
useStarImagesMutation,
|
useStarImagesMutation,
|
||||||
useUnstarImagesMutation,
|
useUnstarImagesMutation,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import {
|
|
||||||
sentImageToCanvas,
|
|
||||||
sentImageToImg2Img,
|
|
||||||
} from 'features/gallery/store/actions';
|
|
||||||
|
|
||||||
type SingleSelectionMenuItemsProps = {
|
type SingleSelectionMenuItemsProps = {
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO;
|
||||||
@ -61,9 +61,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(
|
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(
|
||||||
imageDTO?.image_name
|
imageDTO?.image_name
|
||||||
);
|
);
|
||||||
const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow(
|
|
||||||
imageDTO?.workflow_id
|
const [getWorkflow, getWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||||
);
|
const handleLoadWorkflow = useCallback(() => {
|
||||||
|
getWorkflow(imageDTO.image_name).then((workflow) => {
|
||||||
|
dispatch(workflowLoadRequested(workflow.data));
|
||||||
|
});
|
||||||
|
}, [dispatch, getWorkflow, imageDTO]);
|
||||||
|
|
||||||
const [starImages] = useStarImagesMutation();
|
const [starImages] = useStarImagesMutation();
|
||||||
const [unstarImages] = useUnstarImagesMutation();
|
const [unstarImages] = useUnstarImagesMutation();
|
||||||
@ -101,13 +105,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
recallSeed(metadata?.seed);
|
recallSeed(metadata?.seed);
|
||||||
}, [metadata?.seed, recallSeed]);
|
}, [metadata?.seed, recallSeed]);
|
||||||
|
|
||||||
const handleLoadWorkflow = useCallback(() => {
|
|
||||||
if (!workflow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(workflowLoadRequested(workflow));
|
|
||||||
}, [dispatch, workflow]);
|
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(imageDTO));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
@ -179,9 +176,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
{t('parameters.downloadImage')}
|
{t('parameters.downloadImage')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={isLoadingWorkflow ? <SpinnerIcon /> : <FaCircleNodes />}
|
icon={getWorkflowResult.isLoading ? <SpinnerIcon /> : <FaCircleNodes />}
|
||||||
onClickCapture={handleLoadWorkflow}
|
onClickCapture={handleLoadWorkflow}
|
||||||
isDisabled={isLoadingWorkflow || !workflow}
|
isDisabled={!imageDTO.has_workflow}
|
||||||
>
|
>
|
||||||
{t('nodes.loadWorkflow')}
|
{t('nodes.loadWorkflow')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -14,10 +14,10 @@ import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableCon
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
import DataViewer from './DataViewer';
|
import DataViewer from './DataViewer';
|
||||||
import ImageMetadataActions from './ImageMetadataActions';
|
import ImageMetadataActions from './ImageMetadataActions';
|
||||||
|
import ImageMetadataWorkflowTabContent from './ImageMetadataWorkflowTabContent';
|
||||||
|
|
||||||
type ImageMetadataViewerProps = {
|
type ImageMetadataViewerProps = {
|
||||||
image: ImageDTO;
|
image: ImageDTO;
|
||||||
@ -32,7 +32,6 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { metadata } = useDebouncedMetadata(image.image_name);
|
const { metadata } = useDebouncedMetadata(image.image_name);
|
||||||
const { workflow } = useDebouncedWorkflow(image.workflow_id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -67,9 +66,9 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>{t('metadata.recallParameters')}</Tab>
|
<Tab>{t('metadata.recallParameters')}</Tab>
|
||||||
<Tab>{t('metadata.metadata')}</Tab>
|
<Tab isDisabled={!metadata}>{t('metadata.metadata')}</Tab>
|
||||||
<Tab>{t('metadata.imageDetails')}</Tab>
|
<Tab>{t('metadata.imageDetails')}</Tab>
|
||||||
<Tab>{t('metadata.workflow')}</Tab>
|
<Tab isDisabled={!image.has_workflow}>{t('metadata.workflow')}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
@ -97,11 +96,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{workflow ? (
|
<ImageMetadataWorkflowTabContent image={image} />
|
||||||
<DataViewer data={workflow} label={t('metadata.workflow')} />
|
|
||||||
) : (
|
|
||||||
<IAINoContentFallback label={t('nodes.noWorkflow')} />
|
|
||||||
)}
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetImageWorkflowQuery } from 'services/api/endpoints/images';
|
||||||
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import DataViewer from './DataViewer';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
image: ImageDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentData: workflow } = useGetImageWorkflowQuery(image.image_name);
|
||||||
|
|
||||||
|
if (!workflow) {
|
||||||
|
return <IAINoContentFallback label={t('nodes.noWorkflow')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DataViewer data={workflow} label={t('metadata.workflow')} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ImageMetadataWorkflowTabContent);
|
@ -0,0 +1,25 @@
|
|||||||
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaFolderOpen } from 'react-icons/fa';
|
||||||
|
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||||
|
|
||||||
|
const WorkflowLibraryButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaFolderOpen />}
|
||||||
|
onClick={onOpen}
|
||||||
|
tooltip={t('workflows.workflowLibrary')}
|
||||||
|
aria-label={t('workflows.workflowLibrary')}
|
||||||
|
/>
|
||||||
|
<WorkflowLibraryModal isOpen={isOpen} onClose={onClose} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryButton);
|
@ -0,0 +1,40 @@
|
|||||||
|
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { WorkflowCategory } from './types';
|
||||||
|
import { Dispatch, SetStateAction, memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
category: WorkflowCategory;
|
||||||
|
setCategory: Dispatch<SetStateAction<WorkflowCategory>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowLibraryCategories = ({ category, setCategory }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const handleClickUser = useCallback(() => {
|
||||||
|
setCategory('user');
|
||||||
|
}, [setCategory]);
|
||||||
|
const handleClickSystem = useCallback(() => {
|
||||||
|
setCategory('system');
|
||||||
|
}, [setCategory]);
|
||||||
|
return (
|
||||||
|
<Flex layerStyle="second" p={2} borderRadius="base">
|
||||||
|
<ButtonGroup orientation="vertical">
|
||||||
|
<IAIButton
|
||||||
|
onClick={handleClickUser}
|
||||||
|
variant={category === 'user' ? 'invokeAI' : 'ghost'}
|
||||||
|
>
|
||||||
|
{t('workflows.userCategory')}
|
||||||
|
</IAIButton>
|
||||||
|
<IAIButton
|
||||||
|
onClick={handleClickSystem}
|
||||||
|
variant={category === 'system' ? 'invokeAI' : 'ghost'}
|
||||||
|
>
|
||||||
|
{t('workflows.systemCategory')}
|
||||||
|
</IAIButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryCategories);
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { WorkflowCategory } from './types';
|
||||||
|
import { Dispatch, SetStateAction, memo } from 'react';
|
||||||
|
import { paths } from 'services/api/schema';
|
||||||
|
import WorkflowLibraryCategories from './WorkflowLibraryCategories';
|
||||||
|
import WorkflowLibraryPagination from './WorkflowLibraryPagination';
|
||||||
|
import WorkflowLibraryList from './WorkflowLibraryList';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||||
|
category: WorkflowCategory;
|
||||||
|
setCategory: Dispatch<SetStateAction<WorkflowCategory>>;
|
||||||
|
page: number;
|
||||||
|
setPage: Dispatch<SetStateAction<number>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowLibraryContent = ({
|
||||||
|
data,
|
||||||
|
category,
|
||||||
|
setCategory,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" gap={2}>
|
||||||
|
<WorkflowLibraryCategories
|
||||||
|
category={category}
|
||||||
|
setCategory={setCategory}
|
||||||
|
/>
|
||||||
|
<Flex h="full" w="full" gap={2} flexDir="column">
|
||||||
|
<WorkflowLibraryList data={data} />
|
||||||
|
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryContent);
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaFolderOpen, FaTrash } from 'react-icons/fa';
|
||||||
|
import { paths } from 'services/api/schema';
|
||||||
|
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowLibraryList = ({ data }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w="full" h="full" layerStyle="second" p={2} borderRadius="base">
|
||||||
|
<ScrollableContent>
|
||||||
|
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||||
|
{data.items.map((w) => (
|
||||||
|
<Flex key={w.workflow_id} w="full">
|
||||||
|
<Flex w="full" alignItems="center" gap={2}>
|
||||||
|
<Text>{w.workflow_id}</Text>
|
||||||
|
<Spacer />
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaFolderOpen />}
|
||||||
|
aria-label={t('workflows.loadWorkflow')}
|
||||||
|
tooltip={t('workflows.loadWorkflow')}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
icon={<FaTrash />}
|
||||||
|
colorScheme="error"
|
||||||
|
aria-label={t('workflows.deleteWorkflow')}
|
||||||
|
tooltip={t('workflows.deleteWorkflow')}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ScrollableContent>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryList);
|
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import WorkflowLibraryWrapper from './WorkflowLibraryWrapper';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowLibraryModal = ({ isOpen, onClose }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent
|
||||||
|
w="80%"
|
||||||
|
h="80%"
|
||||||
|
minW="unset"
|
||||||
|
minH="unset"
|
||||||
|
maxW="unset"
|
||||||
|
maxH="unset"
|
||||||
|
>
|
||||||
|
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<WorkflowLibraryWrapper />
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryModal);
|
@ -0,0 +1,88 @@
|
|||||||
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||||
|
import { paths } from 'services/api/schema';
|
||||||
|
|
||||||
|
const PAGES_TO_DISPLAY = 7;
|
||||||
|
|
||||||
|
type PageData = {
|
||||||
|
page: number;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: number;
|
||||||
|
setPage: Dispatch<SetStateAction<number>>;
|
||||||
|
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handlePrevPage = useCallback(() => {
|
||||||
|
setPage((p) => Math.max(p - 1, 0));
|
||||||
|
}, [setPage]);
|
||||||
|
|
||||||
|
const handleNextPage = useCallback(() => {
|
||||||
|
setPage((p) => Math.min(p + 1, data.pages - 1));
|
||||||
|
}, [data.pages, setPage]);
|
||||||
|
|
||||||
|
const pages: PageData[] = useMemo(() => {
|
||||||
|
const pages = [];
|
||||||
|
let first =
|
||||||
|
data.pages > PAGES_TO_DISPLAY
|
||||||
|
? Math.max(0, page - Math.floor(PAGES_TO_DISPLAY / 2))
|
||||||
|
: 0;
|
||||||
|
const last =
|
||||||
|
data.pages > PAGES_TO_DISPLAY
|
||||||
|
? Math.min(data.pages, first + PAGES_TO_DISPLAY)
|
||||||
|
: data.pages;
|
||||||
|
if (last - first < PAGES_TO_DISPLAY && data.pages > PAGES_TO_DISPLAY) {
|
||||||
|
first = last - PAGES_TO_DISPLAY;
|
||||||
|
}
|
||||||
|
for (let i = first; i < last; i++) {
|
||||||
|
pages.push({
|
||||||
|
page: i,
|
||||||
|
onClick: () => setPage(i),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}, [data.pages, page, setPage]);
|
||||||
|
|
||||||
|
if (data.items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
<IAIIconButton
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
isDisabled={page === 0}
|
||||||
|
aria-label={t('common.prevPage')}
|
||||||
|
icon={<FaChevronLeft />}
|
||||||
|
/>
|
||||||
|
{pages.map((p) => (
|
||||||
|
<IAIButton
|
||||||
|
w={10}
|
||||||
|
onClick={p.page === page ? undefined : p.onClick}
|
||||||
|
variant={p.page === page ? 'invokeAI' : 'ghost'}
|
||||||
|
key={p.page}
|
||||||
|
transitionDuration="0s" // the delay in animation looks jank
|
||||||
|
>
|
||||||
|
{p.page + 1}
|
||||||
|
</IAIButton>
|
||||||
|
))}
|
||||||
|
<IAIIconButton
|
||||||
|
onClick={handleNextPage}
|
||||||
|
isDisabled={page === data.pages - 1}
|
||||||
|
aria-label={t('common.nextPage')}
|
||||||
|
icon={<FaChevronRight />}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryPagination);
|
@ -0,0 +1,31 @@
|
|||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||||
|
import WorkflowLibraryContent from './WorkflowLibraryContent';
|
||||||
|
import { WorkflowCategory } from './types';
|
||||||
|
|
||||||
|
const PER_PAGE = 10;
|
||||||
|
|
||||||
|
const WorkflowLibraryWrapper = () => {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [category, setCategory] = useState<WorkflowCategory>('user');
|
||||||
|
const { data } = useListWorkflowsQuery({
|
||||||
|
page,
|
||||||
|
per_page: PER_PAGE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowLibraryContent
|
||||||
|
data={data}
|
||||||
|
page={page}
|
||||||
|
setPage={setPage}
|
||||||
|
category={category}
|
||||||
|
setCategory={setCategory}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(WorkflowLibraryWrapper);
|
@ -0,0 +1 @@
|
|||||||
|
export type WorkflowCategory = 'user' | 'system';
|
@ -1,45 +0,0 @@
|
|||||||
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { useEmbedWorkflow } from 'features/nodes/hooks/useEmbedWorkflow';
|
|
||||||
import { useWithWorkflow } from 'features/nodes/hooks/useWithWorkflow';
|
|
||||||
import { nodeEmbedWorkflowChanged } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { ChangeEvent, memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const withWorkflow = useWithWorkflow(nodeId);
|
|
||||||
const embedWorkflow = useEmbedWorkflow(nodeId);
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
dispatch(
|
|
||||||
nodeEmbedWorkflowChanged({
|
|
||||||
nodeId,
|
|
||||||
embedWorkflow: e.target.checked,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[dispatch, nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!withWorkflow) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
|
||||||
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
|
|
||||||
{t('metadata.workflow')}
|
|
||||||
</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
className="nopan"
|
|
||||||
size="sm"
|
|
||||||
onChange={handleChange}
|
|
||||||
isChecked={embedWorkflow}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(EmbedWorkflowCheckbox);
|
|
@ -1,9 +1,8 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||||
import { memo } from 'react';
|
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import EmbedWorkflowCheckbox from './EmbedWorkflowCheckbox';
|
import { memo } from 'react';
|
||||||
import SaveToGalleryCheckbox from './SaveToGalleryCheckbox';
|
import SaveToGalleryCheckbox from './SaveToGalleryCheckbox';
|
||||||
import UseCacheCheckbox from './UseCacheCheckbox';
|
import UseCacheCheckbox from './UseCacheCheckbox';
|
||||||
|
|
||||||
@ -28,7 +27,6 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isCacheEnabled && <UseCacheCheckbox nodeId={nodeId} />}
|
{isCacheEnabled && <UseCacheCheckbox nodeId={nodeId} />}
|
||||||
{hasImageOutput && <EmbedWorkflowCheckbox nodeId={nodeId} />}
|
|
||||||
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
|
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { memo } from 'react';
|
|||||||
import LoadWorkflowButton from './LoadWorkflowButton';
|
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||||
import ResetWorkflowButton from './ResetWorkflowButton';
|
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||||
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||||
|
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
return (
|
return (
|
||||||
@ -18,6 +19,7 @@ const TopCenterPanel = () => {
|
|||||||
<DownloadWorkflowButton />
|
<DownloadWorkflowButton />
|
||||||
<LoadWorkflowButton />
|
<LoadWorkflowButton />
|
||||||
<ResetWorkflowButton />
|
<ResetWorkflowButton />
|
||||||
|
<WorkflowLibraryButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,17 +11,16 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
||||||
|
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||||
import {
|
import {
|
||||||
InvocationNodeData,
|
InvocationNode,
|
||||||
InvocationTemplate,
|
InvocationTemplate,
|
||||||
isInvocationNode,
|
isInvocationNode,
|
||||||
} from 'features/nodes/types/invocation';
|
} from 'features/nodes/types/invocation';
|
||||||
|
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Node } from 'reactflow';
|
|
||||||
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
|
||||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
|
||||||
import EditableNodeTitle from './details/EditableNodeTitle';
|
import EditableNodeTitle from './details/EditableNodeTitle';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
@ -62,7 +61,7 @@ const InspectorDetailsTab = () => {
|
|||||||
export default memo(InspectorDetailsTab);
|
export default memo(InspectorDetailsTab);
|
||||||
|
|
||||||
type ContentProps = {
|
type ContentProps = {
|
||||||
node: Node<InvocationNodeData>;
|
node: InvocationNode;
|
||||||
template: InvocationTemplate;
|
template: InvocationTemplate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
||||||
|
|
||||||
export const useEmbedWorkflow = (nodeId: string) => {
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ nodes }) => {
|
|
||||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
|
||||||
if (!isInvocationNode(node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node.data.embedWorkflow;
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const embedWorkflow = useAppSelector(selector);
|
|
||||||
return embedWorkflow;
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { stateSelector } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
||||||
|
|
||||||
export const useWithWorkflow = (nodeId: string) => {
|
|
||||||
const selector = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(
|
|
||||||
stateSelector,
|
|
||||||
({ nodes }) => {
|
|
||||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
|
||||||
if (!isInvocationNode(node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
|
||||||
if (!nodeTemplate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return nodeTemplate.withWorkflow;
|
|
||||||
},
|
|
||||||
defaultSelectorOptions
|
|
||||||
),
|
|
||||||
[nodeId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const withWorkflow = useAppSelector(selector);
|
|
||||||
return withWorkflow;
|
|
||||||
};
|
|
@ -344,20 +344,6 @@ const nodesSlice = createSlice({
|
|||||||
}
|
}
|
||||||
field.label = label;
|
field.label = label;
|
||||||
},
|
},
|
||||||
nodeEmbedWorkflowChanged: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ nodeId: string; embedWorkflow: boolean }>
|
|
||||||
) => {
|
|
||||||
const { nodeId, embedWorkflow } = action.payload;
|
|
||||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
|
||||||
|
|
||||||
const node = state.nodes?.[nodeIndex];
|
|
||||||
|
|
||||||
if (!isInvocationNode(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.data.embedWorkflow = embedWorkflow;
|
|
||||||
},
|
|
||||||
nodeUseCacheChanged: (
|
nodeUseCacheChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ nodeId: string; useCache: boolean }>
|
action: PayloadAction<{ nodeId: string; useCache: boolean }>
|
||||||
@ -984,7 +970,6 @@ export const {
|
|||||||
nodeAdded,
|
nodeAdded,
|
||||||
nodeReplaced,
|
nodeReplaced,
|
||||||
nodeEditorReset,
|
nodeEditorReset,
|
||||||
nodeEmbedWorkflowChanged,
|
|
||||||
nodeExclusivelySelected,
|
nodeExclusivelySelected,
|
||||||
nodeIsIntermediateChanged,
|
nodeIsIntermediateChanged,
|
||||||
nodeIsOpenChanged,
|
nodeIsOpenChanged,
|
||||||
|
@ -18,7 +18,6 @@ export const zInvocationTemplate = z.object({
|
|||||||
inputs: z.record(zFieldInputTemplate),
|
inputs: z.record(zFieldInputTemplate),
|
||||||
outputs: z.record(zFieldOutputTemplate),
|
outputs: z.record(zFieldOutputTemplate),
|
||||||
outputType: z.string().min(1),
|
outputType: z.string().min(1),
|
||||||
withWorkflow: z.boolean(),
|
|
||||||
version: zSemVer,
|
version: zSemVer,
|
||||||
useCache: z.boolean(),
|
useCache: z.boolean(),
|
||||||
nodePack: z.string().min(1).nullish(),
|
nodePack: z.string().min(1).nullish(),
|
||||||
@ -33,7 +32,6 @@ export const zInvocationNodeData = z.object({
|
|||||||
label: z.string(),
|
label: z.string(),
|
||||||
isOpen: z.boolean(),
|
isOpen: z.boolean(),
|
||||||
notes: z.string(),
|
notes: z.string(),
|
||||||
embedWorkflow: z.boolean(),
|
|
||||||
isIntermediate: z.boolean(),
|
isIntermediate: z.boolean(),
|
||||||
useCache: z.boolean(),
|
useCache: z.boolean(),
|
||||||
version: zSemVer,
|
version: zSemVer,
|
||||||
|
@ -73,6 +73,7 @@ export type WorkflowEdge = z.infer<typeof zWorkflowEdge>;
|
|||||||
|
|
||||||
// #region Workflow
|
// #region Workflow
|
||||||
export const zWorkflowV2 = z.object({
|
export const zWorkflowV2 = z.object({
|
||||||
|
id: z.string().min(1).optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
author: z.string(),
|
author: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { NodesState } from 'features/nodes/store/types';
|
import { NodesState } from 'features/nodes/store/types';
|
||||||
|
import {
|
||||||
|
FieldInputInstance,
|
||||||
|
isColorFieldInputInstance,
|
||||||
|
} from 'features/nodes/types/field';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { cloneDeep, omit, reduce } from 'lodash-es';
|
import { cloneDeep, omit, reduce } from 'lodash-es';
|
||||||
import { Graph } from 'services/api/types';
|
import { Graph } from 'services/api/types';
|
||||||
import { AnyInvocation } from 'services/events/types';
|
import { AnyInvocation } from 'services/events/types';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
|
||||||
import {
|
|
||||||
FieldInputInstance,
|
|
||||||
isColorFieldInputInstance,
|
|
||||||
} from 'features/nodes/types/field';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to do special handling for some fields
|
* We need to do special handling for some fields
|
||||||
@ -44,7 +43,7 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
|||||||
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
|
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
|
||||||
(nodesAccumulator, node) => {
|
(nodesAccumulator, node) => {
|
||||||
const { id, data } = node;
|
const { id, data } = node;
|
||||||
const { type, inputs, isIntermediate, embedWorkflow } = data;
|
const { type, inputs, isIntermediate } = data;
|
||||||
|
|
||||||
// Transform each node's inputs to simple key-value pairs
|
// Transform each node's inputs to simple key-value pairs
|
||||||
const transformedInputs = reduce(
|
const transformedInputs = reduce(
|
||||||
@ -69,11 +68,6 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
|||||||
is_intermediate: isIntermediate,
|
is_intermediate: isIntermediate,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (embedWorkflow) {
|
|
||||||
// add the workflow to the node
|
|
||||||
Object.assign(graphNode, { workflow: buildWorkflow(nodesState) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add it to the nodes object
|
// Add it to the nodes object
|
||||||
Object.assign(nodesAccumulator, {
|
Object.assign(nodesAccumulator, {
|
||||||
[id]: graphNode,
|
[id]: graphNode,
|
||||||
|
@ -67,7 +67,6 @@ export const buildInvocationNode = (
|
|||||||
label: '',
|
label: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
embedWorkflow: false,
|
|
||||||
isIntermediate: type === 'save_image' ? false : true,
|
isIntermediate: type === 'save_image' ? false : true,
|
||||||
useCache: template.useCache,
|
useCache: template.useCache,
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { satisfies } from 'compare-versions';
|
import { satisfies } from 'compare-versions';
|
||||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||||
import {
|
import {
|
||||||
InvocationNodeData,
|
InvocationNode,
|
||||||
InvocationTemplate,
|
InvocationTemplate,
|
||||||
} from 'features/nodes/types/invocation';
|
} from 'features/nodes/types/invocation';
|
||||||
import { zParsedSemver } from 'features/nodes/types/semver';
|
import { zParsedSemver } from 'features/nodes/types/semver';
|
||||||
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
import { cloneDeep, keys, defaultsDeep, pick } from 'lodash-es';
|
||||||
import { Node } from 'reactflow';
|
|
||||||
import { buildInvocationNode } from './buildInvocationNode';
|
import { buildInvocationNode } from './buildInvocationNode';
|
||||||
|
|
||||||
export const getNeedsUpdate = (
|
export const getNeedsUpdate = (
|
||||||
node: Node<InvocationNodeData>,
|
node: InvocationNode,
|
||||||
template: InvocationTemplate
|
template: InvocationTemplate
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (node.data.type !== template.type) {
|
if (node.data.type !== template.type) {
|
||||||
@ -24,7 +23,7 @@ export const getNeedsUpdate = (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const getMayUpdateNode = (
|
export const getMayUpdateNode = (
|
||||||
node: Node<InvocationNodeData>,
|
node: InvocationNode,
|
||||||
template: InvocationTemplate
|
template: InvocationTemplate
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const needsUpdate = getNeedsUpdate(node, template);
|
const needsUpdate = getNeedsUpdate(node, template);
|
||||||
@ -45,9 +44,9 @@ export const getMayUpdateNode = (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const updateNode = (
|
export const updateNode = (
|
||||||
node: Node<InvocationNodeData>,
|
node: InvocationNode,
|
||||||
template: InvocationTemplate
|
template: InvocationTemplate
|
||||||
): Node<InvocationNodeData> => {
|
): InvocationNode => {
|
||||||
const mayUpdate = getMayUpdateNode(node, template);
|
const mayUpdate = getMayUpdateNode(node, template);
|
||||||
|
|
||||||
if (!mayUpdate || node.data.type !== template.type) {
|
if (!mayUpdate || node.data.type !== template.type) {
|
||||||
@ -64,5 +63,8 @@ export const updateNode = (
|
|||||||
clone.data.version = template.version;
|
clone.data.version = template.version;
|
||||||
defaultsDeep(clone, defaults); // mutates!
|
defaultsDeep(clone, defaults); // mutates!
|
||||||
|
|
||||||
|
// Remove any fields that are not in the template
|
||||||
|
clone.data.inputs = pick(clone.data.inputs, keys(defaults.data.inputs));
|
||||||
|
clone.data.outputs = pick(clone.data.outputs, keys(defaults.data.outputs));
|
||||||
return clone;
|
return clone;
|
||||||
};
|
};
|
||||||
|
@ -86,7 +86,6 @@ export const parseSchema = (
|
|||||||
const description = schema.description ?? '';
|
const description = schema.description ?? '';
|
||||||
const version = schema.version;
|
const version = schema.version;
|
||||||
const nodePack = schema.node_pack;
|
const nodePack = schema.node_pack;
|
||||||
let withWorkflow = false;
|
|
||||||
|
|
||||||
const inputs = reduce(
|
const inputs = reduce(
|
||||||
schema.properties,
|
schema.properties,
|
||||||
@ -114,12 +113,6 @@ export const parseSchema = (
|
|||||||
try {
|
try {
|
||||||
const fieldType = parseFieldType(property);
|
const fieldType = parseFieldType(property);
|
||||||
|
|
||||||
if (fieldType.name === 'WorkflowField') {
|
|
||||||
// This supports workflows, set the flag and skip to next field
|
|
||||||
withWorkflow = true;
|
|
||||||
return inputsAccumulator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReservedFieldType(fieldType.name)) {
|
if (isReservedFieldType(fieldType.name)) {
|
||||||
// Skip processing this reserved field
|
// Skip processing this reserved field
|
||||||
return inputsAccumulator;
|
return inputsAccumulator;
|
||||||
@ -260,7 +253,6 @@ export const parseSchema = (
|
|||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
useCache,
|
useCache,
|
||||||
withWorkflow,
|
|
||||||
nodePack,
|
nodePack,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { $store } from 'app/store/nanostores/store';
|
import { $store } from 'app/store/nanostores/store';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { FieldType } from 'features/nodes/types/field';
|
|
||||||
import { InvocationNodeData } from 'features/nodes/types/invocation';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { forEach } from 'lodash-es';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import {
|
import {
|
||||||
WorkflowMigrationError,
|
WorkflowMigrationError,
|
||||||
WorkflowVersionError,
|
WorkflowVersionError,
|
||||||
} from 'features/nodes/types/error';
|
} from 'features/nodes/types/error';
|
||||||
|
import { FieldType } from 'features/nodes/types/field';
|
||||||
|
import { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||||
import { zSemVer } from 'features/nodes/types/semver';
|
import { zSemVer } from 'features/nodes/types/semver';
|
||||||
import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap';
|
import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap';
|
||||||
import { WorkflowV1, zWorkflowV1 } from 'features/nodes/types/v1/workflowV1';
|
import { WorkflowV1, zWorkflowV1 } from 'features/nodes/types/v1/workflowV1';
|
||||||
import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow';
|
import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { forEach } from 'lodash-es';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper schema to extract the version from a workflow.
|
* Helper schema to extract the version from a workflow.
|
||||||
@ -25,6 +25,11 @@ const zWorkflowMetaVersion = z.object({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrates a workflow from V1 to V2.
|
* Migrates a workflow from V1 to V2.
|
||||||
|
*
|
||||||
|
* Changes include:
|
||||||
|
* - Field types are now structured
|
||||||
|
* - Invocation node pack is now saved in the node data
|
||||||
|
* - Workflow schema version bumped to 2.0.0
|
||||||
*/
|
*/
|
||||||
const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||||
const invocationTemplates = ($store.get()?.getState() as RootState).nodes
|
const invocationTemplates = ($store.get()?.getState() as RootState).nodes
|
||||||
@ -39,7 +44,6 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
|||||||
t('nodes.unknownFieldType', { type: input.type })
|
t('nodes.unknownFieldType', { type: input.type })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Cast as the V2 type
|
|
||||||
(input.type as unknown as FieldType) = newFieldType;
|
(input.type as unknown as FieldType) = newFieldType;
|
||||||
});
|
});
|
||||||
forEach(node.data.outputs, (output) => {
|
forEach(node.data.outputs, (output) => {
|
||||||
@ -50,19 +54,19 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
|||||||
t('nodes.unknownFieldType', { type: output.type })
|
t('nodes.unknownFieldType', { type: output.type })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Cast as the V2 type
|
|
||||||
(output.type as unknown as FieldType) = newFieldType;
|
(output.type as unknown as FieldType) = newFieldType;
|
||||||
});
|
});
|
||||||
// Migrate nodePack
|
// Add node pack
|
||||||
const invocationTemplate = invocationTemplates[node.data.type];
|
const invocationTemplate = invocationTemplates[node.data.type];
|
||||||
const nodePack = invocationTemplate
|
const nodePack = invocationTemplate
|
||||||
? invocationTemplate.nodePack
|
? invocationTemplate.nodePack
|
||||||
: t('common.unknown');
|
: t('common.unknown');
|
||||||
// Cast as the V2 type
|
|
||||||
(node.data as unknown as InvocationNodeData).nodePack = nodePack;
|
(node.data as unknown as InvocationNodeData).nodePack = nodePack;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(workflowToMigrate.meta.version as WorkflowV2['meta']['version']) = '2.0.0';
|
// Bump version
|
||||||
|
(workflowToMigrate as unknown as WorkflowV2).meta.version = '2.0.0';
|
||||||
|
// Parsing strips out any extra properties not in the latest version
|
||||||
return zWorkflowV2.parse(workflowToMigrate);
|
return zWorkflowV2.parse(workflowToMigrate);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,7 +77,6 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV2 => {
|
|||||||
const workflowVersionResult = zWorkflowMetaVersion.safeParse(data);
|
const workflowVersionResult = zWorkflowMetaVersion.safeParse(data);
|
||||||
|
|
||||||
if (!workflowVersionResult.success) {
|
if (!workflowVersionResult.success) {
|
||||||
console.log(data);
|
|
||||||
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
} from 'features/gallery/store/types';
|
} from 'features/gallery/store/types';
|
||||||
import { CoreMetadata, zCoreMetadata } from 'features/nodes/types/metadata';
|
import { CoreMetadata, zCoreMetadata } from 'features/nodes/types/metadata';
|
||||||
import { keyBy } from 'lodash-es';
|
import { keyBy } from 'lodash-es';
|
||||||
import { ApiTagDescription, LIST_TAG, api } from '..';
|
|
||||||
import { components, paths } from 'services/api/schema';
|
import { components, paths } from 'services/api/schema';
|
||||||
import {
|
import {
|
||||||
DeleteBoardResult,
|
DeleteBoardResult,
|
||||||
@ -26,6 +25,7 @@ import {
|
|||||||
imagesAdapter,
|
imagesAdapter,
|
||||||
imagesSelectors,
|
imagesSelectors,
|
||||||
} from 'services/api/util';
|
} from 'services/api/util';
|
||||||
|
import { ApiTagDescription, LIST_TAG, api } from '..';
|
||||||
import { boardsApi } from './boards';
|
import { boardsApi } from './boards';
|
||||||
|
|
||||||
export const imagesApi = api.injectEndpoints({
|
export const imagesApi = api.injectEndpoints({
|
||||||
@ -128,6 +128,16 @@ export const imagesApi = api.injectEndpoints({
|
|||||||
},
|
},
|
||||||
keepUnusedDataFor: 86400, // 24 hours
|
keepUnusedDataFor: 86400, // 24 hours
|
||||||
}),
|
}),
|
||||||
|
getImageWorkflow: build.query<
|
||||||
|
paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json'],
|
||||||
|
string
|
||||||
|
>({
|
||||||
|
query: (image_name) => ({ url: `images/i/${image_name}/workflow` }),
|
||||||
|
providesTags: (result, error, image_name) => [
|
||||||
|
{ type: 'ImageWorkflow', id: image_name },
|
||||||
|
],
|
||||||
|
keepUnusedDataFor: 86400, // 24 hours
|
||||||
|
}),
|
||||||
deleteImage: build.mutation<void, ImageDTO>({
|
deleteImage: build.mutation<void, ImageDTO>({
|
||||||
query: ({ image_name }) => ({
|
query: ({ image_name }) => ({
|
||||||
url: `images/i/${image_name}`,
|
url: `images/i/${image_name}`,
|
||||||
@ -1560,6 +1570,8 @@ export const {
|
|||||||
useLazyListImagesQuery,
|
useLazyListImagesQuery,
|
||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useGetImageMetadataQuery,
|
useGetImageMetadataQuery,
|
||||||
|
useGetImageWorkflowQuery,
|
||||||
|
useLazyGetImageWorkflowQuery,
|
||||||
useDeleteImageMutation,
|
useDeleteImageMutation,
|
||||||
useDeleteImagesMutation,
|
useDeleteImagesMutation,
|
||||||
useUploadImageMutation,
|
useUploadImageMutation,
|
||||||
|
@ -1,30 +1,70 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||||
import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow';
|
|
||||||
import { api } from '..';
|
|
||||||
import { paths } from 'services/api/schema';
|
import { paths } from 'services/api/schema';
|
||||||
|
import { LIST_TAG, api } from '..';
|
||||||
|
|
||||||
export const workflowsApi = api.injectEndpoints({
|
export const workflowsApi = api.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
getWorkflow: build.query<WorkflowV2 | undefined, string>({
|
getWorkflow: build.query<
|
||||||
|
paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json'],
|
||||||
|
string
|
||||||
|
>({
|
||||||
query: (workflow_id) => `workflows/i/${workflow_id}`,
|
query: (workflow_id) => `workflows/i/${workflow_id}`,
|
||||||
providesTags: (result, error, workflow_id) => [
|
providesTags: (result, error, workflow_id) => [
|
||||||
{ type: 'Workflow', id: workflow_id },
|
{ type: 'Workflow', id: workflow_id },
|
||||||
],
|
],
|
||||||
transformResponse: (
|
}),
|
||||||
response: paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json']
|
deleteWorkflow: build.mutation<void, string>({
|
||||||
) => {
|
query: (workflow_id) => ({
|
||||||
if (response) {
|
url: `workflows/i/${workflow_id}`,
|
||||||
const result = zWorkflowV2.safeParse(response);
|
method: 'DELETE',
|
||||||
if (result.success) {
|
}),
|
||||||
return result.data;
|
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||||
} else {
|
}),
|
||||||
logger('images').warn('Problem parsing workflow');
|
createWorkflow: build.mutation<
|
||||||
}
|
paths['/api/v1/workflows/']['post']['responses']['200']['content']['application/json'],
|
||||||
}
|
WorkflowV2
|
||||||
return;
|
>({
|
||||||
},
|
query: (workflow) => ({
|
||||||
|
url: 'workflows',
|
||||||
|
method: 'POST',
|
||||||
|
body: workflow,
|
||||||
|
}),
|
||||||
|
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||||
|
}),
|
||||||
|
updateWorkflow: build.mutation<
|
||||||
|
paths['/api/v1/workflows/i/{workflow_id}']['patch']['responses']['200']['content']['application/json'],
|
||||||
|
WorkflowV2
|
||||||
|
>({
|
||||||
|
query: (workflow) => ({
|
||||||
|
url: `workflows/i/${workflow.id}`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: workflow,
|
||||||
|
}),
|
||||||
|
invalidatesTags: (response, error, arg) => [
|
||||||
|
{ type: 'Workflow', id: LIST_TAG },
|
||||||
|
{ type: 'Workflow', id: arg.id },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
listWorkflows: build.query<
|
||||||
|
paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'],
|
||||||
|
NonNullable<paths['/api/v1/workflows/']['get']['parameters']['query']>
|
||||||
|
>({
|
||||||
|
query: (params) => ({
|
||||||
|
url: 'workflows/',
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
providesTags: (result, error, params) => [
|
||||||
|
{ type: 'Workflow', id: LIST_TAG },
|
||||||
|
{ type: 'Workflow', id: params?.page },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useGetWorkflowQuery } = workflowsApi;
|
export const {
|
||||||
|
useGetWorkflowQuery,
|
||||||
|
useCreateWorkflowMutation,
|
||||||
|
useDeleteWorkflowMutation,
|
||||||
|
useUpdateWorkflowMutation,
|
||||||
|
useListWorkflowsQuery,
|
||||||
|
} = workflowsApi;
|
||||||
|
@ -20,6 +20,7 @@ export const tagTypes = [
|
|||||||
'ImageNameList',
|
'ImageNameList',
|
||||||
'ImageList',
|
'ImageList',
|
||||||
'ImageMetadata',
|
'ImageMetadata',
|
||||||
|
'ImageWorkflow',
|
||||||
'ImageMetadataFromFile',
|
'ImageMetadataFromFile',
|
||||||
'IntermediatesCount',
|
'IntermediatesCount',
|
||||||
'SessionQueueItem',
|
'SessionQueueItem',
|
||||||
|
544
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
544
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user