Skip to content

Commit 729bae1

Browse files
authored
Feat(UI): Search bar in image info code tabs and add vertical margins for improved UX in Recall Parameters tab. (#8786)
* Adjusted Search bar position and added padding in image info viewer. * Minor bug fix with spaces being highlighted.
1 parent fcc81f1 commit 729bae1

File tree

5 files changed

+246
-33
lines changed

5 files changed

+246
-33
lines changed

invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
import type { FlexProps } from '@invoke-ai/ui-library';
2-
import { Box, chakra, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
2+
import {
3+
Box,
4+
chakra,
5+
Flex,
6+
IconButton,
7+
Input,
8+
InputGroup,
9+
InputRightElement,
10+
Tooltip,
11+
useShiftModifier,
12+
} from '@invoke-ai/ui-library';
313
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
414
import { useClipboard } from 'common/hooks/useClipboard';
515
import { isString } from 'es-toolkit/compat';
616
import { Formatter, TableCommaPlacement } from 'fracturedjsonjs';
717
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
8-
import type { CSSProperties } from 'react';
9-
import { memo, useCallback, useMemo } from 'react';
18+
import type { ChangeEvent, CSSProperties } from 'react';
19+
import { memo, useCallback, useMemo, useState } from 'react';
1020
import { useTranslation } from 'react-i18next';
11-
import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi';
21+
import { PiCopyBold, PiDownloadSimpleBold, PiXBold } from 'react-icons/pi';
1222

1323
const formatter = new Formatter();
1424
formatter.Options.TableCommaPlacement = TableCommaPlacement.BeforePadding;
@@ -22,6 +32,10 @@ type Props = {
2232
withCopy?: boolean;
2333
extraCopyActions?: { label: string; getData: (data: unknown) => unknown }[];
2434
wrapData?: boolean;
35+
withSearch?: boolean;
36+
searchTerm?: string;
37+
onSearchTermChange?: (value: string) => void;
38+
showSearchInput?: boolean;
2539
} & FlexProps;
2640

2741
const overlayscrollbarsOptions = getOverlayScrollbarsParams({
@@ -40,11 +54,18 @@ const DataViewer = (props: Props) => {
4054
withCopy = true,
4155
extraCopyActions,
4256
wrapData = true,
57+
withSearch = false,
58+
searchTerm: searchTermProp,
59+
onSearchTermChange,
60+
showSearchInput = true,
4361
...rest
4462
} = props;
4563
const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]);
4664
const shift = useShiftModifier();
4765
const clipboard = useClipboard();
66+
const [internalSearchTerm, setInternalSearchTerm] = useState('');
67+
const isControlledSearch = searchTermProp !== undefined;
68+
const searchTerm = isControlledSearch ? searchTermProp : internalSearchTerm;
4869
const handleCopy = useCallback(() => {
4970
clipboard.writeText(dataString);
5071
}, [clipboard, dataString]);
@@ -61,14 +82,75 @@ const DataViewer = (props: Props) => {
6182

6283
const { t } = useTranslation();
6384

85+
const highlightedDataString = useMemo(() => {
86+
const trimmedSearchTerm = searchTerm.trim();
87+
if (!trimmedSearchTerm) {
88+
return dataString;
89+
}
90+
91+
const regex = new RegExp(`(${escapeRegExp(trimmedSearchTerm)})`, 'gi');
92+
const parts = dataString.split(regex);
93+
94+
return parts.map((part, index) => {
95+
const isMatch = index % 2 === 1;
96+
if (!isMatch) {
97+
return <span key={index}>{part}</span>;
98+
}
99+
return (
100+
<chakra.mark key={index} bg="accent.700" color="accent.50" px={1} borderRadius="sm">
101+
{part}
102+
</chakra.mark>
103+
);
104+
});
105+
}, [dataString, searchTerm]);
106+
107+
const setSearchTerm = useCallback(
108+
(value: string) => {
109+
if (isControlledSearch) {
110+
onSearchTermChange?.(value);
111+
return;
112+
}
113+
setInternalSearchTerm(value);
114+
},
115+
[isControlledSearch, onSearchTermChange]
116+
);
117+
118+
const handleChangeSearch = useCallback(
119+
(e: ChangeEvent<HTMLInputElement>) => {
120+
setSearchTerm(e.target.value);
121+
},
122+
[setSearchTerm]
123+
);
124+
125+
const handleClearSearch = useCallback(() => {
126+
setSearchTerm('');
127+
}, [setSearchTerm]);
128+
64129
return (
65130
<Flex bg="base.800" borderRadius="base" flexGrow={1} w="full" h="full" position="relative" {...rest}>
66131
<Box position="absolute" top={0} left={0} right={0} bottom={0} overflow="auto" p={2} fontSize="sm">
67132
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayscrollbarsOptions}>
68-
<ChakraPre whiteSpace={wrapData ? 'pre-wrap' : undefined}>{dataString}</ChakraPre>
133+
<ChakraPre whiteSpace={wrapData ? 'pre-wrap' : undefined}>{highlightedDataString}</ChakraPre>
69134
</OverlayScrollbarsComponent>
70135
</Box>
71-
<Flex position="absolute" top={0} insetInlineEnd={0} p={2}>
136+
<Flex position="absolute" top={0} insetInlineEnd={0} p={2} gap={2} alignItems="center">
137+
{withSearch && showSearchInput && (
138+
<InputGroup size="sm" w={48}>
139+
<Input placeholder={t('common.search')} value={searchTerm} onChange={handleChangeSearch} />
140+
{searchTerm && (
141+
<InputRightElement h="full" pe={2}>
142+
<IconButton
143+
aria-label={t('boards.clearSearch')}
144+
icon={<PiXBold size={16} />}
145+
variant="link"
146+
opacity={0.7}
147+
onClick={handleClearSearch}
148+
size="sm"
149+
/>
150+
</InputRightElement>
151+
)}
152+
</InputGroup>
153+
)}
72154
{withDownload && (
73155
<Tooltip label={`${t('gallery.download')} ${label} JSON`}>
74156
<IconButton
@@ -131,3 +213,5 @@ const ExtraCopyAction = ({ label, data, getData }: ExtraCopyActionProps) => {
131213
</Tooltip>
132214
);
133215
};
216+
217+
const escapeRegExp = (value: string) => value.replace(/[-/\\^$*+?.()|[\]{}]/g, '$&');

invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import DataViewer from './DataViewer';
88

99
type Props = {
1010
image: ImageDTO;
11+
searchTerm?: string;
12+
showSearchInput?: boolean;
1113
};
1214

13-
const ImageMetadataGraphTabContent = ({ image }: Props) => {
15+
const ImageMetadataGraphTabContent = ({ image, searchTerm, showSearchInput }: Props) => {
1416
const { t } = useTranslation();
1517
const { currentData } = useDebouncedImageWorkflow(image);
1618
const graph = useMemo(() => {
@@ -29,7 +31,14 @@ const ImageMetadataGraphTabContent = ({ image }: Props) => {
2931
}
3032

3133
return (
32-
<DataViewer fileName={`${image.image_name.replace('.png', '')}_graph`} data={graph} label={t('nodes.graph')} />
34+
<DataViewer
35+
fileName={`${image.image_name.replace('.png', '')}_graph`}
36+
data={graph}
37+
label={t('nodes.graph')}
38+
withSearch
39+
searchTerm={searchTerm}
40+
showSearchInput={showSearchInput}
41+
/>
3342
);
3443
};
3544

invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx

Lines changed: 125 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
1+
import {
2+
ExternalLink,
3+
Flex,
4+
IconButton,
5+
Input,
6+
InputGroup,
7+
InputRightElement,
8+
Tab,
9+
TabList,
10+
TabPanel,
11+
TabPanels,
12+
Tabs,
13+
} from '@invoke-ai/ui-library';
214
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
315
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
416
import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent';
517
import { ImageMetadataHandlers } from 'features/metadata/parsing';
6-
import { memo } from 'react';
18+
import type { ChangeEvent } from 'react';
19+
import { memo, useCallback, useState } from 'react';
720
import { useTranslation } from 'react-i18next';
21+
import { PiXBold } from 'react-icons/pi';
822
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
923
import type { ImageDTO } from 'services/api/types';
1024

@@ -16,6 +30,16 @@ type ImageMetadataViewerProps = {
1630
image: ImageDTO;
1731
};
1832

33+
const CODE_TAB_PADDING_INLINE = 18;
34+
const TAB_INDEX = {
35+
recall: 0,
36+
metadata: 1,
37+
imageDetails: 2,
38+
workflow: 3,
39+
graph: 4,
40+
} as const;
41+
const TAB_COUNT = Object.keys(TAB_INDEX).length;
42+
1943
const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
2044
// TODO: fix hotkeys
2145
// const dispatch = useAppDispatch();
@@ -25,11 +49,40 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
2549
const { t } = useTranslation();
2650

2751
const { metadata, isLoading } = useDebouncedMetadata(image.image_name);
52+
const [activeTabIndex, setActiveTabIndex] = useState(0);
53+
const [searchTerms, setSearchTerms] = useState<string[]>(() => Array(TAB_COUNT).fill(''));
54+
const isSearchableTab = activeTabIndex !== TAB_INDEX.recall;
55+
const activeSearchTerm = searchTerms[activeTabIndex] ?? '';
56+
57+
const handleTabChange = useCallback((index: number) => {
58+
setActiveTabIndex(index);
59+
}, []);
60+
61+
const handleChangeSearch = useCallback(
62+
(e: ChangeEvent<HTMLInputElement>) => {
63+
const value = e.target.value;
64+
setSearchTerms((prev) => {
65+
const next = [...prev];
66+
next[activeTabIndex] = value;
67+
return next;
68+
});
69+
},
70+
[activeTabIndex]
71+
);
72+
73+
const handleClearSearch = useCallback(() => {
74+
setSearchTerms((prev) => {
75+
const next = [...prev];
76+
next[activeTabIndex] = '';
77+
return next;
78+
});
79+
}, [activeTabIndex]);
2880

2981
return (
3082
<Flex
3183
layerStyle="first"
3284
padding={4}
85+
paddingInline={16}
3386
gap={1}
3487
flexDirection="column"
3588
width="full"
@@ -41,14 +94,42 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
4194
<ExternalLink href={image.image_url} label={image.image_name} />
4295
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CreatedBy} />
4396

44-
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
45-
<TabList>
46-
<Tab>{t('metadata.recallParameters')}</Tab>
47-
<Tab>{t('metadata.metadata')}</Tab>
48-
<Tab>{t('metadata.imageDetails')}</Tab>
49-
<Tab>{t('metadata.workflow')}</Tab>
50-
<Tab>{t('nodes.graph')}</Tab>
51-
</TabList>
97+
<Tabs
98+
variant="line"
99+
isLazy={true}
100+
display="flex"
101+
flexDir="column"
102+
w="full"
103+
h="full"
104+
index={activeTabIndex}
105+
onChange={handleTabChange}
106+
>
107+
<Flex alignItems="flex-start" gap={2} borderBottomWidth="1px" borderColor="base.600">
108+
<TabList flex="1" pb={2} borderBottom="none">
109+
<Tab>{t('metadata.recallParameters')}</Tab>
110+
<Tab>{t('metadata.metadata')}</Tab>
111+
<Tab>{t('metadata.imageDetails')}</Tab>
112+
<Tab>{t('metadata.workflow')}</Tab>
113+
<Tab>{t('nodes.graph')}</Tab>
114+
</TabList>
115+
{isSearchableTab && (
116+
<InputGroup size="sm" w={48} me={6}>
117+
<Input placeholder={t('common.search')} value={activeSearchTerm} onChange={handleChangeSearch} />
118+
{activeSearchTerm && (
119+
<InputRightElement h="full" pe={2}>
120+
<IconButton
121+
aria-label={t('boards.clearSearch')}
122+
icon={<PiXBold size={16} />}
123+
variant="link"
124+
opacity={0.7}
125+
onClick={handleClearSearch}
126+
size="sm"
127+
/>
128+
</InputRightElement>
129+
)}
130+
</InputGroup>
131+
)}
132+
</Flex>
52133

53134
<TabPanels>
54135
<TabPanel>
@@ -62,31 +143,53 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
62143
</TabPanel>
63144
<TabPanel>
64145
{metadata ? (
65-
<DataViewer
66-
fileName={`${image.image_name.replace('.png', '')}_metadata`}
67-
data={metadata}
68-
label={t('metadata.metadata')}
69-
/>
146+
<Flex w="full" h="full" paddingInline={CODE_TAB_PADDING_INLINE}>
147+
<DataViewer
148+
fileName={`${image.image_name.replace('.png', '')}_metadata`}
149+
data={metadata}
150+
label={t('metadata.metadata')}
151+
withSearch
152+
searchTerm={searchTerms[TAB_INDEX.metadata]}
153+
showSearchInput={false}
154+
/>
155+
</Flex>
70156
) : (
71157
<IAINoContentFallback label={t('metadata.noMetaData')} />
72158
)}
73159
</TabPanel>
74160
<TabPanel>
75161
{image ? (
76-
<DataViewer
77-
fileName={`${image.image_name.replace('.png', '')}_details`}
78-
data={image}
79-
label={t('metadata.imageDetails')}
80-
/>
162+
<Flex w="full" h="full" paddingInline={CODE_TAB_PADDING_INLINE}>
163+
<DataViewer
164+
fileName={`${image.image_name.replace('.png', '')}_details`}
165+
data={image}
166+
label={t('metadata.imageDetails')}
167+
withSearch
168+
searchTerm={searchTerms[TAB_INDEX.imageDetails]}
169+
showSearchInput={false}
170+
/>
171+
</Flex>
81172
) : (
82173
<IAINoContentFallback label={t('metadata.noImageDetails')} />
83174
)}
84175
</TabPanel>
85176
<TabPanel>
86-
<ImageMetadataWorkflowTabContent image={image} />
177+
<Flex w="full" h="full" paddingInline={CODE_TAB_PADDING_INLINE}>
178+
<ImageMetadataWorkflowTabContent
179+
image={image}
180+
searchTerm={searchTerms[TAB_INDEX.workflow]}
181+
showSearchInput={false}
182+
/>
183+
</Flex>
87184
</TabPanel>
88185
<TabPanel>
89-
<ImageMetadataGraphTabContent image={image} />
186+
<Flex w="full" h="full" paddingInline={CODE_TAB_PADDING_INLINE}>
187+
<ImageMetadataGraphTabContent
188+
image={image}
189+
searchTerm={searchTerms[TAB_INDEX.graph]}
190+
showSearchInput={false}
191+
/>
192+
</Flex>
90193
</TabPanel>
91194
</TabPanels>
92195
</Tabs>

invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import DataViewer from './DataViewer';
88

99
type Props = {
1010
image: ImageDTO;
11+
searchTerm?: string;
12+
showSearchInput?: boolean;
1113
};
1214

13-
const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
15+
const ImageMetadataWorkflowTabContent = ({ image, searchTerm, showSearchInput }: Props) => {
1416
const { t } = useTranslation();
1517
const { currentData } = useDebouncedImageWorkflow(image);
1618
const workflow = useMemo(() => {
@@ -33,6 +35,9 @@ const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
3335
fileName={`${image.image_name.replace('.png', '')}_workflow`}
3436
data={workflow}
3537
label={t('metadata.workflow')}
38+
withSearch
39+
searchTerm={searchTerm}
40+
showSearchInput={showSearchInput}
3641
/>
3742
);
3843
};

0 commit comments

Comments
 (0)