mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 07:18:05 -05:00
Compare commits
4 Commits
controlnet
...
metadata-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b78772b5d | ||
|
|
e23c25513f | ||
|
|
74a1203a90 | ||
|
|
cc161925c4 |
@@ -2340,7 +2340,8 @@
|
||||
"next": "Next",
|
||||
"saveToGallery": "Save To Gallery",
|
||||
"showResultsOn": "Showing Results",
|
||||
"showResultsOff": "Hiding Results"
|
||||
"showResultsOff": "Hiding Results",
|
||||
"info": "Info"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SimpleStagingAreaToolbarMenu } from 'features/controlLayers/components/
|
||||
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
|
||||
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
|
||||
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
|
||||
import { StagingAreaToolbarInfoButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton';
|
||||
import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
|
||||
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
|
||||
import { memo } from 'react';
|
||||
@@ -16,6 +17,7 @@ export const SimpleStagingAreaToolbar = memo(() => {
|
||||
<StagingAreaToolbarNextButton />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<StagingAreaToolbarInfoButton />
|
||||
<StagingAreaToolbarDiscardSelectedButton />
|
||||
<SimpleStagingAreaToolbarMenu />
|
||||
<StagingAreaToolbarDiscardAllButton />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/component
|
||||
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
|
||||
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
|
||||
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
|
||||
import { StagingAreaToolbarInfoButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton';
|
||||
import { StagingAreaToolbarMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenu';
|
||||
import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
|
||||
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
|
||||
@@ -42,6 +43,7 @@ export const StagingAreaToolbar = memo(() => {
|
||||
<StagingAreaToolbarAcceptButton />
|
||||
<StagingAreaToolbarToggleShowResultsButton />
|
||||
<StagingAreaToolbarSaveSelectedToGalleryButton />
|
||||
<StagingAreaToolbarInfoButton isDisabled={!shouldShowStagedImage} />
|
||||
<StagingAreaToolbarMenu />
|
||||
<StagingAreaToolbarDiscardSelectedButton isDisabled={!shouldShowStagedImage} />
|
||||
<StagingAreaToolbarDiscardAllButton isDisabled={!shouldShowStagedImage} />
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { MetadataItem } from 'features/metadata/components/MetadataItem';
|
||||
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
|
||||
import { useMetadataExtraction } from 'features/metadata/hooks/useMetadataExtraction';
|
||||
import { handlers } from 'features/metadata/util/handlers';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiInfoBold } from 'react-icons/pi';
|
||||
|
||||
export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
|
||||
const ctx = useCanvasSessionContext();
|
||||
const selectedItem = useStore(ctx.$selectedItem);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Extract metadata using the unified hook
|
||||
const metadata = useMetadataExtraction(selectedItem);
|
||||
|
||||
if (!selectedItem) {
|
||||
return (
|
||||
<IconButton
|
||||
tooltip={t('controlLayers.stagingArea.info')}
|
||||
aria-label={t('controlLayers.stagingArea.info')}
|
||||
icon={<PiInfoBold />}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement="top" isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
tooltip={t('controlLayers.stagingArea.info')}
|
||||
aria-label={t('controlLayers.stagingArea.info')}
|
||||
icon={<PiInfoBold />}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent maxW="500px" bg="base.900" borderColor="base.700">
|
||||
<PopoverBody p={4}>
|
||||
<VStack align="start" spacing={4} fontSize="sm">
|
||||
{/* Prompts Section */}
|
||||
<VStack align="start" spacing={3} w="full">
|
||||
<Text fontWeight="semibold" fontSize="md" color="base.100">
|
||||
Prompts
|
||||
</Text>
|
||||
|
||||
{metadata !== null && (
|
||||
<>
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.positivePrompt}
|
||||
displayMode="card"
|
||||
showCopy={true}
|
||||
/>
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.negativePrompt}
|
||||
displayMode="card"
|
||||
showCopy={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
<Divider borderColor="base.700" />
|
||||
|
||||
{/* Models and LoRAs Section - Left Column */}
|
||||
<Grid templateColumns="1fr 1fr" gap={6} w="full">
|
||||
<VStack align="start" spacing={4} w="full">
|
||||
{/* Model Section */}
|
||||
<VStack align="start" spacing={3} w="full">
|
||||
<Text fontWeight="semibold" fontSize="md" color="base.100">
|
||||
Model
|
||||
</Text>
|
||||
|
||||
{metadata !== null && (
|
||||
<VStack align="start" spacing={2} w="full">
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.model}
|
||||
displayMode="badge"
|
||||
colorScheme="invokeBlue"
|
||||
showCopy={true}
|
||||
/>
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.vae}
|
||||
displayMode="badge"
|
||||
colorScheme="base"
|
||||
showCopy={true}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
{/* LoRA Section */}
|
||||
{metadata !== null && <MetadataLoRAs metadata={metadata} displayMode="badge" showCopy={true} />}
|
||||
</VStack>
|
||||
|
||||
{/* Other Settings Section - Right Column */}
|
||||
<VStack align="start" spacing={3} w="full">
|
||||
<Text fontWeight="semibold" fontSize="md" color="base.100">
|
||||
Other Settings
|
||||
</Text>
|
||||
|
||||
{metadata !== null && (
|
||||
<VStack align="start" spacing={3} w="full">
|
||||
<MetadataItem metadata={metadata} handlers={handlers.seed} displayMode="simple" showCopy={true} />
|
||||
<MetadataItem metadata={metadata} handlers={handlers.steps} displayMode="simple" showCopy={true} />
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.cfgScale}
|
||||
displayMode="simple"
|
||||
showCopy={true}
|
||||
/>
|
||||
<MetadataItem
|
||||
metadata={metadata}
|
||||
handlers={handlers.scheduler}
|
||||
displayMode="simple"
|
||||
showCopy={true}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Grid>
|
||||
|
||||
{/* Error Section */}
|
||||
{selectedItem.error_message && (
|
||||
<>
|
||||
<Divider borderColor="base.700" />
|
||||
<VStack align="start" spacing={2} w="full">
|
||||
<Text fontWeight="semibold" fontSize="md" color="error.300">
|
||||
Error
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="error.200"
|
||||
bg="error.900"
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
w="full"
|
||||
border="1px solid"
|
||||
borderColor="error.700"
|
||||
>
|
||||
{selectedItem.error_message}
|
||||
</Text>
|
||||
</VStack>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarInfoButton.displayName = 'StagingAreaToolbarInfoButton';
|
||||
@@ -9,29 +9,58 @@ type MetadataItemProps<T> = {
|
||||
metadata: unknown;
|
||||
handlers: MetadataHandlers<T>;
|
||||
direction?: 'row' | 'column';
|
||||
/** Display mode for the metadata item */
|
||||
displayMode?: 'default' | 'badge' | 'simple' | 'card';
|
||||
/** Color scheme for badge display mode */
|
||||
colorScheme?: string;
|
||||
/** Whether to show copy functionality */
|
||||
showCopy?: boolean;
|
||||
/** Whether to show recall functionality */
|
||||
showRecall?: boolean;
|
||||
};
|
||||
|
||||
const _MetadataItem = typedMemo(<T,>({ metadata, handlers, direction = 'row' }: MetadataItemProps<T>) => {
|
||||
const { label, isDisabled, value, renderedValue, onRecall } = useMetadataItem(metadata, handlers);
|
||||
const _MetadataItem = typedMemo(
|
||||
<T,>({
|
||||
metadata,
|
||||
handlers,
|
||||
direction = 'row',
|
||||
displayMode = 'default',
|
||||
colorScheme = 'invokeBlue',
|
||||
showCopy = false,
|
||||
showRecall = true,
|
||||
}: MetadataItemProps<T>) => {
|
||||
const { label, isDisabled, value, renderedValue, onRecall, valueOrNull } = useMetadataItem(metadata, handlers);
|
||||
|
||||
if (value === MetadataParseFailedToken) {
|
||||
return null;
|
||||
if (value === MetadataParseFailedToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For display modes other than default, we need the raw value for copy functionality
|
||||
if (displayMode !== 'default') {
|
||||
if (!valueOrNull) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MetadataItemView
|
||||
label={label}
|
||||
onRecall={showRecall ? onRecall : undefined}
|
||||
isDisabled={isDisabled}
|
||||
renderedValue={renderedValue}
|
||||
direction={direction}
|
||||
displayMode={displayMode}
|
||||
colorScheme={colorScheme}
|
||||
showCopy={showCopy}
|
||||
valueOrNull={valueOrNull}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MetadataItemView
|
||||
label={label}
|
||||
onRecall={onRecall}
|
||||
isDisabled={isDisabled}
|
||||
renderedValue={renderedValue}
|
||||
direction={direction}
|
||||
/>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
export const MetadataItem = typedMemo(_MetadataItem);
|
||||
|
||||
|
||||
@@ -1,28 +1,229 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { Badge, Flex, HStack, IconButton, Text, Tooltip, VStack } from '@invoke-ai/ui-library';
|
||||
import { useClipboard } from 'common/hooks/useClipboard';
|
||||
import { RecallButton } from 'features/metadata/components/RecallButton';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
|
||||
type MetadataItemViewProps = {
|
||||
onRecall: () => void;
|
||||
onRecall?: () => void;
|
||||
label: string;
|
||||
renderedValue: React.ReactNode;
|
||||
isDisabled: boolean;
|
||||
direction?: 'row' | 'column';
|
||||
/** Display mode for the metadata item */
|
||||
displayMode?: 'default' | 'badge' | 'simple' | 'card';
|
||||
/** Color scheme for badge display mode */
|
||||
colorScheme?: string;
|
||||
/** Whether to show copy functionality */
|
||||
showCopy?: boolean;
|
||||
/** Raw value for copy functionality */
|
||||
valueOrNull?: unknown;
|
||||
};
|
||||
|
||||
export const MetadataItemView = memo(
|
||||
({ label, onRecall, isDisabled, renderedValue, direction = 'row' }: MetadataItemViewProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onRecall && <RecallButton label={label} onClick={onRecall} isDisabled={isDisabled} />}
|
||||
<Flex direction={direction} fontSize="sm">
|
||||
<Text fontWeight="semibold" whiteSpace="pre-wrap" pr={2}>
|
||||
{label}:
|
||||
</Text>
|
||||
{renderedValue}
|
||||
({
|
||||
label,
|
||||
onRecall,
|
||||
isDisabled,
|
||||
renderedValue,
|
||||
direction = 'row',
|
||||
displayMode = 'default',
|
||||
colorScheme = 'invokeBlue',
|
||||
showCopy = false,
|
||||
valueOrNull,
|
||||
}: MetadataItemViewProps) => {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
if (valueOrNull !== null) {
|
||||
clipboard.writeText(String(valueOrNull));
|
||||
}
|
||||
}, [clipboard, valueOrNull]);
|
||||
|
||||
// Default display mode (original behavior)
|
||||
if (displayMode === 'default') {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onRecall && <RecallButton label={label} onClick={onRecall} isDisabled={isDisabled} />}
|
||||
<Flex direction={direction} fontSize="sm">
|
||||
<Text fontWeight="semibold" whiteSpace="pre-wrap" pr={2}>
|
||||
{label}:
|
||||
</Text>
|
||||
{renderedValue}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
// Card display mode (for prompts)
|
||||
if (displayMode === 'card') {
|
||||
return (
|
||||
<VStack align="start" spacing={1} w="full">
|
||||
<Text fontSize="xs" fontWeight="medium" color="base.300" textTransform="uppercase" letterSpacing="wide">
|
||||
{label}
|
||||
</Text>
|
||||
<VStack
|
||||
position="relative"
|
||||
w="full"
|
||||
_hover={{
|
||||
'& .hover-actions': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
bg="base.800"
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
w="full"
|
||||
wordBreak="break-word"
|
||||
border="1px solid"
|
||||
borderColor="base.700"
|
||||
color="base.100"
|
||||
lineHeight="tall"
|
||||
>
|
||||
{renderedValue}
|
||||
</Text>
|
||||
<HStack
|
||||
className="hover-actions"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
opacity={0}
|
||||
transition="opacity 0.2s"
|
||||
spacing={1}
|
||||
>
|
||||
{showCopy && (
|
||||
<Tooltip label="Copy to clipboard">
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon={<PiCopyBold />}
|
||||
onClick={handleCopy}
|
||||
colorScheme="base"
|
||||
variant="ghost"
|
||||
aria-label={`Copy ${label} to clipboard`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onRecall && <RecallButton label={label} onClick={onRecall} isDisabled={isDisabled} />}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple display mode (for seed, steps, etc.)
|
||||
if (displayMode === 'simple') {
|
||||
return (
|
||||
<VStack align="start" spacing={1} w="full">
|
||||
<Text fontSize="xs" fontWeight="medium" color="base.300" textTransform="uppercase" letterSpacing="wide">
|
||||
{label}
|
||||
</Text>
|
||||
<VStack
|
||||
position="relative"
|
||||
w="full"
|
||||
_hover={{
|
||||
'& .hover-actions': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="base.100"
|
||||
fontFamily="mono"
|
||||
bg="base.800"
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
textAlign="center"
|
||||
>
|
||||
{renderedValue}
|
||||
</Text>
|
||||
<HStack
|
||||
className="hover-actions"
|
||||
position="absolute"
|
||||
top={1}
|
||||
right={1}
|
||||
opacity={0}
|
||||
transition="opacity 0.2s"
|
||||
spacing={1}
|
||||
>
|
||||
{showCopy && (
|
||||
<Tooltip label="Copy to clipboard">
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon={<PiCopyBold />}
|
||||
onClick={handleCopy}
|
||||
colorScheme="base"
|
||||
variant="ghost"
|
||||
aria-label={`Copy ${label} to clipboard`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onRecall && <RecallButton label={label} onClick={onRecall} isDisabled={isDisabled} />}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// Badge display mode (for models, etc.)
|
||||
if (displayMode === 'badge') {
|
||||
return (
|
||||
<VStack align="start" spacing={1} w="full">
|
||||
<Text fontSize="xs" fontWeight="medium" color="base.300" textTransform="uppercase" letterSpacing="wide">
|
||||
{label}
|
||||
</Text>
|
||||
<VStack
|
||||
position="relative"
|
||||
w="fit-content"
|
||||
_hover={{
|
||||
'& .hover-actions': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge colorScheme={colorScheme} variant="subtle" fontSize="sm" px={3} py={2} borderRadius="md">
|
||||
{renderedValue}
|
||||
</Badge>
|
||||
<HStack
|
||||
className="hover-actions"
|
||||
position="absolute"
|
||||
top={-2}
|
||||
right={-2}
|
||||
opacity={0}
|
||||
transition="opacity 0.2s"
|
||||
spacing={1}
|
||||
bg="base.900"
|
||||
borderRadius="md"
|
||||
p={1}
|
||||
border="1px solid"
|
||||
borderColor="base.700"
|
||||
shadow="lg"
|
||||
>
|
||||
{showCopy && (
|
||||
<Tooltip label="Copy to clipboard">
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon={<PiCopyBold />}
|
||||
onClick={handleCopy}
|
||||
colorScheme="base"
|
||||
variant="ghost"
|
||||
aria-label={`Copy ${label} to clipboard`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onRecall && <RecallButton label={label} onClick={onRecall} isDisabled={isDisabled} />}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { Badge, HStack, IconButton, Text, Tooltip, VStack } from '@invoke-ai/ui-library';
|
||||
import { useClipboard } from 'common/hooks/useClipboard';
|
||||
import type { LoRA } from 'features/controlLayers/store/types';
|
||||
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
|
||||
import { RecallButton } from 'features/metadata/components/RecallButton';
|
||||
import type { MetadataHandlers } from 'features/metadata/types';
|
||||
import { handlers } from 'features/metadata/util/handlers';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
metadata: unknown;
|
||||
/** Display mode for LoRA items */
|
||||
displayMode?: 'default' | 'badge';
|
||||
/** Whether to show copy functionality */
|
||||
showCopy?: boolean;
|
||||
/** Whether to show recall functionality */
|
||||
showRecall?: boolean;
|
||||
};
|
||||
|
||||
export const MetadataLoRAs = ({ metadata }: Props) => {
|
||||
export const MetadataLoRAs = ({ metadata, displayMode = 'default', showCopy = false, showRecall = true }: Props) => {
|
||||
const [loras, setLoRAs] = useState<LoRA[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,32 +35,72 @@ export const MetadataLoRAs = ({ metadata }: Props) => {
|
||||
|
||||
const label = useMemo(() => handlers.loras.getLabel(), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loras.map((lora) => (
|
||||
<MetadataViewLoRA key={lora.model.key} label={label} lora={lora} handlers={handlers.loras} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
// Default display mode (original behavior)
|
||||
if (displayMode === 'default') {
|
||||
return (
|
||||
<>
|
||||
{loras.map((lora) => (
|
||||
<MetadataViewLoRA
|
||||
key={lora.model.key}
|
||||
label={label}
|
||||
lora={lora}
|
||||
handlers={handlers.loras}
|
||||
showRecall={showRecall}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Badge display mode (for staging area)
|
||||
if (displayMode === 'badge') {
|
||||
if (!loras || loras.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack align="start" spacing={3} w="full">
|
||||
<Text fontWeight="semibold" fontSize="md" color="base.100">
|
||||
LoRAs
|
||||
</Text>
|
||||
<VStack align="start" spacing={2} w="full">
|
||||
{loras.map((lora: LoRA, index: number) => (
|
||||
<BadgeLoRA
|
||||
key={lora.id || index}
|
||||
lora={lora}
|
||||
index={index}
|
||||
handlers={handlers.loras}
|
||||
showCopy={showCopy}
|
||||
showRecall={showRecall}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const MetadataViewLoRA = ({
|
||||
label,
|
||||
lora,
|
||||
handlers,
|
||||
showRecall = true,
|
||||
}: {
|
||||
label: string;
|
||||
lora: LoRA;
|
||||
handlers: MetadataHandlers<LoRA[], LoRA>;
|
||||
showRecall?: boolean;
|
||||
}) => {
|
||||
const onRecall = useCallback(() => {
|
||||
if (!handlers.recallItem) {
|
||||
if (!handlers.recallItem || !showRecall) {
|
||||
return;
|
||||
}
|
||||
handlers.recallItem(lora, true).catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [handlers, lora]);
|
||||
}, [handlers, lora, showRecall]);
|
||||
|
||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
||||
useEffect(() => {
|
||||
@@ -66,5 +116,120 @@ const MetadataViewLoRA = ({
|
||||
_renderValue();
|
||||
}, [handlers, lora]);
|
||||
|
||||
return <MetadataItemView label={label} isDisabled={false} onRecall={onRecall} renderedValue={renderedValue} />;
|
||||
return (
|
||||
<MetadataItemView
|
||||
label={label}
|
||||
isDisabled={false}
|
||||
onRecall={showRecall ? onRecall : undefined}
|
||||
renderedValue={renderedValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const BadgeLoRA = ({
|
||||
lora,
|
||||
index,
|
||||
handlers,
|
||||
showCopy = false,
|
||||
showRecall = true,
|
||||
}: {
|
||||
lora: LoRA;
|
||||
index: number;
|
||||
handlers: MetadataHandlers<LoRA[], LoRA>;
|
||||
showCopy?: boolean;
|
||||
showRecall?: boolean;
|
||||
}) => {
|
||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
||||
const _clipboard = useClipboard();
|
||||
|
||||
useEffect(() => {
|
||||
const _renderValue = async () => {
|
||||
if (!handlers.renderItemValue) {
|
||||
setRenderedValue(`${lora.model.key} - ${lora.weight}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const rendered = await handlers.renderItemValue(lora);
|
||||
setRenderedValue(rendered);
|
||||
} catch {
|
||||
setRenderedValue(`${lora.model.key} - ${lora.weight}`);
|
||||
}
|
||||
};
|
||||
|
||||
_renderValue();
|
||||
}, [handlers, lora]);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
_clipboard.writeText(`${lora.model.key} - ${lora.weight}`);
|
||||
}, [_clipboard, lora]);
|
||||
|
||||
const onRecall = useCallback(() => {
|
||||
if (!handlers.recallItem || !showRecall) {
|
||||
return;
|
||||
}
|
||||
handlers.recallItem(lora, true).catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [handlers, lora, showRecall]);
|
||||
|
||||
return (
|
||||
<VStack align="start" spacing={1} w="full">
|
||||
<Text fontSize="xs" fontWeight="medium" color="base.300" textTransform="uppercase" letterSpacing="wide">
|
||||
LoRA {index + 1}
|
||||
</Text>
|
||||
<VStack
|
||||
position="relative"
|
||||
w="full"
|
||||
_hover={{
|
||||
'& .hover-actions': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
colorScheme="purple"
|
||||
variant="subtle"
|
||||
fontSize="sm"
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
textAlign="center"
|
||||
>
|
||||
{renderedValue}
|
||||
</Badge>
|
||||
<HStack
|
||||
className="hover-actions"
|
||||
position="absolute"
|
||||
top={-2}
|
||||
right={-2}
|
||||
opacity={0}
|
||||
transition="opacity 0.2s"
|
||||
spacing={1}
|
||||
bg="base.900"
|
||||
borderRadius="md"
|
||||
p={1}
|
||||
border="1px solid"
|
||||
borderColor="base.700"
|
||||
shadow="lg"
|
||||
>
|
||||
{showCopy && (
|
||||
<Tooltip label="Copy to clipboard">
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon={<PiCopyBold />}
|
||||
onClick={handleCopy}
|
||||
colorScheme="base"
|
||||
variant="ghost"
|
||||
aria-label={`Copy LoRA ${index + 1} to clipboard`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showRecall && handlers.recallItem && (
|
||||
<RecallButton label={handlers.getLabel()} onClick={onRecall} isDisabled={false} />
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { extractMetadata } from 'features/metadata/util/metadataExtraction';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Hook for extracting metadata from different data structures
|
||||
* @param data The data object that might contain metadata
|
||||
* @returns The extracted metadata or null if not found
|
||||
*/
|
||||
export const useMetadataExtraction = (data: unknown): unknown => {
|
||||
return useMemo(() => {
|
||||
return extractMetadata(data);
|
||||
}, [data]);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Utility functions for extracting metadata from different sources
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type guard to check if an object has a session property
|
||||
*/
|
||||
const hasSession = (data: unknown): data is { session: { graph?: { nodes?: Record<string, unknown> } } } => {
|
||||
return (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
'session' in data &&
|
||||
typeof (data as Record<string, unknown>).session === 'object' &&
|
||||
(data as Record<string, unknown>).session !== null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if an object has a metadata property
|
||||
*/
|
||||
const hasMetadata = (data: unknown): data is { metadata: unknown } => {
|
||||
return data !== null && typeof data === 'object' && 'metadata' in data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts metadata from a session graph
|
||||
* @param session The session object containing the graph
|
||||
* @returns The extracted metadata or null if not found
|
||||
*/
|
||||
export const extractMetadataFromSession = (
|
||||
session: { graph?: { nodes?: Record<string, unknown> } } | null
|
||||
): unknown => {
|
||||
if (!session?.graph?.nodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the metadata node (core_metadata with unique suffix)
|
||||
const nodeKeys = Object.keys(session.graph.nodes);
|
||||
const metadataNodeKey = nodeKeys.find((key) => key.startsWith('core_metadata:'));
|
||||
|
||||
if (!metadataNodeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.graph.nodes[metadataNodeKey];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts metadata from an image DTO
|
||||
* @param image The image DTO object
|
||||
* @returns The extracted metadata or null if not found
|
||||
*/
|
||||
export const extractMetadataFromImage = (image: { metadata?: unknown } | null): unknown => {
|
||||
return image?.metadata || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic metadata extraction that works with different data structures
|
||||
* @param data The data object that might contain metadata
|
||||
* @returns The extracted metadata or null if not found
|
||||
*/
|
||||
export const extractMetadata = (data: unknown): unknown => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to extract from session graph
|
||||
if (hasSession(data)) {
|
||||
const sessionMetadata = extractMetadataFromSession(data.session);
|
||||
if (sessionMetadata) {
|
||||
return sessionMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from image DTO
|
||||
if (hasMetadata(data)) {
|
||||
const imageMetadata = extractMetadataFromImage(data);
|
||||
if (imageMetadata) {
|
||||
return imageMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// If the data itself looks like metadata, return it
|
||||
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user