mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
[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:
commit
f0e4a2124a
@ -54,6 +54,7 @@
|
|||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^22.4.10",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"i18next-http-backend": "^2.1.1",
|
"i18next-http-backend": "^2.1.1",
|
||||||
|
@ -60,3 +60,5 @@ export const IN_PROGRESS_IMAGE_TYPES: Array<{
|
|||||||
{ key: 'Fast', value: 'latents' },
|
{ key: 'Fast', value: 'latents' },
|
||||||
{ key: 'Accurate', value: 'full-res' },
|
{ key: 'Accurate', value: 'full-res' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const NODE_MIN_WIDTH = 250;
|
||||||
|
@ -42,8 +42,6 @@ const FieldHandle = (props: FieldHandleProps) => {
|
|||||||
const { nodeId, field, isValidConnection, handleType, styles } = props;
|
const { nodeId, field, isValidConnection, handleType, styles } = props;
|
||||||
const { name, title, type, description } = field;
|
const { name, title, type, description } = field;
|
||||||
|
|
||||||
console.log(props);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={type}
|
label={type}
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import 'reactflow/dist/style.css';
|
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 { map } from 'lodash';
|
||||||
import { FIELDS } from '../types/constants';
|
import { FIELDS } from '../types/constants';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const FieldTypeLegend = () => {
|
const FieldTypeLegend = () => {
|
||||||
return (
|
return (
|
||||||
<HStack>
|
<Flex gap={2} flexDirection={{ base: 'column', xl: 'row' }}>
|
||||||
{map(FIELDS, ({ title, description, color }, key) => (
|
{map(FIELDS, ({ title, description, color }, key) => (
|
||||||
<Tooltip key={key} label={description}>
|
<Tooltip key={key} label={description}>
|
||||||
<Badge colorScheme={color} sx={{ userSelect: 'none' }}>
|
<Badge
|
||||||
|
colorScheme={color}
|
||||||
|
sx={{ userSelect: 'none' }}
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</HStack>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
MiniMap,
|
|
||||||
OnConnect,
|
OnConnect,
|
||||||
OnEdgesChange,
|
OnEdgesChange,
|
||||||
OnNodesChange,
|
OnNodesChange,
|
||||||
@ -23,6 +22,8 @@ import TopLeftPanel from './panels/TopLeftPanel';
|
|||||||
import TopRightPanel from './panels/TopRightPanel';
|
import TopRightPanel from './panels/TopRightPanel';
|
||||||
import TopCenterPanel from './panels/TopCenterPanel';
|
import TopCenterPanel from './panels/TopCenterPanel';
|
||||||
import BottomLeftPanel from './panels/BottomLeftPanel.tsx';
|
import BottomLeftPanel from './panels/BottomLeftPanel.tsx';
|
||||||
|
import MinimapPanel from './panels/MinimapPanel';
|
||||||
|
import NodeSearch from './search/NodeSearch';
|
||||||
|
|
||||||
const nodeTypes = { invocation: InvocationComponent };
|
const nodeTypes = { invocation: InvocationComponent };
|
||||||
|
|
||||||
@ -59,12 +60,9 @@ export const Flow = () => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onConnectEnd: OnConnectEnd = useCallback(
|
const onConnectEnd: OnConnectEnd = useCallback(() => {
|
||||||
(event) => {
|
|
||||||
dispatch(connectionEnded());
|
dispatch(connectionEnded());
|
||||||
},
|
}, [dispatch]);
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
@ -80,12 +78,13 @@ export const Flow = () => {
|
|||||||
style: { strokeWidth: 2 },
|
style: { strokeWidth: 2 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopLeftPanel />
|
<NodeSearch />
|
||||||
|
{/* <TopLeftPanel /> */}
|
||||||
<TopCenterPanel />
|
<TopCenterPanel />
|
||||||
<TopRightPanel />
|
<TopRightPanel />
|
||||||
<BottomLeftPanel />
|
<BottomLeftPanel />
|
||||||
<Background />
|
<Background />
|
||||||
<MiniMap nodeStrokeWidth={3} zoomable pannable />
|
<MinimapPanel />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -119,7 +119,9 @@ const IAINodeInputs = (props: IAINodeInputsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (index < inputSockets.length) {
|
if (index < inputSockets.length) {
|
||||||
IAINodeInputsToRender.push(<Divider />);
|
IAINodeInputsToRender.push(
|
||||||
|
<Divider key={`${inputSocket.id}.divider`} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IAINodeInputsToRender.push(
|
IAINodeInputsToRender.push(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { NODE_MIN_WIDTH } from 'app/constants';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { NodeResizeControl, NodeResizerProps } from 'reactflow';
|
import { NodeResizeControl, NodeResizerProps } from 'reactflow';
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ const IAINodeResizer = (props: NodeResizerProps) => {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
}}
|
}}
|
||||||
minWidth={350}
|
minWidth={NODE_MIN_WIDTH}
|
||||||
{...rest}
|
{...rest}
|
||||||
></NodeResizeControl>
|
></NodeResizeControl>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,7 @@ import { RootState } from 'app/store';
|
|||||||
import { AnyInvocationType } from 'services/events/types';
|
import { AnyInvocationType } from 'services/events/types';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/storeHooks';
|
import { useAppSelector } from 'app/storeHooks';
|
||||||
|
import { NODE_MIN_WIDTH } from 'app/constants';
|
||||||
|
|
||||||
type InvocationComponentWrapperProps = PropsWithChildren & {
|
type InvocationComponentWrapperProps = PropsWithChildren & {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@ -28,6 +29,7 @@ const InvocationComponentWrapper = (props: InvocationComponentWrapperProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: 'md',
|
borderRadius: 'md',
|
||||||
|
minWidth: NODE_MIN_WIDTH,
|
||||||
boxShadow: props.selected
|
boxShadow: props.selected
|
||||||
? `${nodeSelectedOutline}, ${nodeShadow}`
|
? `${nodeSelectedOutline}, ${nodeShadow}`
|
||||||
: `${nodeShadow}`,
|
: `${nodeShadow}`,
|
||||||
|
@ -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);
|
@ -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);
|
@ -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"
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
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:
|
get-amd-module-type@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-3.0.2.tgz#46550cee2b8e1fa4c3f2c8a5753c36990aa49ab0"
|
resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-3.0.2.tgz#46550cee2b8e1fa4c3f2c8a5753c36990aa49ab0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user