From cc161925c40855f7a0ded4513e679f9a0d247437 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Jun 2025 14:30:44 +0000 Subject: [PATCH 1/4] Add info button to staging area toolbar with generation details Co-authored-by: kent --- invokeai/frontend/web/public/locales/en.json | 3 +- .../StagingArea/SimpleStagingAreaToolbar.tsx | 2 + .../StagingAreaToolbarInfoButton.tsx | 153 ++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 1f71e39b9ea..61e85cbe2b9 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2340,7 +2340,8 @@ "next": "Next", "saveToGallery": "Save To Gallery", "showResultsOn": "Showing Results", - "showResultsOff": "Hiding Results" + "showResultsOff": "Hiding Results", + "info": "Info" } }, "upscaling": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx index 3fa5e3e0cab..7c9d5f8721b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx @@ -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(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx new file mode 100644 index 00000000000..71e1effbdfa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx @@ -0,0 +1,153 @@ +import { 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 { memo, useCallback } 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(); + + 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`; + }, []); + + if (!selectedItem) { + return ( + } + colorScheme="invokeBlue" + isDisabled={true} + /> + ); + } + + return ( + + + } + colorScheme="invokeBlue" + isDisabled={isDisabled} + /> + + + + + Generation Info + + + + + Status: + {' '} + {selectedItem.status} + + + + Item ID: + {' '} + {selectedItem.item_id} + + + + Priority: + {' '} + {selectedItem.priority} + + + {selectedItem.origin && ( + + + Origin: + {' '} + {selectedItem.origin} + + )} + + {selectedItem.destination && ( + + + Destination: + {' '} + {selectedItem.destination} + + )} + + + + Created: + {' '} + {formatTimestamp(selectedItem.created_at)} + + + {selectedItem.started_at && ( + + + Started: + {' '} + {formatTimestamp(selectedItem.started_at)} + + )} + + {selectedItem.completed_at && ( + + + Completed: + {' '} + {formatTimestamp(selectedItem.completed_at)} + + )} + + {selectedItem.started_at && selectedItem.completed_at && ( + + + Duration: + {' '} + {formatDuration(selectedItem.started_at, selectedItem.completed_at)} + + )} + + {selectedItem.credits && ( + + + Credits: + {' '} + {selectedItem.credits} + + )} + + {selectedItem.error_message && ( + + + Error: + {' '} + {selectedItem.error_message} + + )} + + + + + + ); +}); + +StagingAreaToolbarInfoButton.displayName = 'StagingAreaToolbarInfoButton'; From 74a1203a90583695add8a1aa23ce2c757bba6432 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:57:08 -0400 Subject: [PATCH 2/4] Consistency in handling logic --- .../StagingArea/StagingAreaToolbar.tsx | 2 + .../StagingAreaToolbarInfoButton.tsx | 227 ++++++++-------- .../metadata/components/MetadataItem.tsx | 33 ++- .../metadata/components/MetadataItemView.tsx | 244 +++++++++++++++++- .../metadata/components/MetadataLoRAs.tsx | 195 +++++++++++++- .../metadata/hooks/useMetadataExtraction.ts | 13 + .../metadata/util/metadataExtraction.ts | 67 +++++ 7 files changed, 648 insertions(+), 133 deletions(-) create mode 100644 invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts create mode 100644 invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 6b57e5cf93d..9fa3f5c9399 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -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(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx index 71e1effbdfa..ac7c757204e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx @@ -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} /> - - - - Generation Info - - - - - Status: - {' '} - {selectedItem.status} - - - - Item ID: - {' '} - {selectedItem.item_id} - - - - Priority: - {' '} - {selectedItem.priority} - - - {selectedItem.origin && ( - - - Origin: - {' '} - {selectedItem.origin} - + + + + {/* Prompts Section */} + + Prompts + + {metadata !== null && ( + <> + + + )} + - {selectedItem.destination && ( - - - Destination: - {' '} - {selectedItem.destination} - - )} + - - - Created: - {' '} - {formatTimestamp(selectedItem.created_at)} - + {/* Models and LoRAs Section - Left Column */} + + + {/* Model Section */} + + Model + + {metadata !== null && ( + + + + + )} + - {selectedItem.started_at && ( - - - Started: - {' '} - {formatTimestamp(selectedItem.started_at)} - - )} + {/* LoRA Section */} + {metadata !== null && ( + + )} + - {selectedItem.completed_at && ( - - - Completed: - {' '} - {formatTimestamp(selectedItem.completed_at)} - - )} + {/* Other Settings Section - Right Column */} + + Other Settings + + {metadata !== null && ( + + + + + + + )} + + - {selectedItem.started_at && selectedItem.completed_at && ( - - - Duration: - {' '} - {formatDuration(selectedItem.started_at, selectedItem.completed_at)} - - )} - - {selectedItem.credits && ( - - - Credits: - {' '} - {selectedItem.credits} - - )} - - {selectedItem.error_message && ( - - - Error: - {' '} - {selectedItem.error_message} - - )} - + {/* Error Section */} + {selectedItem.error_message && ( + <> + + + Error + + {selectedItem.error_message} + + + + )} diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx index cfa3ee3df34..c8149fe184c 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx @@ -9,10 +9,26 @@ type MetadataItemProps = { metadata: unknown; handlers: MetadataHandlers; 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(({ metadata, handlers, direction = 'row' }: MetadataItemProps) => { - const { label, isDisabled, value, renderedValue, onRecall } = useMetadataItem(metadata, handlers); +const _MetadataItem = typedMemo(({ + metadata, + handlers, + direction = 'row', + displayMode = 'default', + colorScheme = 'invokeBlue', + showCopy = false, + showRecall = true +}: MetadataItemProps) => { + const { label, isDisabled, value, renderedValue, onRecall, valueOrNull } = useMetadataItem(metadata, handlers); if (value === MetadataParseFailedToken) { return null; @@ -22,13 +38,24 @@ const _MetadataItem = typedMemo(({ 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 ( ); }); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx index 233e427798d..d0128982455 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx @@ -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 ( - - {onRecall && } - - - {label}: - - {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 ( + + {onRecall && } + + + {label}: + + {renderedValue} + - - ); + ); + } + + // Card display mode (for prompts) + if (displayMode === 'card') { + return ( + + + {label} + + + + {renderedValue} + + + {showCopy && ( + + } + onClick={handleCopy} + colorScheme="base" + variant="ghost" + aria-label={`Copy ${label} to clipboard`} + /> + + )} + {onRecall && ( + + )} + + + + ); + } + + // Simple display mode (for seed, steps, etc.) + if (displayMode === 'simple') { + return ( + + + {label} + + + + {renderedValue} + + + {showCopy && ( + + } + onClick={handleCopy} + colorScheme="base" + variant="ghost" + aria-label={`Copy ${label} to clipboard`} + /> + + )} + {onRecall && ( + + )} + + + + ); + } + + // Badge display mode (for models, etc.) + if (displayMode === 'badge') { + return ( + + + {label} + + + + {renderedValue} + + + {showCopy && ( + + } + onClick={handleCopy} + colorScheme="base" + variant="ghost" + aria-label={`Copy ${label} to clipboard`} + /> + + )} + {onRecall && ( + + )} + + + + ); + } + + return null; } ); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx index 1b184979898..578a8de3d64 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx @@ -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([]); + 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) => ( - - ))} - - ); + // Default display mode (original behavior) + if (displayMode === 'default') { + return ( + <> + {loras.map((lora) => ( + + ))} + + ); + } + + // Badge display mode (for staging area) + if (displayMode === 'badge') { + if (!loras || loras.length === 0) { + return null; + } + + return ( + + LoRAs + + {loras.map((lora: LoRA, index: number) => ( + + ))} + + + ); + } + + return null; }; const MetadataViewLoRA = ({ label, lora, handlers, + showRecall = true, }: { label: string; lora: LoRA; handlers: MetadataHandlers; + 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(null); useEffect(() => { @@ -66,5 +120,124 @@ const MetadataViewLoRA = ({ _renderValue(); }, [handlers, lora]); - return ; + return ( + + ); +}; + +const BadgeLoRA = ({ + lora, + index, + handlers, + showCopy = false, + showRecall = true, +}: { + lora: LoRA; + index: number; + handlers: MetadataHandlers; + showCopy?: boolean; + showRecall?: boolean; +}) => { + const [renderedValue, setRenderedValue] = useState(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 ( + + + LoRA {index + 1} + + + + {renderedValue} + + + {showCopy && ( + + } + onClick={handleCopy} + colorScheme="base" + variant="ghost" + aria-label={`Copy LoRA ${index + 1} to clipboard`} + /> + + )} + {showRecall && handlers.recallItem && ( + + )} + + + + ); }; diff --git a/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts b/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts new file mode 100644 index 00000000000..6a2a8bc5161 --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts @@ -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]); +}; \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts b/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts new file mode 100644 index 00000000000..c2fae90d429 --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts @@ -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 } } | 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; +}; \ No newline at end of file From e23c25513f28956eeb0b788d55683a5945919bd2 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:29:51 -0400 Subject: [PATCH 3/4] Linting fixes --- .../StagingAreaToolbarInfoButton.tsx | 104 +++++++++--------- .../metadata/components/MetadataItem.tsx | 72 ++++++------ .../metadata/components/MetadataItemView.tsx | 99 +++++++---------- .../metadata/components/MetadataLoRAs.tsx | 66 +++++------ .../metadata/hooks/useMetadataExtraction.ts | 4 +- .../metadata/util/metadataExtraction.ts | 42 +++++-- 6 files changed, 195 insertions(+), 192 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx index ac7c757204e..2d8b598ee35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarInfoButton.tsx @@ -1,4 +1,14 @@ -import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, Text, VStack, Divider, Grid } from '@invoke-ai/ui-library'; +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'; @@ -45,19 +55,21 @@ export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?: {/* Prompts Section */} - Prompts - + + Prompts + + {metadata !== null && ( <> - - @@ -72,20 +84,22 @@ export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?: {/* Model Section */} - Model - + + Model + + {metadata !== null && ( - - {/* LoRA Section */} - {metadata !== null && ( - - )} + {metadata !== null && } {/* Other Settings Section - Right Column */} - Other Settings - + + Other Settings + + {metadata !== null && ( - + + - - - @@ -144,13 +144,15 @@ export const StagingAreaToolbarInfoButton = memo(({ isDisabled }: { isDisabled?: <> - Error - + Error + + = { showRecall?: boolean; }; -const _MetadataItem = typedMemo(({ - metadata, - handlers, - direction = 'row', - displayMode = 'default', - colorScheme = 'invokeBlue', - showCopy = false, - showRecall = true -}: MetadataItemProps) => { - const { label, isDisabled, value, renderedValue, onRecall, valueOrNull } = useMetadataItem(metadata, handlers); +const _MetadataItem = typedMemo( + ({ + metadata, + handlers, + direction = 'row', + displayMode = 'default', + colorScheme = 'invokeBlue', + showCopy = false, + showRecall = true, + }: MetadataItemProps) => { + const { label, isDisabled, value, renderedValue, onRecall, valueOrNull } = useMetadataItem(metadata, handlers); - if (value === MetadataParseFailedToken) { - return null; - } - - if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) { - return null; - } + if (value === MetadataParseFailedToken) { + return null; + } - // For display modes other than default, we need the raw value for copy functionality - if (displayMode !== 'default') { - if (!valueOrNull) { + if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) { return null; } - } - return ( - - ); -}); + // For display modes other than default, we need the raw value for copy functionality + if (displayMode !== 'default') { + if (!valueOrNull) { + return null; + } + } + + return ( + + ); + } +); export const MetadataItem = typedMemo(_MetadataItem); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx index d0128982455..a1a4d359160 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx @@ -1,6 +1,6 @@ -import { Flex, Text, VStack, HStack, Badge, IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { RecallButton } from 'features/metadata/components/RecallButton'; +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, useCallback } from 'react'; import { PiCopyBold } from 'react-icons/pi'; @@ -21,21 +21,21 @@ type MetadataItemViewProps = { }; export const MetadataItemView = memo( - ({ - label, - onRecall, - isDisabled, - renderedValue, + ({ + label, + onRecall, + isDisabled, + renderedValue, direction = 'row', displayMode = 'default', colorScheme = 'invokeBlue', showCopy = false, - valueOrNull + valueOrNull, }: MetadataItemViewProps) => { const clipboard = useClipboard(); const handleCopy = useCallback(() => { - if (valueOrNull != null) { + if (valueOrNull !== null) { clipboard.writeText(String(valueOrNull)); } }, [clipboard, valueOrNull]); @@ -62,21 +62,21 @@ export const MetadataItemView = memo( {label} - - {renderedValue} - )} - {onRecall && ( - - )} + {onRecall && } @@ -126,19 +120,29 @@ export const MetadataItemView = memo( {label} - - + {renderedValue} - )} - {onRecall && ( - - )} + {onRecall && } @@ -179,26 +177,19 @@ export const MetadataItemView = memo( {label} - - + {renderedValue} - )} - {onRecall && ( - - )} + {onRecall && } diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx index 578a8de3d64..c3eaf64a499 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx @@ -1,11 +1,11 @@ +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 { 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 = { @@ -18,14 +18,8 @@ type Props = { showRecall?: boolean; }; -export const MetadataLoRAs = ({ - metadata, - displayMode = 'default', - showCopy = false, - showRecall = true -}: Props) => { +export const MetadataLoRAs = ({ metadata, displayMode = 'default', showCopy = false, showRecall = true }: Props) => { const [loras, setLoRAs] = useState([]); - const clipboard = useClipboard(); useEffect(() => { const parse = async () => { @@ -46,10 +40,10 @@ export const MetadataLoRAs = ({ return ( <> {loras.map((lora) => ( - @@ -66,10 +60,12 @@ export const MetadataLoRAs = ({ return ( - LoRAs + + LoRAs + {loras.map((lora: LoRA, index: number) => ( - ); }; @@ -144,7 +140,7 @@ const BadgeLoRA = ({ showRecall?: boolean; }) => { const [renderedValue, setRenderedValue] = useState(null); - const clipboard = useClipboard(); + const _clipboard = useClipboard(); useEffect(() => { const _renderValue = async () => { @@ -164,8 +160,8 @@ const BadgeLoRA = ({ }, [handlers, lora]); const handleCopy = useCallback(() => { - clipboard.writeText(`${lora.model.key} - ${lora.weight}`); - }, [clipboard, lora]); + _clipboard.writeText(`${lora.model.key} - ${lora.weight}`); + }, [_clipboard, lora]); const onRecall = useCallback(() => { if (!handlers.recallItem || !showRecall) { @@ -181,20 +177,20 @@ const BadgeLoRA = ({ LoRA {index + 1} - - {renderedValue} - )} {showRecall && handlers.recallItem && ( - + )} diff --git a/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts b/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts index 6a2a8bc5161..9bf3e208827 100644 --- a/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts +++ b/invokeai/frontend/web/src/features/metadata/hooks/useMetadataExtraction.ts @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; import { extractMetadata } from 'features/metadata/util/metadataExtraction'; +import { useMemo } from 'react'; /** * Hook for extracting metadata from different data structures @@ -10,4 +10,4 @@ export const useMetadataExtraction = (data: unknown): unknown => { return useMemo(() => { return extractMetadata(data); }, [data]); -}; \ No newline at end of file +}; diff --git a/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts b/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts index c2fae90d429..9b46c5added 100644 --- a/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts +++ b/invokeai/frontend/web/src/features/metadata/util/metadataExtraction.ts @@ -2,24 +2,46 @@ * 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 } } } => { + return ( + data !== null && + typeof data === 'object' && + 'session' in data && + typeof (data as Record).session === 'object' && + (data as Record).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 } } | null): unknown => { +export const extractMetadataFromSession = ( + session: { graph?: { nodes?: Record } } | 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:')); - + const metadataNodeKey = nodeKeys.find((key) => key.startsWith('core_metadata:')); + if (!metadataNodeKey) { return null; } - + return session.graph.nodes[metadataNodeKey]; }; @@ -43,16 +65,16 @@ export const extractMetadata = (data: unknown): unknown => { } // Try to extract from session graph - if ('session' in data && data.session && typeof data.session === 'object') { - const sessionMetadata = extractMetadataFromSession(data.session as any); + if (hasSession(data)) { + const sessionMetadata = extractMetadataFromSession(data.session); if (sessionMetadata) { return sessionMetadata; } } // Try to extract from image DTO - if ('metadata' in data) { - const imageMetadata = extractMetadataFromImage(data as any); + if (hasMetadata(data)) { + const imageMetadata = extractMetadataFromImage(data); if (imageMetadata) { return imageMetadata; } @@ -64,4 +86,4 @@ export const extractMetadata = (data: unknown): unknown => { } return null; -}; \ No newline at end of file +}; From 0b78772b5dbaf5e0e75880475fbde7ead7786740 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:21:34 -0400 Subject: [PATCH 4/4] Lints --- .../web/src/features/metadata/components/MetadataItemView.tsx | 2 +- .../web/src/features/metadata/components/MetadataLoRAs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx index a1a4d359160..9aa1a8a6060 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItemView.tsx @@ -1,4 +1,4 @@ -import { Badge, Flex, HStack, IconButton, Text, Tooltip,VStack } 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, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx index c3eaf64a499..feced56ad8e 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx @@ -1,4 +1,4 @@ -import { Badge, HStack, IconButton, Text, Tooltip,VStack } from '@invoke-ai/ui-library'; +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';