refactor(ui): migrating to new metadata handlers

This commit is contained in:
psychedelicious
2025-07-04 17:08:00 +10:00
parent 2d06949e80
commit f23be119fc
7 changed files with 62 additions and 152 deletions

View File

@@ -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]
);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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,
});
};

View File

@@ -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)[] = []