Compare commits

...

4 Commits

Author SHA1 Message Date
Kent Keirsey
0b78772b5d Lints 2025-06-30 10:21:34 -04:00
Kent Keirsey
e23c25513f Linting fixes 2025-06-27 15:29:51 -04:00
Kent Keirsey
74a1203a90 Consistency in handling logic 2025-06-27 14:57:08 -04:00
Cursor Agent
cc161925c4 Add info button to staging area toolbar with generation details
Co-authored-by: kent <kent@invoke.ai>
2025-06-27 14:30:44 +00:00
9 changed files with 719 additions and 45 deletions

View File

@@ -2340,7 +2340,8 @@
"next": "Next",
"saveToGallery": "Save To Gallery",
"showResultsOn": "Showing Results",
"showResultsOff": "Hiding Results"
"showResultsOff": "Hiding Results",
"info": "Info"
}
},
"upscaling": {

View File

@@ -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 />

View File

@@ -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} />

View File

@@ -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';

View File

@@ -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);

View File

@@ -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;
}
);

View File

@@ -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>
);
};

View File

@@ -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]);
};

View File

@@ -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;
};