mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): Basic Node Search Component
Very buggy
This commit is contained in:
parent
e29c2fb782
commit
ff891b1ff2
@ -23,6 +23,7 @@ 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 MinimapPanel from './panels/MinimapPanel';
|
||||||
|
import NodeSearch from './search/NodeSearch';
|
||||||
|
|
||||||
const nodeTypes = { invocation: InvocationComponent };
|
const nodeTypes = { invocation: InvocationComponent };
|
||||||
|
|
||||||
@ -77,7 +78,8 @@ export const Flow = () => {
|
|||||||
style: { strokeWidth: 2 },
|
style: { strokeWidth: 2 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopLeftPanel />
|
<NodeSearch />
|
||||||
|
{/* <TopLeftPanel /> */}
|
||||||
<TopCenterPanel />
|
<TopCenterPanel />
|
||||||
<TopRightPanel />
|
<TopRightPanel />
|
||||||
<BottomLeftPanel />
|
<BottomLeftPanel />
|
||||||
|
@ -0,0 +1,179 @@
|
|||||||
|
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 {
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 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 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[] = [];
|
||||||
|
|
||||||
|
nodes.forEach(({ title, description, type }, index) => {
|
||||||
|
if (title.toLowerCase().includes(searchText)) {
|
||||||
|
nodeListToRender.push(
|
||||||
|
<NodeListItem
|
||||||
|
key={index}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
type={type}
|
||||||
|
isSelected={focusedIndex === index}
|
||||||
|
addNode={addNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
<NodeListItem
|
||||||
|
key={index}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
type={type}
|
||||||
|
isSelected={focusedIndex === index}
|
||||||
|
addNode={addNode}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
background="base.900"
|
||||||
|
borderRadius={6}
|
||||||
|
maxHeight={400}
|
||||||
|
overflowY="scroll"
|
||||||
|
>
|
||||||
|
{nodeListToRender}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchKeyHandler = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
const { key } = e;
|
||||||
|
let nextIndex = 0;
|
||||||
|
|
||||||
|
if (key === 'ArrowDown') {
|
||||||
|
setShowNodeList(true);
|
||||||
|
nextIndex = (focusedIndex + 1) % nodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'ArrowUp') {
|
||||||
|
setShowNodeList(true);
|
||||||
|
nextIndex = (focusedIndex + nodes.length - 1) % nodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// # TODO Handle Blur
|
||||||
|
// if (key === 'Escape') {
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (key === 'Enter') {
|
||||||
|
const 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={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
setShowNodeList(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{showNodeList && renderNodeList()}
|
||||||
|
</Flex>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(NodeSearch);
|
Loading…
Reference in New Issue
Block a user