mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Consistency in handling logic
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, Text, VStack } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, Text, VStack, Divider, Grid } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
|
||||
import { memo, useCallback } from 'react';
|
||||
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';
|
||||
|
||||
@@ -10,20 +14,8 @@ export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?:
|
||||
const selectedItem = useStore(ctx.$selectedItem);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formatTimestamp = useCallback((timestamp: string | null | undefined) => {
|
||||
if (!timestamp) {
|
||||
return 'N/A';
|
||||
}
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}, []);
|
||||
|
||||
const formatDuration = useCallback((start: string | null | undefined, end: string | null | undefined) => {
|
||||
if (!start || !end) {
|
||||
return 'N/A';
|
||||
}
|
||||
const duration = new Date(end).getTime() - new Date(start).getTime();
|
||||
return `${(duration / 1000).toFixed(2)}s`;
|
||||
}, []);
|
||||
// Extract metadata using the unified hook
|
||||
const metadata = useMetadataExtraction(selectedItem);
|
||||
|
||||
if (!selectedItem) {
|
||||
return (
|
||||
@@ -48,101 +40,126 @@ export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?:
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent maxW="300px">
|
||||
<PopoverBody>
|
||||
<VStack align="start" spacing={2} fontSize="sm">
|
||||
<Text fontWeight="bold">Generation Info</Text>
|
||||
|
||||
<VStack align="start" spacing={1} w="full">
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Status:
|
||||
</Text>{' '}
|
||||
{selectedItem.status}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Item ID:
|
||||
</Text>{' '}
|
||||
{selectedItem.item_id}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Priority:
|
||||
</Text>{' '}
|
||||
{selectedItem.priority}
|
||||
</Text>
|
||||
|
||||
{selectedItem.origin && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Origin:
|
||||
</Text>{' '}
|
||||
{selectedItem.origin}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{selectedItem.destination && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Destination:
|
||||
</Text>{' '}
|
||||
{selectedItem.destination}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Created:
|
||||
</Text>{' '}
|
||||
{formatTimestamp(selectedItem.created_at)}
|
||||
</Text>
|
||||
|
||||
{selectedItem.started_at && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Started:
|
||||
</Text>{' '}
|
||||
{formatTimestamp(selectedItem.started_at)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{selectedItem.completed_at && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Completed:
|
||||
</Text>{' '}
|
||||
{formatTimestamp(selectedItem.completed_at)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{selectedItem.started_at && selectedItem.completed_at && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Duration:
|
||||
</Text>{' '}
|
||||
{formatDuration(selectedItem.started_at, selectedItem.completed_at)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{selectedItem.credits && (
|
||||
<Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Credits:
|
||||
</Text>{' '}
|
||||
{selectedItem.credits}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{selectedItem.error_message && (
|
||||
<Text color="error.300">
|
||||
<Text as="span" fontWeight="semibold">
|
||||
Error:
|
||||
</Text>{' '}
|
||||
{selectedItem.error_message}
|
||||
</Text>
|
||||
<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>
|
||||
|
||||
@@ -9,10 +9,26 @@ 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;
|
||||
@@ -22,13 +38,24 @@ const _MetadataItem = typedMemo(<T,>({ metadata, handlers, direction = 'row' }:
|
||||
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={onRecall}
|
||||
onRecall={showRecall ? onRecall : undefined}
|
||||
isDisabled={isDisabled}
|
||||
renderedValue={renderedValue}
|
||||
direction={direction}
|
||||
displayMode={displayMode}
|
||||
colorScheme={colorScheme}
|
||||
showCopy={showCopy}
|
||||
valueOrNull={valueOrNull}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,28 +1,244 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { Flex, Text, VStack, HStack, Badge, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { RecallButton } from 'features/metadata/components/RecallButton';
|
||||
import { memo } from 'react';
|
||||
import { useClipboard } from 'common/hooks/useClipboard';
|
||||
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,15 +1,31 @@
|
||||
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 { VStack, Text, Badge, HStack, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useClipboard } from 'common/hooks/useClipboard';
|
||||
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[]>([]);
|
||||
const clipboard = useClipboard();
|
||||
|
||||
useEffect(() => {
|
||||
const parse = async () => {
|
||||
@@ -25,32 +41,70 @@ 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 +120,124 @@ 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 { useMemo } from 'react';
|
||||
import { extractMetadata } from 'features/metadata/util/metadataExtraction';
|
||||
|
||||
/**
|
||||
* 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,67 @@
|
||||
/**
|
||||
* Utility functions for extracting metadata from different sources
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 ('session' in data && data.session && typeof data.session === 'object') {
|
||||
const sessionMetadata = extractMetadataFromSession(data.session as any);
|
||||
if (sessionMetadata) {
|
||||
return sessionMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from image DTO
|
||||
if ('metadata' in data) {
|
||||
const imageMetadata = extractMetadataFromImage(data as any);
|
||||
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