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 bb7316447e..cb00e1557c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,12 +1,17 @@ -import type { FlexProps } from '@invoke-ai/ui-library'; -import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; +import { Box, Flex, IconButton } from '@invoke-ai/ui-library'; import { typedMemo } from 'common/util/typedMemo'; -import { isPrimitive } from 'es-toolkit'; -import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; -import { MetadataHanders, type MetadataHandler, useMetadata } from 'features/metadata/parsing'; -import type { ReactNode } from 'react'; +import type { + CollectionMetadataHandler, + SingleMetadataHandler, + UnrecallableMetadataHandler, +} from 'features/metadata/parsing'; +import { + MetadataHanders, + useCollectionMetadataDatum, + useSingleMetadataDatum, + useUnrecallableMetadataDatum, +} from 'features/metadata/parsing'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import { PiArrowBendUpLeftBold } from 'react-icons/pi'; type Props = { @@ -22,53 +27,71 @@ const ImageMetadataActions = (props: Props) => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; export default memo(ImageMetadataActions); -const MetadataItem2 = typedMemo( - ({ metadata, handler, ...rest }: { metadata: unknown; handler: MetadataHandler } & FlexProps) => { - const { t } = useTranslation(); - const { data, recall } = useMetadata(metadata, handler); +const UnrecallableMetadataDatum = typedMemo( + ({ metadata, handler }: { metadata: unknown; handler: UnrecallableMetadataHandler }) => { + const { data } = useUnrecallableMetadataDatum(metadata, handler); if (!data.isParsed) { return null; } if (data.isSuccess) { - const label = handler.renderLabel(data.value, t); - const value = handler.renderValue(data.value, t); + const { LabelComponent, ValueComponent } = handler; + return ( + + + + + ); + } + } +); +UnrecallableMetadataDatum.displayName = 'UnrecallableMetadataDatum'; + +const SingleMetadataDatum = typedMemo( + ({ metadata, handler }: { metadata: unknown; handler: SingleMetadataHandler }) => { + const { data, recall } = useSingleMetadataDatum(metadata, handler); + + if (!data.isParsed) { + return null; + } + + if (data.isSuccess) { + const { LabelComponent, ValueComponent } = handler; return ( - - - - + + + + ); } } ); -MetadataItem2.displayName = 'MetadataItem2'; +SingleMetadataDatum.displayName = 'SingleMetadataDatum'; -const MetadataLabel = ({ label }: { label: ReactNode }) => { - if (isPrimitive(label)) { - return ( - - {label} - - ); - } else { - return <>{label}; - } -}; +const CollectionMetadataDatum = typedMemo( + ({ metadata, handler }: { metadata: unknown; handler: CollectionMetadataHandler }) => { + const { data, recallAll, recallItem } = useCollectionMetadataDatum(metadata, handler); -const MetadataValue = ({ value }: { value: ReactNode }) => { - if (isPrimitive(value)) { - return {value}; + if (!data.isParsed) { + return null; + } + + if (data.isSuccess) { + const { LabelComponent, ValueComponent } = handler; + + return ( + <> + {data.value.map((value, i) => ( + + } + size="xs" + variant="ghost" + onClick={() => recallItem(value)} + /> + + + + + + ))} + + ); + } } - return <>{value}; -}; +); +CollectionMetadataDatum.displayName = 'CollectionMetadataDatum'; 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 aa50498848..80ddca5d93 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -25,7 +25,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { // }); const { t } = useTranslation(); - const { metadata } = useDebouncedMetadata(image.image_name); + const { metadata, isLoading } = useDebouncedMetadata(image.image_name); const createdBy = useMetadataItem(metadata, handlers.createdBy); return ( @@ -58,7 +58,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { - {metadata ? ( + {metadata && !isLoading ? ( diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 7b7b60b806..77cb583cd7 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Text } from '@invoke-ai/ui-library'; import type { AppStore } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; import { withResultAsync } from 'common/util/result'; -import { get } from 'es-toolkit/compat'; +import { get, isArray, isString } from 'es-toolkit/compat'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; +import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/lorasSlice'; import { negativePrompt2Changed, negativePromptChanged, @@ -26,6 +30,7 @@ import { setSteps, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; +import type { LoRA } from 'features/controlLayers/store/types'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifier } from 'features/nodes/types/v2/common'; @@ -53,6 +58,7 @@ import type { ParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { + zLoRAWeight, zParameterCFGRescaleMultiplier, zParameterCFGScale, zParameterGuidance, @@ -71,13 +77,40 @@ import { zParameterSteps, zParameterStrength, } from 'features/parameters/types/parameterSchemas'; -import type { TFunction } from 'i18next'; -import { type ReactNode, useCallback, useEffect, useState } from 'react'; +import type { ComponentType } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { modelsApi } from 'services/api/endpoints/models'; -import type { ModelType } from 'services/api/types'; +import type { AnyModelConfig, ModelType } from 'services/api/types'; import { assert } from 'tsafe'; import z from 'zod/v4'; +const MetadataLabel = ({ i18nKey }: { i18nKey: string }) => { + const { t } = useTranslation(); + return ( + + {t(i18nKey)}: + + ); +}; + +const MetadataLabelWithCount = ({ i18nKey, i }: { i18nKey: string; i: number; values: T }) => { + const { t } = useTranslation(); + return ( + + {`${t(i18nKey)} ${i + 1}:`} + + ); +}; + +const MetadataPrimitiveValue = ({ value }: { value: string | number | boolean | null | undefined }) => { + return {value}; +}; + +const getProperty = (obj: unknown, path: string): unknown => { + return get(obj, path) as unknown; +}; + type UnparsedData = { isParsed: false; isSuccess: false; @@ -85,6 +118,13 @@ type UnparsedData = { value: null; error: null; }; +const buildUnparsedData = (): UnparsedData => ({ + isParsed: false, + isSuccess: false, + isError: false, + value: null, + error: null, +}); type ParsedSuccessData = { isParsed: true; @@ -93,6 +133,13 @@ type ParsedSuccessData = { value: T; error: null; }; +const buildParsedSuccessData = (value: T): ParsedSuccessData => ({ + isParsed: true, + isSuccess: true, + isError: false, + value, + error: null, +}); type ParsedErrorData = { isParsed: true; @@ -101,289 +148,342 @@ type ParsedErrorData = { value: null; error: Error; }; +const buildParsedErrorData = (error: Error): ParsedErrorData => ({ + isParsed: true, + isSuccess: false, + isError: true, + value: null, + error, +}); export type Data = UnparsedData | ParsedSuccessData | ParsedErrorData; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export type MetadataHandler = { +type SingleMetadataLabelProps = { + value: T; +}; +type SingleMetadataValueProps = { + value: T; +}; +export type SingleMetadataHandler = { type: string; parse: (metadata: unknown, store: AppStore) => Promise | T; - recall?: (value: T, store: AppStore) => void; - renderLabel: (value: T, t: TFunction) => ReactNode; - renderValue: (value: T, t: TFunction) => ReactNode; + recall: (value: T, store: AppStore) => void; + LabelComponent: ComponentType>; + ValueComponent: ComponentType>; +}; + +type CollectionMetadataLabelProps = { + values: T; + i: number; +}; +type CollectionMetadataValueProps = { + value: T[number]; +}; +export type CollectionMetadataHandler = { + type: string; + parse: (metadata: unknown, store: AppStore) => Promise | T; + recallAll: (values: T, store: AppStore) => void; + recallItem: (value: T[number], store: AppStore) => void; + LabelComponent: ComponentType>; + ValueComponent: ComponentType>; +}; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type UnrecallableMetadataLabelProps = { + value: T; +}; +type UnrecallableMetadataValueProps = { + value: T; +}; +export type UnrecallableMetadataHandler = { + type: string; + parse: (metadata: unknown, store: AppStore) => Promise | T; + LabelComponent: ComponentType>; + ValueComponent: ComponentType>; }; //#region Created By -const CreatedBy: MetadataHandler = { +const CreatedBy: UnrecallableMetadataHandler = { type: 'CreatedBy', parse: (metadata, _store) => { - const raw = get(metadata, 'created_by'); + const raw = getProperty(metadata, 'created_by'); const parsed = z.string().parse(raw); return parsed; }, - renderLabel: (_value, t) => t('metadata.createdBy'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: UnrecallableMetadataLabelProps) => , }; //#endregion Created By //#region Generation Mode -const GenerationMode: MetadataHandler = { +const GenerationMode: UnrecallableMetadataHandler = { type: 'GenerationMode', parse: (metadata, _store) => { - const raw = get(metadata, 'generation_mode'); + const raw = getProperty(metadata, 'generation_mode'); const parsed = z.string().parse(raw); return parsed; }, - renderLabel: (_value, t) => t('metadata.generationMode'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: UnrecallableMetadataLabelProps) => , }; //#endregion Generation Mode //#region Positive Prompt -const PositivePrompt: MetadataHandler = { +const PositivePrompt: SingleMetadataHandler = { type: 'PositivePrompt', parse: (metadata, _store) => { - const raw = get(metadata, 'positive_prompt'); + const raw = getProperty(metadata, 'positive_prompt'); const parsed = zParameterPositivePrompt.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(positivePromptChanged(value)); }, - renderLabel: (_value, t) => t('metadata.positivePrompt'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion Positive Prompt //#region Negative Prompt -const NegativePrompt: MetadataHandler = { +const NegativePrompt: SingleMetadataHandler = { type: 'NegativePrompt', parse: (metadata, _store) => { - const raw = get(metadata, 'negative_prompt'); + const raw = getProperty(metadata, 'negative_prompt'); const parsed = zParameterNegativePrompt.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(negativePromptChanged(value)); }, - renderLabel: (_value, t) => t('metadata.negativePrompt'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion Negative Prompt //#region SDXL Positive Style Prompt -const PositiveStylePrompt: MetadataHandler = { +const PositiveStylePrompt: SingleMetadataHandler = { type: 'PositiveStylePrompt', parse: (metadata, _store) => { - const raw = get(metadata, 'positive_style_prompt'); + const raw = getProperty(metadata, 'positive_style_prompt'); const parsed = zParameterPositiveStylePromptSDXL.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(positivePrompt2Changed(value)); }, - renderLabel: (_value, t) => t('sdxl.posStylePrompt'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion SDXL Positive Style Prompt //#region SDXL Negative Style Prompt -const NegativeStylePrompt: MetadataHandler = { +const NegativeStylePrompt: SingleMetadataHandler = { type: 'NegativeStylePrompt', parse: (metadata, _store) => { - const raw = get(metadata, 'negative_style_prompt'); + const raw = getProperty(metadata, 'negative_style_prompt'); const parsed = zParameterNegativeStylePromptSDXL.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(negativePrompt2Changed(value)); }, - renderLabel: (_value, t) => t('sdxl.negStylePrompt'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion SDXL Negative Style Prompt //#region CFG Scale -const CFGScale: MetadataHandler = { +const CFGScale: SingleMetadataHandler = { type: 'CFGScale', parse: (metadata, _store) => { - const raw = get(metadata, 'cfg_scale'); + const raw = getProperty(metadata, 'cfg_scale'); const parsed = zParameterCFGScale.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setCfgScale(value)); }, - renderLabel: (_value, t) => t('metadata.cfgScale'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion CFG Scale //#region CFG Rescale Multiplier -const CFGRescaleMultiplier: MetadataHandler = { +const CFGRescaleMultiplier: SingleMetadataHandler = { type: 'CFGRescaleMultiplier', parse: (metadata, _store) => { - const raw = get(metadata, 'cfg_rescale_multiplier'); + const raw = getProperty(metadata, 'cfg_rescale_multiplier'); const parsed = zParameterCFGRescaleMultiplier.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setCfgRescaleMultiplier(value)); }, - renderLabel: (_value, t) => t('metadata.cfgRescaleMultiplier'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion CFG Rescale Multiplier //#region Guidance -const Guidance: MetadataHandler = { +const Guidance: SingleMetadataHandler = { type: 'Guidance', parse: (metadata, _store) => { - const raw = get(metadata, 'guidance'); + const raw = getProperty(metadata, 'guidance'); const parsed = zParameterGuidance.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setGuidance(value)); }, - renderLabel: (_value, t) => t('metadata.guidance'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Guidance //#region Scheduler -const Scheduler: MetadataHandler = { +const Scheduler: SingleMetadataHandler = { type: 'Scheduler', parse: (metadata, _store) => { - const raw = get(metadata, 'scheduler'); + const raw = getProperty(metadata, 'scheduler'); const parsed = zParameterScheduler.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setScheduler(value)); }, - renderLabel: (_value, t) => t('metadata.scheduler'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Scheduler //#region Width -const Width: MetadataHandler = { +const Width: SingleMetadataHandler = { type: 'Width', parse: (metadata, _store) => { - const raw = get(metadata, 'width'); + const raw = getProperty(metadata, 'width'); const parsed = zParameterImageDimension.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true })); }, - renderLabel: (_value, t) => t('metadata.width'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Width //#region Height -const Height: MetadataHandler = { +const Height: SingleMetadataHandler = { type: 'Height', parse: (metadata, _store) => { - const raw = get(metadata, 'height'); + const raw = getProperty(metadata, 'height'); const parsed = zParameterImageDimension.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true })); }, - renderLabel: (_value, t) => t('metadata.height'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Height //#region Seed -const Seed: MetadataHandler = { +const Seed: SingleMetadataHandler = { type: 'Seed', parse: (metadata, _store) => { - const raw = get(metadata, 'seed'); + const raw = getProperty(metadata, 'seed'); const parsed = zParameterSeed.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setSeed(value)); }, - renderLabel: (_value, t) => t('metadata.seed'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Seed //#region Steps -const Steps: MetadataHandler = { +const Steps: SingleMetadataHandler = { type: 'Steps', parse: (metadata, _store) => { - const raw = get(metadata, 'steps'); + const raw = getProperty(metadata, 'steps'); const parsed = zParameterSteps.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setSteps(value)); }, - renderLabel: (_value, t) => t('metadata.steps'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion Steps //#region DenoisingStrength -const DenoisingStrength: MetadataHandler = { +const DenoisingStrength: SingleMetadataHandler = { type: 'DenoisingStrength', parse: (metadata, _store) => { - const raw = get(metadata, 'strength'); + const raw = getProperty(metadata, 'strength'); const parsed = zParameterStrength.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setImg2imgStrength(value)); }, - renderLabel: (_value, t) => t('metadata.strength'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion DenoisingStrength //#region SeamlessX -const SeamlessX: MetadataHandler = { +const SeamlessX: SingleMetadataHandler = { type: 'SeamlessX', parse: (metadata, _store) => { - const raw = get(metadata, 'seamless_x'); + const raw = getProperty(metadata, 'seamless_x'); const parsed = zParameterSeamlessX.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setSeamlessXAxis(value)); }, - renderLabel: (_value, t) => t('metadata.seamlessXAxis'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion SeamlessX //#region SeamlessY -const SeamlessY: MetadataHandler = { +const SeamlessY: SingleMetadataHandler = { type: 'SeamlessY', parse: (metadata, _store) => { - const raw = get(metadata, 'seamless_y'); + const raw = getProperty(metadata, 'seamless_y'); const parsed = zParameterSeamlessY.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setSeamlessYAxis(value)); }, - renderLabel: (_value, t) => t('metadata.seamlessYAxis'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion SeamlessY //#region RefinerModel -const RefinerModel: MetadataHandler = { +const RefinerModel: SingleMetadataHandler = { type: 'RefinerModel', parse: async (metadata, store) => { - const raw = get(metadata, 'refiner_model'); + const raw = getProperty(metadata, 'refiner_model'); const parsed = await parseModelIdentifier(raw, store, 'main'); assert(parsed.type === 'main'); assert(parsed.base === 'sdxl-refiner'); @@ -392,112 +492,120 @@ const RefinerModel: MetadataHandler = { recall: (value, store) => { store.dispatch(refinerModelChanged(value)); }, - renderLabel: (_value, t) => t('sdxl.refinermodel'), - renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion RefinerModel //#region RefinerSteps -const RefinerSteps: MetadataHandler = { +const RefinerSteps: SingleMetadataHandler = { type: 'RefinerSteps', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_steps'); + const raw = getProperty(metadata, 'refiner_steps'); const parsed = zParameterSteps.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerSteps(value)); }, - renderLabel: (_value, t) => t('sdxl.refinerSteps'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion RefinerSteps //#region RefinerCFGScale -const RefinerCFGScale: MetadataHandler = { +const RefinerCFGScale: SingleMetadataHandler = { type: 'RefinerCFGScale', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_cfg_scale'); + const raw = getProperty(metadata, 'refiner_cfg_scale'); const parsed = zParameterCFGScale.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerCFGScale(value)); }, - renderLabel: (_value, t) => t('sdxl.cfgScale'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion RefinerCFGScale //#region RefinerScheduler -const RefinerScheduler: MetadataHandler = { +const RefinerScheduler: SingleMetadataHandler = { type: 'RefinerScheduler', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_scheduler'); + const raw = getProperty(metadata, 'refiner_scheduler'); const parsed = zParameterScheduler.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerScheduler(value)); }, - renderLabel: (_value, t) => t('sdxl.scheduler'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => , }; //#endregion RefinerScheduler //#region RefinerPositiveAestheticScore -const RefinerPositiveAestheticScore: MetadataHandler = { +const RefinerPositiveAestheticScore: SingleMetadataHandler = { type: 'RefinerPositiveAestheticScore', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_positive_aesthetic_score'); + const raw = getProperty(metadata, 'refiner_positive_aesthetic_score'); const parsed = zParameterSDXLRefinerPositiveAestheticScore.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerPositiveAestheticScore(value)); }, - renderLabel: (_value, t) => t('sdxl.posAestheticScore'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion RefinerPositiveAestheticScore //#region RefinerNegativeAestheticScore -const RefinerNegativeAestheticScore: MetadataHandler = { +const RefinerNegativeAestheticScore: SingleMetadataHandler = { type: 'RefinerNegativeAestheticScore', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_negative_aesthetic_score'); + const raw = getProperty(metadata, 'refiner_negative_aesthetic_score'); const parsed = zParameterSDXLRefinerNegativeAestheticScore.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerNegativeAestheticScore(value)); }, - renderLabel: (_value, t) => t('sdxl.negAestheticScore'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion RefinerNegativeAestheticScore //#region RefinerDenoisingStart -const RefinerDenoisingStart: MetadataHandler = { +const RefinerDenoisingStart: SingleMetadataHandler = { type: 'RefinerDenoisingStart', parse: (metadata, _store) => { - const raw = get(metadata, 'refiner_start'); + const raw = getProperty(metadata, 'refiner_start'); const parsed = zParameterSDXLRefinerStart.parse(raw); return parsed; }, recall: (value, store) => { store.dispatch(setRefinerStart(value)); }, - renderLabel: (_value, t) => t('sdxl.refinerStart'), - renderValue: (value) => value, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion RefinerDenoisingStart //#region MainModel -const MainModel: MetadataHandler = { +const MainModel: SingleMetadataHandler = { type: 'MainModel', parse: async (metadata, store) => { - const raw = get(metadata, 'model'); + const raw = getProperty(metadata, 'model'); const parsed = await parseModelIdentifier(raw, store, 'main'); assert(parsed.type === 'main'); return parsed; @@ -505,16 +613,18 @@ const MainModel: MetadataHandler = { recall: (value, store) => { store.dispatch(modelSelected(value)); }, - renderLabel: (_value, t) => t('metadata.model'), - renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion MainModel //#region VAEModel -const VAEModel: MetadataHandler = { +const VAEModel: SingleMetadataHandler = { type: 'VAEModel', parse: async (metadata, store) => { - const raw = get(metadata, 'vae'); + const raw = getProperty(metadata, 'vae'); const parsed = await parseModelIdentifier(raw, store, 'vae'); assert(parsed.type === 'vae'); return parsed; @@ -522,11 +632,73 @@ const VAEModel: MetadataHandler = { recall: (value, store) => { store.dispatch(vaeSelected(value)); }, - renderLabel: (_value, t) => t('metadata.vae'), - renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`, + LabelComponent: () => , + ValueComponent: ({ value }: SingleMetadataValueProps) => ( + + ), }; //#endregion VAEModel +//#region LoRAs +const LoRAs: CollectionMetadataHandler = { + type: 'LoRAs', + parse: async (metadata, store) => { + const rawArray = getProperty(metadata, 'loras'); + assert(isArray(rawArray)); + + const loras: LoRA[] = []; + + for (const rawItem of rawArray) { + try { + let identifier: ModelIdentifierField | null = null; + try { + // New format - { model: ModelIdenfifierField } + const rawIdentifier = getProperty(rawItem, 'model'); + identifier = await parseModelIdentifier(rawIdentifier, store, 'lora'); + } catch { + // Old format - { lora : { key } } + const key = getProperty(rawItem, 'lora.key'); + assert(isString(key)); + const modelConfig = await getModelConfig(key, store); + identifier = zModelIdentifierField.parse(modelConfig); + } + assert(identifier.type === 'lora'); + const weight = getProperty(rawItem, 'weight'); + loras.push({ + id: getPrefixedId('lora'), + model: identifier, + weight: zLoRAWeight.parse(weight), + isEnabled: true, + }); + } catch { + continue; + } + } + + if (loras.length > 0) { + return loras; + } + + throw new Error('No valid LoRAs found in metadata'); + }, + recallItem: (value, store) => { + store.dispatch(loraRecalled({ lora: value })); + }, + recallAll: (values, store) => { + store.dispatch(loraAllDeleted()); + for (const lora of values) { + store.dispatch(loraRecalled({ lora })); + } + }, + LabelComponent: ({ values, i }: CollectionMetadataLabelProps) => ( + + ), + ValueComponent: ({ value }: CollectionMetadataValueProps) => ( + + ), +}; +//#endregion LoRAs + export const MetadataHanders = { CreatedBy, GenerationMode, @@ -554,9 +726,13 @@ export const MetadataHanders = { RefinerDenoisingStart, MainModel, VAEModel, -} satisfies Record; + LoRAs, +} satisfies Record< + string, + UnrecallableMetadataHandler | SingleMetadataHandler | CollectionMetadataHandler +>; -export function useMetadata(metadata: unknown, handler: MetadataHandler) { +export function useSingleMetadataDatum(metadata: unknown, handler: SingleMetadataHandler) { const store = useAppStore(); const [data, setData] = useState>(() => ({ isParsed: false, @@ -570,21 +746,9 @@ export function useMetadata(metadata: unknown, handler: MetadataHandler) { async (metadata: unknown) => { const result = await withResultAsync(async () => await Promise.resolve(handler.parse(metadata, store))); if (result.isOk()) { - setData({ - isParsed: true, - isSuccess: true, - isError: false, - value: result.value, - error: null, - }); + setData(buildParsedSuccessData(result.value)); } else { - setData({ - isParsed: true, - isSuccess: false, - isError: true, - value: null, - error: result.error, - }); + setData(buildParsedErrorData(result.error)); } }, [handler, store] @@ -604,28 +768,92 @@ export function useMetadata(metadata: unknown, handler: MetadataHandler) { return { data, recall }; } +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export function useCollectionMetadataDatum(metadata: unknown, handler: CollectionMetadataHandler) { + const store = useAppStore(); + const [data, setData] = useState>(buildUnparsedData); + + const parse = useCallback( + async (metadata: unknown) => { + const result = await withResultAsync(async () => await Promise.resolve(handler.parse(metadata, store))); + if (result.isOk()) { + setData(buildParsedSuccessData(result.value)); + } else { + setData(buildParsedErrorData(result.error)); + } + }, + [handler, store] + ); + + useEffect(() => { + parse(metadata); + }, [metadata, parse]); + + const recallAll = useCallback(() => { + if (!data.isSuccess) { + return; + } + handler.recallAll(data.value, store); + }, [data.isSuccess, data.value, handler, store]); + + const recallItem = useCallback( + (item: T) => { + handler.recallItem(item, store); + }, + [handler, store] + ); + + return { data, recallAll, recallItem }; +} + +export function useUnrecallableMetadataDatum(metadata: unknown, handler: UnrecallableMetadataHandler) { + const store = useAppStore(); + const [data, setData] = useState>(buildUnparsedData); + + const parse = useCallback( + async (metadata: unknown) => { + const result = await withResultAsync(async () => await Promise.resolve(handler.parse(metadata, store))); + if (result.isOk()) { + setData(buildParsedSuccessData(result.value)); + } else { + setData(buildParsedErrorData(result.error)); + } + }, + [handler, store] + ); + + useEffect(() => { + parse(metadata); + }, [metadata, parse]); + + return { data }; +} + +const getModelConfig = async (key: string, store: AppStore): Promise => { + const modelConfig = await store + .dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false })) + .unwrap(); + return modelConfig; +}; + const parseModelIdentifier = async (raw: unknown, store: AppStore, type: ModelType): Promise => { // First try the current format identifier: key, name, base, type, hash try { - const identifier = zModelIdentifierField.parse(raw); - const modelConfig = store - .dispatch(modelsApi.endpoints.getModelConfig.initiate(identifier.key, { subscribe: false })) - .unwrap(); + const { key } = zModelIdentifierField.parse(raw); + const req = store.dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false })); + const modelConfig = await req.unwrap(); return zModelIdentifierField.parse(modelConfig); } catch { // noop } + // Fall back to old format identifier: model_name, base_model try { - const identifier = zModelIdentifier.parse(raw); - const modelConfig = await store - .dispatch( - modelsApi.endpoints.getModelConfigByAttrs.initiate( - { name: identifier.model_name, base: identifier.base_model, type }, - { subscribe: false } - ) - ) - .unwrap(); + const { model_name: name, base_model: base } = zModelIdentifier.parse(raw); + const req = store.dispatch( + modelsApi.endpoints.getModelConfigByAttrs.initiate({ name, base, type }, { subscribe: false }) + ); + const modelConfig = await req.unwrap(); return zModelIdentifierField.parse(modelConfig); } catch { // noop