[Nodes UI] More Work (#3248)

- Style the Minimap
- Made the Node UI Legend Responsive
- Set Min Width for nodes on Spawn so resize doesn't snap.
- Initial Implementation of Node Search
- Added FuseJS to handle the node filtering
This commit is contained in:
blessedcoolant 2023-04-23 17:51:40 +12:00 committed by GitHub
commit f0e4a2124a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 276 additions and 17 deletions

View File

@ -54,6 +54,7 @@
"dateformat": "^5.0.3",
"formik": "^2.2.9",
"framer-motion": "^9.0.4",
"fuse.js": "^6.6.2",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.1.1",

View File

@ -60,3 +60,5 @@ export const IN_PROGRESS_IMAGE_TYPES: Array<{
{ key: 'Fast', value: 'latents' },
{ key: 'Accurate', value: 'full-res' },
];
export const NODE_MIN_WIDTH = 250;

View File

@ -42,8 +42,6 @@ const FieldHandle = (props: FieldHandleProps) => {
const { nodeId, field, isValidConnection, handleType, styles } = props;
const { name, title, type, description } = field;
console.log(props);
return (
<Tooltip
label={type}

View File

@ -1,20 +1,24 @@
import 'reactflow/dist/style.css';
import { Tooltip, Badge, HStack } from '@chakra-ui/react';
import { Tooltip, Badge, Flex } from '@chakra-ui/react';
import { map } from 'lodash';
import { FIELDS } from '../types/constants';
import { memo } from 'react';
const FieldTypeLegend = () => {
return (
<HStack>
<Flex gap={2} flexDirection={{ base: 'column', xl: 'row' }}>
{map(FIELDS, ({ title, description, color }, key) => (
<Tooltip key={key} label={description}>
<Badge colorScheme={color} sx={{ userSelect: 'none' }}>
<Badge
colorScheme={color}
sx={{ userSelect: 'none' }}
textAlign="center"
>
{title}
</Badge>
</Tooltip>
))}
</HStack>
</Flex>
);
};

View File

@ -1,6 +1,5 @@
import {
Background,
MiniMap,
OnConnect,
OnEdgesChange,
OnNodesChange,
@ -23,6 +22,8 @@ import TopLeftPanel from './panels/TopLeftPanel';
import TopRightPanel from './panels/TopRightPanel';
import TopCenterPanel from './panels/TopCenterPanel';
import BottomLeftPanel from './panels/BottomLeftPanel.tsx';
import MinimapPanel from './panels/MinimapPanel';
import NodeSearch from './search/NodeSearch';
const nodeTypes = { invocation: InvocationComponent };
@ -59,12 +60,9 @@ export const Flow = () => {
[dispatch]
);
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
dispatch(connectionEnded());
},
[dispatch]
);
const onConnectEnd: OnConnectEnd = useCallback(() => {
dispatch(connectionEnded());
}, [dispatch]);
return (
<ReactFlow
@ -80,12 +78,13 @@ export const Flow = () => {
style: { strokeWidth: 2 },
}}
>
<TopLeftPanel />
<NodeSearch />
{/* <TopLeftPanel /> */}
<TopCenterPanel />
<TopRightPanel />
<BottomLeftPanel />
<Background />
<MiniMap nodeStrokeWidth={3} zoomable pannable />
<MinimapPanel />
</ReactFlow>
);
};

View File

@ -119,7 +119,9 @@ const IAINodeInputs = (props: IAINodeInputsProps) => {
);
if (index < inputSockets.length) {
IAINodeInputsToRender.push(<Divider />);
IAINodeInputsToRender.push(
<Divider key={`${inputSocket.id}.divider`} />
);
}
IAINodeInputsToRender.push(

View File

@ -1,3 +1,4 @@
import { NODE_MIN_WIDTH } from 'app/constants';
import { memo } from 'react';
import { NodeResizeControl, NodeResizerProps } from 'reactflow';
@ -14,7 +15,7 @@ const IAINodeResizer = (props: NodeResizerProps) => {
bottom: 0,
right: 0,
}}
minWidth={350}
minWidth={NODE_MIN_WIDTH}
{...rest}
></NodeResizeControl>
);

View File

