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