fix(ui): improve nodes performance

This commit is contained in:
psychedelicious 2023-04-22 21:51:25 +10:00
parent 44a653925a
commit 4901911c1a
13 changed files with 195 additions and 117 deletions

View File

@ -49,7 +49,7 @@ export const AddNodeMenu = () => {
); );
return ( return (
<Menu> <Menu isLazy>
<MenuButton <MenuButton
as={IAIIconButton} as={IAIIconButton}
aria-label="Add Node" aria-label="Add Node"

View File

@ -1,5 +1,5 @@
import { Tooltip } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react';
import { CSSProperties, useMemo } from 'react'; import { CSSProperties, memo, useMemo } from 'react';
import { import {
Handle, Handle,
Position, Position,
@ -38,13 +38,14 @@ type FieldHandleProps = {
styles?: CSSProperties; styles?: CSSProperties;
}; };
export const FieldHandle = (props: FieldHandleProps) => { 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
key={name}
label={type} label={type}
placement={handleType === 'target' ? 'start' : 'end'} placement={handleType === 'target' ? 'start' : 'end'}
hasArrow hasArrow
@ -67,3 +68,5 @@ export const FieldHandle = (props: FieldHandleProps) => {
</Tooltip> </Tooltip>
); );
}; };
export default memo(FieldHandle);

View File

@ -30,6 +30,10 @@ import { IAIIconButton } from 'exports';
import { InfoIcon } from '@chakra-ui/icons'; import { InfoIcon } from '@chakra-ui/icons';
import { ViewportControls } from './ViewportControls'; import { ViewportControls } from './ViewportControls';
import NodeGraphOverlay from './NodeGraphOverlay'; import NodeGraphOverlay from './NodeGraphOverlay';
import TopLeftPanel from './panels/TopLeftPanel';
import TopRightPanel from './panels/TopRightPanel';
import TopCenterPanel from './panels/TopCenterPanel';
import BottomLeftPanel from './panels/BottomLeftPanel.tsx';
const nodeTypes = { invocation: InvocationComponent }; const nodeTypes = { invocation: InvocationComponent };
@ -37,9 +41,6 @@ export const Flow = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const nodes = useAppSelector((state: RootState) => state.nodes.nodes); const nodes = useAppSelector((state: RootState) => state.nodes.nodes);
const edges = useAppSelector((state: RootState) => state.nodes.edges); const edges = useAppSelector((state: RootState) => state.nodes.edges);
const shouldShowGraphOverlay = useAppSelector(
(state: RootState) => state.nodes.shouldShowGraphOverlay
);
const onNodesChange: OnNodesChange = useCallback( const onNodesChange: OnNodesChange = useCallback(
(changes) => { (changes) => {
@ -76,10 +77,6 @@ export const Flow = () => {
[dispatch] [dispatch]
); );
const handleInvoke = useCallback(() => {
dispatch(nodesGraphBuilt());
}, [dispatch]);
return ( return (
<ReactFlow <ReactFlow
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
@ -94,19 +91,10 @@ export const Flow = () => {
style: { strokeWidth: 2 }, style: { strokeWidth: 2 },
}} }}
> >
<Panel position="top-left"> <TopLeftPanel />
<AddNodeMenu /> <TopCenterPanel />
</Panel> <TopRightPanel />
<Panel position="top-center"> <BottomLeftPanel />
<Button onClick={handleInvoke}>Will it blend?</Button>
</Panel>
<Panel position="top-right">
<FieldTypeLegend />
{shouldShowGraphOverlay && <NodeGraphOverlay />}
</Panel>
<Panel position="bottom-left">
<ViewportControls />
</Panel>
<Background /> <Background />
<MiniMap nodeStrokeWidth={3} zoomable pannable /> <MiniMap nodeStrokeWidth={3} zoomable pannable />
</ReactFlow> </ReactFlow>

View File

@ -5,7 +5,7 @@ import { FaInfoCircle } from 'react-icons/fa';
interface IAINodeHeaderProps { interface IAINodeHeaderProps {
nodeId: string; nodeId: string;
template: MutableRefObject<InvocationTemplate | undefined>; template: InvocationTemplate;
} }
export default function IAINodeHeader(props: IAINodeHeaderProps) { export default function IAINodeHeader(props: IAINodeHeaderProps) {
@ -21,11 +21,11 @@ export default function IAINodeHeader(props: IAINodeHeaderProps) {
> >
<Tooltip label={nodeId}> <Tooltip label={nodeId}>
<Heading size="xs" fontWeight={600} color="base.100"> <Heading size="xs" fontWeight={600} color="base.100">
{template.current?.title} {template.title}
</Heading> </Heading>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
label={template.current?.description} label={template.description}
placement="top" placement="top"
hasArrow hasArrow
shouldWrapChildren shouldWrapChildren

View File

@ -3,7 +3,7 @@ import {
InputFieldValue, InputFieldValue,
InvocationTemplate, InvocationTemplate,
} from 'features/nodes/types/types'; } from 'features/nodes/types/types';
import { MutableRefObject, ReactNode } from 'react'; import { memo, MutableRefObject, ReactNode } from 'react';
import { map } from 'lodash'; import { map } from 'lodash';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
@ -17,7 +17,7 @@ import {
Icon, Icon,
Divider, Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FieldHandle } from '../FieldHandle'; import FieldHandle from '../FieldHandle';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { InputFieldComponent } from '../InputFieldComponent'; import { InputFieldComponent } from '../InputFieldComponent';
import { FaInfoCircle } from 'react-icons/fa'; import { FaInfoCircle } from 'react-icons/fa';
@ -37,7 +37,6 @@ function IAINodeInput(props: IAINodeInputProps) {
return ( return (
<Box <Box
key={input.id}
position="relative" position="relative"
borderColor={ borderColor={
!template !template
@ -96,11 +95,11 @@ function IAINodeInput(props: IAINodeInputProps) {
interface IAINodeInputsProps { interface IAINodeInputsProps {
nodeId: string; nodeId: string;
template: MutableRefObject<InvocationTemplate | undefined>; template: InvocationTemplate;
inputs: Record<string, InputFieldValue>; inputs: Record<string, InputFieldValue>;
} }
export default function IAINodeInputs(props: IAINodeInputsProps) { const IAINodeInputs = (props: IAINodeInputsProps) => {
const { nodeId, template, inputs } = props; const { nodeId, template, inputs } = props;
const connectedInputs = useAppSelector( const connectedInputs = useAppSelector(
@ -112,7 +111,7 @@ export default function IAINodeInputs(props: IAINodeInputsProps) {
const inputSockets = map(inputs); const inputSockets = map(inputs);
inputSockets.forEach((inputSocket, index) => { inputSockets.forEach((inputSocket, index) => {
const inputTemplate = template.current?.inputs[inputSocket.name]; const inputTemplate = template.inputs[inputSocket.name];
const isConnected = Boolean( const isConnected = Boolean(
connectedInputs.filter((connectedInput) => { connectedInputs.filter((connectedInput) => {
@ -129,6 +128,7 @@ export default function IAINodeInputs(props: IAINodeInputsProps) {
IAINodeInputsToRender.push( IAINodeInputsToRender.push(
<IAINodeInput <IAINodeInput
key={inputSocket.id}
nodeId={nodeId} nodeId={nodeId}
input={inputSocket} input={inputSocket}
template={inputTemplate} template={inputTemplate}
@ -145,4 +145,6 @@ export default function IAINodeInputs(props: IAINodeInputsProps) {
}; };
return renderIAINodeInputs(); return renderIAINodeInputs();
} };
export default memo(IAINodeInputs);

View File

@ -3,12 +3,12 @@ import {
OutputFieldTemplate, OutputFieldTemplate,
OutputFieldValue, OutputFieldValue,
} from 'features/nodes/types/types'; } from 'features/nodes/types/types';
import { MutableRefObject, ReactNode } from 'react'; import { memo, MutableRefObject, ReactNode } from 'react';
import { map } from 'lodash'; import { map } from 'lodash';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { Box, Flex, FormControl, FormLabel, HStack } from '@chakra-ui/react'; import { Box, Flex, FormControl, FormLabel, HStack } from '@chakra-ui/react';
import { FieldHandle } from '../FieldHandle'; import FieldHandle from '../FieldHandle';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
interface IAINodeOutputProps { interface IAINodeOutputProps {
@ -23,7 +23,7 @@ function IAINodeOutput(props: IAINodeOutputProps) {
const isValidConnection = useIsValidConnection(); const isValidConnection = useIsValidConnection();
return ( return (
<Box key={output.id} position="relative"> <Box position="relative">
<FormControl isDisabled={!template ? true : connected} paddingRight={3}> <FormControl isDisabled={!template ? true : connected} paddingRight={3}>
{!template ? ( {!template ? (
<HStack justifyContent="space-between" alignItems="center"> <HStack justifyContent="space-between" alignItems="center">
@ -52,11 +52,11 @@ function IAINodeOutput(props: IAINodeOutputProps) {
interface IAINodeOutputsProps { interface IAINodeOutputsProps {
nodeId: string; nodeId: string;
template: MutableRefObject<InvocationTemplate | undefined>; template: InvocationTemplate;
outputs: Record<string, OutputFieldValue>; outputs: Record<string, OutputFieldValue>;
} }
export default function IAINodeOutputs(props: IAINodeOutputsProps) { const IAINodeOutputs = (props: IAINodeOutputsProps) => {
const { nodeId, template, outputs } = props; const { nodeId, template, outputs } = props;
const connectedInputs = useAppSelector( const connectedInputs = useAppSelector(
@ -68,7 +68,7 @@ export default function IAINodeOutputs(props: IAINodeOutputsProps) {
const outputSockets = map(outputs); const outputSockets = map(outputs);
outputSockets.forEach((outputSocket) => { outputSockets.forEach((outputSocket) => {
const outputTemplate = template.current?.outputs[outputSocket.name]; const outputTemplate = template.outputs[outputSocket.name];
const isConnected = Boolean( const isConnected = Boolean(
connectedInputs.filter((connectedInput) => { connectedInputs.filter((connectedInput) => {
@ -81,6 +81,7 @@ export default function IAINodeOutputs(props: IAINodeOutputsProps) {
IAINodeOutputsToRender.push( IAINodeOutputsToRender.push(
<IAINodeOutput <IAINodeOutput
key={outputSocket.id}
nodeId={nodeId} nodeId={nodeId}
output={outputSocket} output={outputSocket}
template={outputTemplate} template={outputTemplate}
@ -93,4 +94,6 @@ export default function IAINodeOutputs(props: IAINodeOutputsProps) {
}; };
return renderIAINodeOutputs(); return renderIAINodeOutputs();
} };
export default memo(IAINodeOutputs);

View File

@ -1,15 +1,19 @@
import { NodeProps, NodeResizeControl } from 'reactflow'; import { NodeProps, NodeResizeControl } from 'reactflow';
import { Box, Flex, Icon, useToken } from '@chakra-ui/react'; import { Box, Flex, Icon, useToken } from '@chakra-ui/react';
import { FaExclamationCircle } from 'react-icons/fa'; import { FaExclamationCircle } from 'react-icons/fa';
import { InvocationValue } from '../types/types'; import { InvocationTemplate, InvocationValue } from '../types/types';
import { memo, PropsWithChildren, useRef } from 'react'; import { memo, PropsWithChildren, useMemo, useRef } from 'react';
import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate';
import IAINodeOutputs from './IAINode/IAINodeOutputs'; import IAINodeOutputs from './IAINode/IAINodeOutputs';
import IAINodeInputs from './IAINode/IAINodeInputs'; import IAINodeInputs from './IAINode/IAINodeInputs';
import IAINodeHeader from './IAINode/IAINodeHeader'; import IAINodeHeader from './IAINode/IAINodeHeader';
import { IoResize } from 'react-icons/io5'; import { IoResize } from 'react-icons/io5';
import IAINodeResizer from './IAINode/IAINodeResizer'; import IAINodeResizer from './IAINode/IAINodeResizer';
import { RootState } from 'app/store';
import { AnyInvocationType } from 'services/events/types';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
type InvocationComponentWrapperProps = PropsWithChildren & { type InvocationComponentWrapperProps = PropsWithChildren & {
selected: boolean; selected: boolean;
@ -36,16 +40,35 @@ const InvocationComponentWrapper = (props: InvocationComponentWrapperProps) => {
); );
}; };
const makeTemplateSelector = (type: AnyInvocationType) =>
createSelector(
[(state: RootState) => state.nodes],
(nodes) => {
const template = nodes.invocationTemplates[type];
if (!template) {
return;
}
return template;
},
{
memoizeOptions: {
resultEqualityCheck: (
a: InvocationTemplate | undefined,
b: InvocationTemplate | undefined
) => a !== undefined && b !== undefined && a.type === b.type,
},
}
);
export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => { export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
const { id: nodeId, data, selected } = props; const { id: nodeId, data, selected } = props;
const { type, inputs, outputs } = data; const { type, inputs, outputs } = data;
const getInvocationTemplate = useGetInvocationTemplate(); const templateSelector = useMemo(() => makeTemplateSelector(type), [type]);
// TODO: determine if a field/handle is connected and disable the input if so
const template = useRef(getInvocationTemplate(type)); const template = useAppSelector(templateSelector);
if (!template.current) { if (!template) {
return ( return (
<InvocationComponentWrapper selected={selected}> <InvocationComponentWrapper selected={selected}>
<Flex sx={{ alignItems: 'center', justifyContent: 'center' }}> <Flex sx={{ alignItems: 'center', justifyContent: 'center' }}>

View File

@ -0,0 +1,11 @@
import { memo } from 'react';
import { Panel } from 'reactflow';
import { ViewportControls } from '../ViewportControls';
const BottomLeftPanel = () => (
<Panel position="bottom-left">
<ViewportControls />
</Panel>
);
export default memo(BottomLeftPanel);

View File

@ -0,0 +1,23 @@
import { useAppDispatch } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { memo, useCallback } from 'react';
import { Panel } from 'reactflow';
import { nodesGraphBuilt } from 'services/thunks/session';
const TopCenterPanel = () => {
const dispatch = useAppDispatch();
const handleInvoke = useCallback(() => {
dispatch(nodesGraphBuilt());
}, [dispatch]);
return (
<Panel position="top-center">
<IAIButton colorScheme="accent" onClick={handleInvoke}>
Will it blend?
</IAIButton>
</Panel>
);
};
export default memo(TopCenterPanel);

View File

@ -0,0 +1,11 @@
import { memo } from 'react';
import { Panel } from 'reactflow';
import { AddNodeMenu } from '../AddNodeMenu';
const TopLeftPanel = () => (
<Panel position="top-left">
<AddNodeMenu />
</Panel>
);
export default memo(TopLeftPanel);

View File

@ -0,0 +1,21 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { memo } from 'react';
import { Panel } from 'reactflow';
import { FieldTypeLegend } from '../FieldTypeLegend';
import NodeGraphOverlay from '../NodeGraphOverlay';
const TopRightPanel = () => {
const shouldShowGraphOverlay = useAppSelector(
(state: RootState) => state.nodes.shouldShowGraphOverlay
);
return (
<Panel position="top-right">
<FieldTypeLegend />
{shouldShowGraphOverlay && <NodeGraphOverlay />}
</Panel>
);
};
export default memo(TopRightPanel);

View File

@ -1,6 +1,8 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { reduce } from 'lodash'; import { reduce } from 'lodash';
import { useCallback } from 'react';
import { Node, useReactFlow } from 'reactflow'; import { Node, useReactFlow } from 'reactflow';
import { AnyInvocationType } from 'services/events/types'; import { AnyInvocationType } from 'services/events/types';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -11,75 +13,82 @@ import {
} from '../types/types'; } from '../types/types';
import { buildInputFieldValue } from '../util/fieldValueBuilders'; import { buildInputFieldValue } from '../util/fieldValueBuilders';
const templatesSelector = createSelector(
[(state: RootState) => state.nodes],
(nodes) => nodes.invocationTemplates,
{ memoizeOptions: { resultEqualityCheck: (a, b) => true } }
);
export const useBuildInvocation = () => { export const useBuildInvocation = () => {
const invocationTemplates = useAppSelector( const invocationTemplates = useAppSelector(templatesSelector);
(state: RootState) => state.nodes.invocationTemplates
);
const reactflow = useReactFlow(); const flow = useReactFlow();
return (type: AnyInvocationType) => { return useCallback(
const template = invocationTemplates[type]; (type: AnyInvocationType) => {
const template = invocationTemplates[type];
if (template === undefined) { if (template === undefined) {
console.error(`Unable to find template ${type}.`); console.error(`Unable to find template ${type}.`);
return; return;
} }
const nodeId = uuidv4(); const nodeId = uuidv4();
const inputs = reduce( const inputs = reduce(
template.inputs, template.inputs,
(inputsAccumulator, inputTemplate, inputName) => { (inputsAccumulator, inputTemplate, inputName) => {
const fieldId = uuidv4(); const fieldId = uuidv4();
const inputFieldValue: InputFieldValue = buildInputFieldValue( const inputFieldValue: InputFieldValue = buildInputFieldValue(
fieldId, fieldId,
inputTemplate inputTemplate
); );
inputsAccumulator[inputName] = inputFieldValue; inputsAccumulator[inputName] = inputFieldValue;
return inputsAccumulator; return inputsAccumulator;
}, },
{} as Record<string, InputFieldValue> {} as Record<string, InputFieldValue>
); );
const outputs = reduce( const outputs = reduce(
template.outputs, template.outputs,
(outputsAccumulator, outputTemplate, outputName) => { (outputsAccumulator, outputTemplate, outputName) => {
const fieldId = uuidv4(); const fieldId = uuidv4();
const outputFieldValue: OutputFieldValue = { const outputFieldValue: OutputFieldValue = {
id: fieldId, id: fieldId,
name: outputName, name: outputName,
type: outputTemplate.type, type: outputTemplate.type,
}; };
outputsAccumulator[outputName] = outputFieldValue; outputsAccumulator[outputName] = outputFieldValue;
return outputsAccumulator; return outputsAccumulator;
}, },
{} as Record<string, OutputFieldValue> {} as Record<string, OutputFieldValue>
); );
const { x, y } = reactflow.project({ const { x, y } = flow.project({
x: window.innerWidth / 2.5, x: window.innerWidth / 2.5,
y: window.innerHeight / 8, y: window.innerHeight / 8,
}); });
const invocation: Node<InvocationValue> = { const invocation: Node<InvocationValue> = {
id: nodeId,
type: 'invocation',
position: { x: x, y: y },
data: {
id: nodeId, id: nodeId,
type, type: 'invocation',
inputs, position: { x: x, y: y },
outputs, data: {
}, id: nodeId,
}; type,
inputs,
outputs,
},
};
return invocation; return invocation;
}; },
[invocationTemplates, flow]
);
}; };

View File

@ -1,16 +0,0 @@
import { useAppSelector } from 'app/storeHooks';
import { invocationTemplatesSelector } from '../store/selectors/invocationTemplatesSelector';
export const useGetInvocationTemplate = () => {
const invocationTemplates = useAppSelector(invocationTemplatesSelector);
return (invocationType: string) => {
const template = invocationTemplates[invocationType];
if (!template) {
return;
}
return template;
};
};