@ -12,6 +12,7 @@ import { RootState } from 'app/store';
import { AnyInvocationType } from 'services/events/types';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import { NODE_MIN_WIDTH } from 'app/constants';
type InvocationComponentWrapperProps = PropsWithChildren & {
selected: boolean;
@ -28,6 +29,7 @@ const InvocationComponentWrapper = (props: InvocationComponentWrapperProps) => {
sx={{
position: 'relative',
borderRadius: 'md',
minWidth: NODE_MIN_WIDTH,
boxShadow: props.selected
? `${nodeSelectedOutline}, ${nodeShadow}`
: `${nodeShadow}`,

View File

@ -0,0 +1,34 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { CSSProperties, memo } from 'react';
import { MiniMap } from 'reactflow';
const MinimapStyle: CSSProperties = {
background: 'var(--invokeai-colors-base-500)',
};
const MinimapPanel = () => {
const currentTheme = useAppSelector(
(state: RootState) => state.ui.currentTheme
);
return (
<MiniMap
nodeStrokeWidth={3}
pannable
zoomable
nodeBorderRadius={30}
style={MinimapStyle}
nodeColor={
currentTheme === 'light'
? 'var(--invokeai-colors-accent-700)'
: currentTheme === 'green'
? 'var(--invokeai-colors-accent-600)'
: 'var(--invokeai-colors-accent-700)'
}
maskColor="var(--invokeai-colors-base-700)"
/>
);
};
export default memo(MinimapPanel);

View File

@ -0,0 +1,211 @@
import { Box, Flex } from '@chakra-ui/layout';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIInput from 'common/components/IAIInput';
import { Panel } from 'reactflow';
import { map } from 'lodash';
import {
ChangeEvent,
FocusEvent,
KeyboardEvent,
memo,
ReactNode,
useCallback,
useRef,
useState,
} from 'react';
import { Tooltip } from '@chakra-ui/tooltip';
import { AnyInvocationType } from 'services/events/types';
import { useBuildInvocation } from 'features/nodes/hooks/useBuildInvocation';
import { makeToast } from 'features/system/hooks/useToastWatcher';
import { addToast } from 'features/system/store/systemSlice';
import { nodeAdded } from '../../store/nodesSlice';
import Fuse from 'fuse.js';
import { InvocationTemplate } from 'features/nodes/types/types';
interface NodeListItemProps {
title: string;
description: string;
type: AnyInvocationType;
isSelected: boolean;
addNode: (nodeType: AnyInvocationType) => void;
}
const NodeListItem = (props: NodeListItemProps) => {
const { title, description, type, isSelected, addNode } = props;
return (
<Tooltip label={description} placement="end" hasArrow>
<Box
px={4}
onClick={() => addNode(type)}
background={isSelected ? 'base.600' : 'none'}
_hover={{
background: 'base.600',
cursor: 'pointer',
}}
>
{title}
</Box>
</Tooltip>
);
};
NodeListItem.displayName = 'NodeListItem';
const NodeSearch = () => {
const invocationTemplates = useAppSelector(
(state: RootState) => state.nodes.invocationTemplates
);
const nodes = map(invocationTemplates);
const [filteredNodes, setFilteredNodes] = useState<
Fuse.FuseResult<InvocationTemplate>[]
>([]);
const buildInvocation = useBuildInvocation();
const dispatch = useAppDispatch();
const [searchText, setSearchText] = useState<string>('');
const [showNodeList, setShowNodeList] = useState<boolean>(false);
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
const nodeSearchRef = useRef<HTMLDivElement>(null);
const fuseOptions = {
findAllMatches: true,
threshold: 0,
ignoreLocation: true,
keys: ['title', 'type', 'tags'],
};
const fuse = new Fuse(nodes, fuseOptions);
const findNode = (e: ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
setFilteredNodes(fuse.search(e.target.value));
setShowNodeList(true);
};
const addNode = useCallback(
(nodeType: AnyInvocationType) => {
const invocation = buildInvocation(nodeType);
if (!invocation) {
const toast = makeToast({
status: 'error',
title: `Unknown Invocation type ${nodeType}`,
});
dispatch(addToast(toast));
return;
}
dispatch(nodeAdded(invocation));
},
[dispatch, buildInvocation]
);
const renderNodeList = () => {
const nodeListToRender: ReactNode[] = [];
if (searchText.length > 0) {
filteredNodes.forEach(({ item }, index) => {
const { title, description, type } = item;
if (title.toLowerCase().includes(searchText)) {
nodeListToRender.push(
<NodeListItem
key={index}
title={title}
description={description}
type={type}
isSelected={focusedIndex === index}
addNode={addNode}
/>
);
}
});
} else {
nodes.forEach(({ title, description, type }, index) => {
nodeListToRender.push(
<NodeListItem
key={index}
title={title}
description={description}
type={type}
isSelected={focusedIndex === index}
addNode={addNode}
/>
);
});
}
return (
<Flex flexDirection="column" background="base.900" borderRadius={6}>
{nodeListToRender}
</Flex>
);
};
const searchKeyHandler = (e: KeyboardEvent<HTMLDivElement>) => {
const { key } = e;
let nextIndex = 0;
if (key === 'ArrowDown') {
setShowNodeList(true);
if (searchText.length > 0) {
nextIndex = (focusedIndex + 1) % filteredNodes.length;
} else {
nextIndex = (focusedIndex + 1) % nodes.length;
}
}
if (key === 'ArrowUp') {
setShowNodeList(true);
if (searchText.length > 0) {
nextIndex =
(focusedIndex + filteredNodes.length - 1) % filteredNodes.length;
} else {
nextIndex = (focusedIndex + nodes.length - 1) % nodes.length;
}
}
// # TODO Handle Blur
// if (key === 'Escape') {
// }
if (key === 'Enter') {
let selectedNodeType: AnyInvocationType;
if (searchText.length > 0) {
selectedNodeType = filteredNodes[focusedIndex].item.type;
} else {
selectedNodeType = nodes[focusedIndex].type;
}
addNode(selectedNodeType);
setShowNodeList(false);
}
setFocusedIndex(nextIndex);
};
const searchInputBlurHandler = (e: FocusEvent<HTMLDivElement>) => {
if (!e.currentTarget.contains(e.relatedTarget)) setShowNodeList(false);
};
return (
<Panel position="top-left">
<Flex
flexDirection="column"
tabIndex={1}
onKeyDown={searchKeyHandler}
onFocus={() => setShowNodeList(true)}
onBlur={searchInputBlurHandler}
ref={nodeSearchRef}
>
<IAIInput value={searchText} onChange={findNode} />
{showNodeList && renderNodeList()}
</Flex>
</Panel>
);
};
export default memo(NodeSearch);

View File

@ -3454,6 +3454,11 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
get-amd-module-type@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-3.0.2.tgz#46550cee2b8e1fa4c3f2c8a5753c36990aa49ab0"