From f23be119fc525edc2d01d61e0e32a9d09eb8eca7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:08:00 +1000 Subject: [PATCH] refactor(ui): migrating to new metadata handlers --- .../web/src/app/hooks/useStudioInitAction.ts | 4 +- .../ImageMetadataActions.tsx | 3 +- .../ImageMetadataViewer.tsx | 14 +-- .../ImageViewer/ImageMetadataMini.tsx | 92 ------------------- .../features/gallery/hooks/useImageActions.ts | 65 +++++-------- .../web/src/features/metadata/parsing.tsx | 30 +++++- .../src/features/metadata/util/handlers.ts | 6 +- 7 files changed, 62 insertions(+), 152 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageMetadataMini.tsx diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts index 4032f6cc8f..c9a742cc68 100644 --- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts +++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts @@ -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] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index bf85f4f468..e8d9f4f4f2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -30,7 +30,6 @@ const ImageMetadataActions = (props: Props) => { return ( - @@ -63,7 +62,7 @@ const ImageMetadataActions = (props: Props) => { export default memo(ImageMetadataActions); -const UnrecallableMetadataDatum = typedMemo( +export const UnrecallableMetadataDatum = typedMemo( ({ metadata, handler }: { metadata: unknown; handler: UnrecallableMetadataHandler }) => { const { data } = useUnrecallableMetadataDatum(metadata, handler); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index 92acffef32..89b19fdea6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -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 ( { overflow="hidden" > - {createdBy.valueOrNull && ( - - {t('metadata.createdBy')}: {createdBy.valueOrNull} - - )} + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageMetadataMini.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageMetadataMini.tsx deleted file mode 100644 index b9cdac48e3..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageMetadataMini.tsx +++ /dev/null @@ -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 ( - - - Loading metadata... - - ); - } - if ( - !createdBy.valueOrNull && - !positivePrompt.valueOrNull && - !negativePrompt.valueOrNull && - !seed.valueOrNull && - !model.valueOrNull && - !strength.valueOrNull - ) { - return ( - - No metadata found - - ); - } - return ( - - - {createdBy.valueOrNull && ( - <> - - {createdBy.label}: - - {createdBy.renderedValue} - - )} - {positivePrompt.valueOrNull && ( - <> - - {positivePrompt.label}: - - {positivePrompt.renderedValue} - - )} - {negativePrompt.valueOrNull && ( - <> - - {negativePrompt.label}: - - {negativePrompt.renderedValue} - - )} - {model.valueOrNull !== null && ( - <> - - {model.label}: - - {model.renderedValue} - - )} - {strength.valueOrNull !== null && ( - <> - - {strength.label}: - - {strength.renderedValue} - - )} - {seed.valueOrNull !== null && ( - <> - - {seed.label}: - - {seed.renderedValue} - - )} - - - ); -}; -ImageMetadataMini.displayName = 'ImageMetadataMini'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index 6a0b46340f..b6996b3281 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -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) { diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index f42601078f..f6c6b9d2d4 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -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 | CollectionMetadataHandler)[]; store: AppStore; + skip?: (SingleMetadataHandler | CollectionMetadataHandler)[]; silent?: boolean; }): Promise | CollectionMetadataHandler, unknown>> => { - const { metadata, handlers, store, silent = false } = arg; + const { metadata, handlers, store, silent = false, skip = [] } = arg; const recalled = new Map | CollectionMetadataHandler, 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 | CollectionMetadataHandler)[] +) => { 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, }); }; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index e587118a54..aef0323386 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -311,7 +311,7 @@ export const handlers = { type ParsedValue = Awaited>; type RecallResults = Partial>; -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)[] = []