mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 18:25:28 -05:00
refactor(ui): migrating to new metadata handlers
This commit is contained in:
@@ -8,7 +8,7 @@ import { paramsReset } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
|
||||
import { MetadataUtils } from 'features/metadata/parsing';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { $isWorkflowLibraryModalOpen } from 'features/nodes/store/workflowLibraryModal';
|
||||
import {
|
||||
@@ -117,7 +117,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
const metadata = getImageMetadataResult.value;
|
||||
store.dispatch(canvasReset());
|
||||
// This shows a toast
|
||||
await parseAndRecallAllMetadata(metadata, true);
|
||||
await MetadataUtils.recallAll(metadata, store);
|
||||
},
|
||||
[store, t]
|
||||
);
|
||||
|
||||
@@ -30,7 +30,6 @@ const ImageMetadataActions = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" ps={8}>
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.CreatedBy} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.GenerationMode} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.PositivePrompt} />
|
||||
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.NegativePrompt} />
|
||||
@@ -63,7 +62,7 @@ const ImageMetadataActions = (props: Props) => {
|
||||
|
||||
export default memo(ImageMetadataActions);
|
||||
|
||||
const UnrecallableMetadataDatum = typedMemo(
|
||||
export const UnrecallableMetadataDatum = typedMemo(
|
||||
<T,>({ metadata, handler }: { metadata: unknown; handler: UnrecallableMetadataHandler<T> }) => {
|
||||
const { data } = useUnrecallableMetadataDatum(metadata, handler);
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library';
|
||||
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent';
|
||||
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
||||
import { handlers } from 'features/metadata/util/handlers';
|
||||
import { MetadataHandlers } from 'features/metadata/parsing';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import DataViewer from './DataViewer';
|
||||
import ImageMetadataActions from './ImageMetadataActions';
|
||||
import ImageMetadataActions, { UnrecallableMetadataDatum } from './ImageMetadataActions';
|
||||
import ImageMetadataWorkflowTabContent from './ImageMetadataWorkflowTabContent';
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
@@ -26,7 +25,6 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { metadata, isLoading } = useDebouncedMetadata(image.image_name);
|
||||
const createdBy = useMetadataItem(metadata, handlers.createdBy);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -41,11 +39,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<ExternalLink href={image.image_url} label={image.image_name} />
|
||||
{createdBy.valueOrNull && (
|
||||
<Text>
|
||||
{t('metadata.createdBy')}: {createdBy.valueOrNull}
|
||||
</Text>
|
||||
)}
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.CreatedBy} />
|
||||
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
<TabList>
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Alert, AlertIcon, AlertTitle, Grid, GridItem, Text } from '@invoke-ai/ui-library';
|
||||
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
||||
import { handlers } from 'features/metadata/util/handlers';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
export const ImageMetadataMini = ({ imageName }: { imageName: string }) => {
|
||||
const { metadata, isLoading } = useDebouncedMetadata(imageName);
|
||||
const createdBy = useMetadataItem(metadata, handlers.createdBy);
|
||||
const positivePrompt = useMetadataItem(metadata, handlers.positivePrompt);
|
||||
const negativePrompt = useMetadataItem(metadata, handlers.negativePrompt);
|
||||
const seed = useMetadataItem(metadata, handlers.seed);
|
||||
const model = useMetadataItem(metadata, handlers.model);
|
||||
const strength = useMetadataItem(metadata, handlers.strength);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Alert status="loading" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<AlertIcon />
|
||||
<AlertTitle>Loading metadata...</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (
|
||||
!createdBy.valueOrNull &&
|
||||
!positivePrompt.valueOrNull &&
|
||||
!negativePrompt.valueOrNull &&
|
||||
!seed.valueOrNull &&
|
||||
!model.valueOrNull &&
|
||||
!strength.valueOrNull
|
||||
) {
|
||||
return (
|
||||
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<AlertTitle>No metadata found</AlertTitle>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<Grid gridTemplateColumns="auto 1fr" columnGap={2} maxW={420}>
|
||||
{createdBy.valueOrNull && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{createdBy.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{createdBy.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
{positivePrompt.valueOrNull && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{positivePrompt.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{positivePrompt.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
{negativePrompt.valueOrNull && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{negativePrompt.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{negativePrompt.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
{model.valueOrNull !== null && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{model.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{model.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
{strength.valueOrNull !== null && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{strength.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{strength.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
{seed.valueOrNull !== null && (
|
||||
<>
|
||||
<GridItem textAlign="end">
|
||||
<Text fontWeight="semibold">{seed.label}:</Text>
|
||||
</GridItem>
|
||||
<GridItem>{seed.renderedValue}</GridItem>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
ImageMetadataMini.displayName = 'ImageMetadataMini';
|
||||
@@ -3,12 +3,7 @@ import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddl
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import {
|
||||
handlers,
|
||||
parseAndRecallAllMetadata,
|
||||
parseAndRecallImageDimensions,
|
||||
parseAndRecallPrompts,
|
||||
} from 'features/metadata/util/handlers';
|
||||
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import {
|
||||
@@ -16,7 +11,6 @@ import {
|
||||
selectStylePresetActivePresetId,
|
||||
} from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -24,7 +18,7 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
@@ -40,7 +34,7 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (metadata) {
|
||||
setHasMetadata(true);
|
||||
try {
|
||||
await handlers.seed.parse(metadata);
|
||||
await MetadataHandlers.Seed.parse(metadata, store);
|
||||
setHasSeed(true);
|
||||
} catch {
|
||||
setHasSeed(false);
|
||||
@@ -48,10 +42,10 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
|
||||
// Need to catch all of these to avoid unhandled promise rejections bubbling up to instrumented error handlers
|
||||
const promptParseResults = await Promise.allSettled([
|
||||
handlers.positivePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.negativePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.sdxlPositiveStylePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.sdxlNegativeStylePrompt.parse(metadata).catch(() => {}),
|
||||
MetadataHandlers.PositivePrompt.parse(metadata, store).catch(() => {}),
|
||||
MetadataHandlers.NegativePrompt.parse(metadata, store).catch(() => {}),
|
||||
MetadataHandlers.PositiveStylePrompt.parse(metadata, store).catch(() => {}),
|
||||
MetadataHandlers.NegativeStylePrompt.parse(metadata, store).catch(() => {}),
|
||||
]);
|
||||
if (promptParseResults.some((result) => result.status === 'fulfilled')) {
|
||||
setHasPrompts(true);
|
||||
@@ -65,17 +59,17 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
}
|
||||
};
|
||||
parseMetadata();
|
||||
}, [metadata]);
|
||||
}, [metadata, store]);
|
||||
|
||||
const clearStylePreset = useCallback(() => {
|
||||
if (activeStylePresetId) {
|
||||
dispatch(activeStylePresetIdChanged(null));
|
||||
store.dispatch(activeStylePresetIdChanged(null));
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('stylePresets.promptTemplateCleared'),
|
||||
});
|
||||
}
|
||||
}, [dispatch, activeStylePresetId, t]);
|
||||
}, [activeStylePresetId, store, t]);
|
||||
|
||||
const recallAll = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
@@ -84,10 +78,9 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
const activeTabName = selectActiveTab(getState());
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
|
||||
MetadataUtils.recallAll(metadata, store, isStaging ? [MetadataHandlers.Width, MetadataHandlers.Height] : []);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, getState, isStaging, clearStylePreset]);
|
||||
}, [imageDTO, metadata, store, isStaging, clearStylePreset]);
|
||||
|
||||
const remix = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
@@ -96,11 +89,10 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
const activeTabName = selectActiveTab(getState());
|
||||
// Recalls all metadata parameters except seed
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']);
|
||||
MetadataUtils.recallAll(metadata, store, [MetadataHandlers.Seed]);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, getState, clearStylePreset]);
|
||||
}, [imageDTO, metadata, store, clearStylePreset]);
|
||||
|
||||
const recallSeed = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
@@ -109,15 +101,8 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
handlers.seed
|
||||
.parse(metadata)
|
||||
.then((seed) => {
|
||||
handlers.seed.recall?.(seed, true);
|
||||
})
|
||||
.catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [imageDTO, metadata]);
|
||||
MetadataUtils.recallByHandler({ metadata, store, handler: MetadataHandlers.Seed });
|
||||
}, [imageDTO, metadata, store]);
|
||||
|
||||
const recallPrompts = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
@@ -126,9 +111,9 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallPrompts(metadata);
|
||||
MetadataUtils.recallPrompts(metadata, store);
|
||||
clearStylePreset();
|
||||
}, [imageDTO, metadata, clearStylePreset]);
|
||||
}, [imageDTO, metadata, store, clearStylePreset]);
|
||||
|
||||
const createAsPreset = useCallback(async () => {
|
||||
if (!imageDTO) {
|
||||
@@ -141,12 +126,12 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
let negativePrompt: string;
|
||||
|
||||
try {
|
||||
positivePrompt = await handlers.positivePrompt.parse(metadata);
|
||||
positivePrompt = await MetadataHandlers.PositivePrompt.parse(metadata, store);
|
||||
} catch (error) {
|
||||
positivePrompt = '';
|
||||
}
|
||||
try {
|
||||
negativePrompt = (await handlers.negativePrompt.parse(metadata)) ?? '';
|
||||
negativePrompt = (await MetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
|
||||
} catch (error) {
|
||||
negativePrompt = '';
|
||||
}
|
||||
@@ -162,7 +147,7 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
updatingStylePresetId: null,
|
||||
isModalOpen: true,
|
||||
});
|
||||
}, [metadata, imageDTO]);
|
||||
}, [imageDTO, metadata, store]);
|
||||
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
|
||||
@@ -184,15 +169,15 @@ export const useImageActions = (imageDTO: ImageDTO | null) => {
|
||||
if (isStaging) {
|
||||
return;
|
||||
}
|
||||
parseAndRecallImageDimensions(imageDTO);
|
||||
}, [imageDTO, isStaging]);
|
||||
MetadataUtils.recallDimensions(imageDTO, store);
|
||||
}, [imageDTO, isStaging, store]);
|
||||
|
||||
const upscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||
}, [dispatch, imageDTO]);
|
||||
store.dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||
}, [imageDTO, store]);
|
||||
|
||||
const _delete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
|
||||
@@ -760,6 +760,20 @@ export const MetadataHandlers = {
|
||||
MainModel,
|
||||
VAEModel,
|
||||
LoRAs,
|
||||
// TODO:
|
||||
// Ref images
|
||||
// controlNet: parseControlNet,
|
||||
// controlNets: parseAllControlNets,
|
||||
// t2iAdapter: parseT2IAdapter,
|
||||
// t2iAdapters: parseAllT2IAdapters,
|
||||
// ipAdapter: parseIPAdapter,
|
||||
// ipAdapters: parseAllIPAdapters,
|
||||
// controlNetToControlLayer: parseControlNetToControlAdapterLayer,
|
||||
// t2iAdapterToControlAdapterLayer: parseT2IAdapterToControlAdapterLayer,
|
||||
// ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer,
|
||||
// layer: parseLayer,
|
||||
// layers: parseLayers,
|
||||
// canvasV2Metadata: parseCanvasV2Metadata,
|
||||
} as const;
|
||||
|
||||
const successToast = (parameter: ReactNode) => {
|
||||
@@ -815,17 +829,22 @@ const recallByHandlers = async (arg: {
|
||||
metadata: unknown;
|
||||
handlers: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[];
|
||||
store: AppStore;
|
||||
skip?: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[];
|
||||
silent?: boolean;
|
||||
}): Promise<Map<SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>, unknown>> => {
|
||||
const { metadata, handlers, store, silent = false } = arg;
|
||||
const { metadata, handlers, store, silent = false, skip = [] } = arg;
|
||||
|
||||
const recalled = new Map<SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>, unknown>();
|
||||
|
||||
const filteredHandlers = handlers.filter(
|
||||
(handler) => !skip.some((skippedHandler) => skippedHandler.type === handler.type)
|
||||
);
|
||||
|
||||
// It's possible for some metadata item's recall to clobber the recall of another. For example, the model recall
|
||||
// may change the width and height. If we are also recalling the width and height directly, we need to ensure that the
|
||||
// model is recalled first, so it doesn't accidentally override the width and height. This is the only known case
|
||||
// where the order of recall matters.
|
||||
const sortedHandlers = handlers.sort((a, b) => {
|
||||
const sortedHandlers = filteredHandlers.sort((a, b) => {
|
||||
if (a === MetadataHandlers.MainModel) {
|
||||
return -1; // MainModel should be recalled first
|
||||
} else if (b === MetadataHandlers.MainModel) {
|
||||
@@ -910,7 +929,11 @@ const recallDimensions = async (metadata: unknown, store: AppStore) => {
|
||||
}
|
||||
};
|
||||
|
||||
const recallAll = async (metadata: unknown, store: AppStore) => {
|
||||
const recallAll = async (
|
||||
metadata: unknown,
|
||||
store: AppStore,
|
||||
skip?: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[]
|
||||
) => {
|
||||
const handlers = Object.values(MetadataHandlers).filter(
|
||||
(handler) => isSingleMetadataHandler(handler) || isCollectionMetadataHandler(handler)
|
||||
);
|
||||
@@ -918,6 +941,7 @@ const recallAll = async (metadata: unknown, store: AppStore) => {
|
||||
metadata,
|
||||
handlers,
|
||||
store,
|
||||
skip,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ export const handlers = {
|
||||
type ParsedValue = Awaited<ReturnType<(typeof handlers)[keyof typeof handlers]['parse']>>;
|
||||
type RecallResults = Partial<Record<keyof typeof handlers, ParsedValue>>;
|
||||
|
||||
export const parseAndRecallPrompts = async (metadata: unknown) => {
|
||||
const parseAndRecallPrompts = async (metadata: unknown) => {
|
||||
const keysToRecall: (keyof typeof handlers)[] = [
|
||||
'positivePrompt',
|
||||
'negativePrompt',
|
||||
@@ -324,7 +324,7 @@ export const parseAndRecallPrompts = async (metadata: unknown) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const parseAndRecallImageDimensions = (metadata: unknown) => {
|
||||
const parseAndRecallImageDimensions = (metadata: unknown) => {
|
||||
const recalled = recallKeys(['width', 'height'], metadata);
|
||||
if (size(recalled) > 0) {
|
||||
parameterSetToast(t('metadata.imageDimensions'));
|
||||
@@ -336,7 +336,7 @@ const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['strength'];
|
||||
// These handlers should be omitted when recalling to the rest of the app
|
||||
const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = [];
|
||||
|
||||
export const parseAndRecallAllMetadata = async (
|
||||
const parseAndRecallAllMetadata = async (
|
||||
metadata: unknown,
|
||||
toControlLayers: boolean,
|
||||
skip: (keyof typeof handlers)[] = []
|
||||
|
||||
Reference in New Issue
Block a user