mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): improve node rendering performance
Previously the editor was using prop-drilling node data and templates to get values deep into nodes. This ended up causing very noticeable performance degradation. For example, any text entry fields were super laggy. Refactor the whole thing to use memoized selectors via hooks. The hooks are mostly very narrow, returning only the data needed. Data objects are never passed down, only node id and field name - sometimes the field kind ('input' or 'output'). The end result is a *much* smoother node editor with very minimal rerenders.
This commit is contained in:
parent
f7c92e1eff
commit
f9b8b5cff2
@ -1,40 +1,34 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { map, some } from 'lodash-es';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { useFieldNames, useWithFooter } from 'features/nodes/hooks/useNodeData';
|
||||
import { memo } from 'react';
|
||||
import InputField from '../fields/InputField';
|
||||
import OutputField from '../fields/OutputField';
|
||||
import NodeFooter, { FOOTER_FIELDS } from './NodeFooter';
|
||||
import NodeFooter from './NodeFooter';
|
||||
import NodeHeader from './NodeHeader';
|
||||
import NodeWrapper from './NodeWrapper';
|
||||
|
||||
type Props = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
isOpen: boolean;
|
||||
label: string;
|
||||
type: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => {
|
||||
const { id: nodeId, data } = nodeProps;
|
||||
const { inputs, outputs, isOpen } = data;
|
||||
|
||||
const inputFields = useMemo(
|
||||
() => map(inputs).filter((i) => i.name !== 'is_intermediate'),
|
||||
[inputs]
|
||||
);
|
||||
const outputFields = useMemo(() => map(outputs), [outputs]);
|
||||
|
||||
const withFooter = useMemo(
|
||||
() => some(outputs, (output) => FOOTER_FIELDS.includes(output.type)),
|
||||
[outputs]
|
||||
);
|
||||
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
const inputFieldNames = useFieldNames(nodeId, 'input');
|
||||
const outputFieldNames = useFieldNames(nodeId, 'output');
|
||||
const withFooter = useWithFooter(nodeId);
|
||||
|
||||
return (
|
||||
<NodeWrapper nodeProps={nodeProps}>
|
||||
<NodeHeader nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
|
||||
<NodeWrapper nodeId={nodeId} selected={selected}>
|
||||
<NodeHeader
|
||||
nodeId={nodeId}
|
||||
isOpen={isOpen}
|
||||
label={label}
|
||||
selected={selected}
|
||||
type={type}
|
||||
/>
|
||||
{isOpen && (
|
||||
<>
|
||||
<Flex
|
||||
@ -54,27 +48,23 @@ const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => {
|
||||
className="nopan"
|
||||
sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}
|
||||
>
|
||||
{outputFields.map((field) => (
|
||||
{outputFieldNames.map((fieldName) => (
|
||||
<OutputField
|
||||
key={`${nodeId}.${field.id}.input-field`}
|
||||
nodeProps={nodeProps}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
key={`${nodeId}.${fieldName}.output-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
))}
|
||||
{inputFields.map((field) => (
|
||||
{inputFieldNames.map((fieldName) => (
|
||||
<InputField
|
||||
key={`${nodeId}.${field.id}.input-field`}
|
||||
nodeProps={nodeProps}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
key={`${nodeId}.${fieldName}.input-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{withFooter && (
|
||||
<NodeFooter nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
|
||||
)}
|
||||
{withFooter && <NodeFooter nodeId={nodeId} />}
|
||||
</>
|
||||
)}
|
||||
</NodeWrapper>
|
||||
|
@ -2,16 +2,15 @@ import { ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { NodeData } from 'features/nodes/types/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { NodeProps, useUpdateNodeInternals } from 'reactflow';
|
||||
import { useUpdateNodeInternals } from 'reactflow';
|
||||
|
||||
interface Props {
|
||||
nodeProps: NodeProps<NodeData>;
|
||||
nodeId: string;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const NodeCollapseButton = (props: Props) => {
|
||||
const { id: nodeId, isOpen } = props.nodeProps.data;
|
||||
const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
|
@ -1,20 +1,17 @@
|
||||
import { useColorModeValue } from '@chakra-ui/react';
|
||||
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||
import { isInvocationNodeData } from 'features/nodes/types/types';
|
||||
import { map } from 'lodash-es';
|
||||
import { CSSProperties, memo, useMemo } from 'react';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
interface Props {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
const NodeCollapsedHandles = (props: Props) => {
|
||||
const { data } = props.nodeProps;
|
||||
const NodeCollapsedHandles = ({ nodeId }: Props) => {
|
||||
const data = useNodeData(nodeId);
|
||||
const { base400, base600 } = useChakraThemeTokens();
|
||||
const backgroundColor = useColorModeValue(base400, base600);
|
||||
|
||||
@ -30,6 +27,10 @@ const NodeCollapsedHandles = (props: Props) => {
|
||||
[backgroundColor]
|
||||
);
|
||||
|
||||
if (!isInvocationNodeData(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
@ -44,7 +45,7 @@ const NodeCollapsedHandles = (props: Props) => {
|
||||
key={`${data.id}-${input.name}-collapsed-input-handle`}
|
||||
type="target"
|
||||
id={input.name}
|
||||
isValidConnection={() => false}
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
style={{ visibility: 'hidden' }}
|
||||
/>
|
||||
@ -52,7 +53,6 @@ const NodeCollapsedHandles = (props: Props) => {
|
||||
<Handle
|
||||
type="source"
|
||||
id={`${data.id}-collapsed-source`}
|
||||
isValidConnection={() => false}
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
style={{ ...dummyHandleStyles, right: '-0.5rem' }}
|
||||
@ -62,7 +62,7 @@ const NodeCollapsedHandles = (props: Props) => {
|
||||
key={`${data.id}-${output.name}-collapsed-output-handle`}
|
||||
type="source"
|
||||
id={output.name}
|
||||
isValidConnection={() => false}
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
style={{ visibility: 'hidden' }}
|
||||
/>
|
||||
|
@ -6,49 +6,22 @@ import {
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
useHasImageOutput,
|
||||
useIsIntermediate,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { some } from 'lodash-es';
|
||||
import { ChangeEvent, memo, useCallback, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
|
||||
export const IMAGE_FIELDS = ['ImageField', 'ImageCollection'];
|
||||
export const FOOTER_FIELDS = IMAGE_FIELDS;
|
||||
|
||||
type Props = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
const NodeFooter = (props: Props) => {
|
||||
const { nodeProps, nodeTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const hasImageOutput = useMemo(
|
||||
() =>
|
||||
some(nodeTemplate?.outputs, (output) =>
|
||||
IMAGE_FIELDS.includes(output.type)
|
||||
),
|
||||
[nodeTemplate?.outputs]
|
||||
);
|
||||
|
||||
const handleChangeIsIntermediate = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
fieldBooleanValueChanged({
|
||||
nodeId: nodeProps.data.id,
|
||||
fieldName: 'is_intermediate',
|
||||
value: !e.target.checked,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, nodeProps.data.id]
|
||||
);
|
||||
|
||||
const NodeFooter = ({ nodeId }: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
@ -62,19 +35,45 @@ const NodeFooter = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<Spacer />
|
||||
{hasImageOutput && (
|
||||
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
||||
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save Output</FormLabel>
|
||||
<Checkbox
|
||||
className="nopan"
|
||||
size="sm"
|
||||
onChange={handleChangeIsIntermediate}
|
||||
isChecked={!nodeProps.data.inputs['is_intermediate']?.value}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
<SaveImageCheckbox nodeId={nodeId} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeFooter);
|
||||
|
||||
const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const hasImageOutput = useHasImageOutput(nodeId);
|
||||
const is_intermediate = useIsIntermediate(nodeId);
|
||||
const handleChangeIsIntermediate = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
fieldBooleanValueChanged({
|
||||
nodeId,
|
||||
fieldName: 'is_intermediate',
|
||||
value: !e.target.checked,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
|
||||
if (!hasImageOutput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
||||
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save Output</FormLabel>
|
||||
<Checkbox
|
||||
className="nopan"
|
||||
size="sm"
|
||||
onChange={handleChangeIsIntermediate}
|
||||
isChecked={!is_intermediate}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
SaveImageCheckbox.displayName = 'SaveImageCheckbox';
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCollapseButton from '../Invocation/NodeCollapseButton';
|
||||
import NodeCollapsedHandles from '../Invocation/NodeCollapsedHandles';
|
||||
import NodeNotesEdit from '../Invocation/NodeNotesEdit';
|
||||
@ -12,14 +7,14 @@ import NodeStatusIndicator from '../Invocation/NodeStatusIndicator';
|
||||
import NodeTitle from '../Invocation/NodeTitle';
|
||||
|
||||
type Props = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
isOpen: boolean;
|
||||
label: string;
|
||||
type: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
const NodeHeader = (props: Props) => {
|
||||
const { nodeProps, nodeTemplate } = props;
|
||||
const { isOpen } = nodeProps.data;
|
||||
|
||||
const NodeHeader = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="nodeHeader"
|
||||
@ -35,18 +30,13 @@ const NodeHeader = (props: Props) => {
|
||||
_dark: { color: 'base.200' },
|
||||
}}
|
||||
>
|
||||
<NodeCollapseButton nodeProps={nodeProps} />
|
||||
<NodeTitle nodeData={nodeProps.data} title={nodeTemplate.title} />
|
||||
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
|
||||
<NodeTitle nodeId={nodeId} />
|
||||
<Flex alignItems="center">
|
||||
<NodeStatusIndicator nodeProps={nodeProps} />
|
||||
<NodeNotesEdit nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
|
||||
<NodeStatusIndicator nodeId={nodeId} />
|
||||
<NodeNotesEdit nodeId={nodeId} />
|
||||
</Flex>
|
||||
{!isOpen && (
|
||||
<NodeCollapsedHandles
|
||||
nodeProps={nodeProps}
|
||||
nodeTemplate={nodeTemplate}
|
||||
/>
|
||||
)}
|
||||
{!isOpen && <NodeCollapsedHandles nodeId={nodeId} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -16,41 +16,31 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAITextarea from 'common/components/IAITextarea';
|
||||
import {
|
||||
useNodeData,
|
||||
useNodeLabel,
|
||||
useNodeTemplate,
|
||||
useNodeTemplateTitle,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { isInvocationNodeData } from 'features/nodes/types/types';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { FaInfoCircle } from 'react-icons/fa';
|
||||
import { NodeProps } from 'reactflow';
|
||||
|
||||
interface Props {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
const NodeNotesEdit = (props: Props) => {
|
||||
const { nodeProps, nodeTemplate } = props;
|
||||
const { data } = nodeProps;
|
||||
const NodeNotesEdit = ({ nodeId }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleNotesChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(nodeNotesChanged({ nodeId: data.id, notes: e.target.value }));
|
||||
},
|
||||
[data.id, dispatch]
|
||||
);
|
||||
const label = useNodeLabel(nodeId);
|
||||
const title = useNodeTemplateTitle(nodeId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
label={
|
||||
nodeTemplate ? (
|
||||
<TooltipContent nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
|
||||
) : undefined
|
||||
}
|
||||
label={<TooltipContent nodeId={nodeId} />}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
>
|
||||
@ -75,19 +65,10 @@ const NodeNotesEdit = (props: Props) => {
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{data.label || nodeTemplate?.title || 'Unknown Node'}
|
||||
</ModalHeader>
|
||||
<ModalHeader>{label || title || 'Unknown Node'}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FormControl>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<IAITextarea
|
||||
value={data.notes}
|
||||
onChange={handleNotesChanged}
|
||||
rows={10}
|
||||
/>
|
||||
</FormControl>
|
||||
<NotesTextarea nodeId={nodeId} />
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
@ -98,16 +79,49 @@ const NodeNotesEdit = (props: Props) => {
|
||||
|
||||
export default memo(NodeNotesEdit);
|
||||
|
||||
type TooltipContentProps = Props;
|
||||
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const data = useNodeData(nodeId);
|
||||
const nodeTemplate = useNodeTemplate(nodeId);
|
||||
|
||||
if (!isInvocationNodeData(data)) {
|
||||
return 'Unknown Node';
|
||||
}
|
||||
|
||||
const TooltipContent = (props: TooltipContentProps) => {
|
||||
return (
|
||||
<Flex sx={{ flexDir: 'column' }}>
|
||||
<Text sx={{ fontWeight: 600 }}>{props.nodeTemplate?.title}</Text>
|
||||
<Text sx={{ fontWeight: 600 }}>{nodeTemplate?.title}</Text>
|
||||
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
|
||||
{props.nodeTemplate?.description}
|
||||
{nodeTemplate?.description}
|
||||
</Text>
|
||||
{props.nodeProps.data.notes && <Text>{props.nodeProps.data.notes}</Text>}
|
||||
{data?.notes && <Text>{data.notes}</Text>}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
TooltipContent.displayName = 'TooltipContent';
|
||||
|
||||
const NotesTextarea = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const data = useNodeData(nodeId);
|
||||
const handleNotesChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
|
||||
},
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
if (!isInvocationNodeData(data)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel>Notes</FormLabel>
|
||||
<IAITextarea
|
||||
value={data?.notes}
|
||||
onChange={handleNotesChanged}
|
||||
rows={10}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
NotesTextarea.displayName = 'NodesTextarea';
|
||||
|
@ -11,17 +11,12 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
NodeExecutionState,
|
||||
NodeStatus,
|
||||
} from 'features/nodes/types/types';
|
||||
import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa';
|
||||
import { NodeProps } from 'reactflow';
|
||||
|
||||
type Props = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
const iconBoxSize = 3;
|
||||
@ -33,8 +28,7 @@ const circleStyles = {
|
||||
'.chakra-progress__track': { stroke: 'transparent' },
|
||||
};
|
||||
|
||||
const NodeStatusIndicator = (props: Props) => {
|
||||
const nodeId = props.nodeProps.data.id;
|
||||
const NodeStatusIndicator = ({ nodeId }: Props) => {
|
||||
const selectNodeExecutionState = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
@ -76,7 +70,7 @@ type TooltipLabelProps = {
|
||||
nodeExecutionState: NodeExecutionState;
|
||||
};
|
||||
|
||||
const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => {
|
||||
const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
|
||||
const { status, progress, progressImage } = nodeExecutionState;
|
||||
if (status === NodeStatus.PENDING) {
|
||||
return <Text>Pending</Text>;
|
||||
@ -118,13 +112,15 @@ const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => {
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
TooltipLabel.displayName = 'TooltipLabel';
|
||||
|
||||
type StatusIconProps = {
|
||||
nodeExecutionState: NodeExecutionState;
|
||||
};
|
||||
|
||||
const StatusIcon = (props: StatusIconProps) => {
|
||||
const StatusIcon = memo((props: StatusIconProps) => {
|
||||
const { progress, status } = props.nodeExecutionState;
|
||||
if (status === NodeStatus.PENDING) {
|
||||
return (
|
||||
@ -182,4 +178,6 @@ const StatusIcon = (props: StatusIconProps) => {
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
StatusIcon.displayName = 'StatusIcon';
|
||||
|
@ -7,26 +7,29 @@ import {
|
||||
useEditableControls,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
useNodeLabel,
|
||||
useNodeTemplateTitle,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import { NodeData } from 'features/nodes/types/types';
|
||||
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
nodeData: NodeData;
|
||||
title: string;
|
||||
nodeId: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const NodeTitle = (props: Props) => {
|
||||
const { title } = props;
|
||||
const { id: nodeId, label } = props.nodeData;
|
||||
const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [localTitle, setLocalTitle] = useState(label || title);
|
||||
const label = useNodeLabel(nodeId);
|
||||
const templateTitle = useNodeTemplateTitle(nodeId);
|
||||
|
||||
const [localTitle, setLocalTitle] = useState('');
|
||||
const handleSubmit = useCallback(
|
||||
async (newTitle: string) => {
|
||||
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
|
||||
setLocalTitle(newTitle || title);
|
||||
setLocalTitle(newTitle || title || 'Problem Setting Title');
|
||||
},
|
||||
[nodeId, dispatch, title]
|
||||
);
|
||||
@ -37,8 +40,8 @@ const NodeTitle = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Another component may change the title; sync local title with global state
|
||||
setLocalTitle(label || title);
|
||||
}, [label, title]);
|
||||
setLocalTitle(label || title || templateTitle || 'Problem Setting Title');
|
||||
}, [label, templateTitle, title]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
@ -6,10 +6,14 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { nodeClicked } from 'features/nodes/store/nodesSlice';
|
||||
import { MouseEvent, PropsWithChildren, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
MouseEvent,
|
||||
PropsWithChildren,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../../types/constants';
|
||||
import { NodeData } from 'features/nodes/types/types';
|
||||
import { NodeProps } from 'reactflow';
|
||||
|
||||
const useNodeSelect = (nodeId: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -25,14 +29,13 @@ const useNodeSelect = (nodeId: string) => {
|
||||
};
|
||||
|
||||
type NodeWrapperProps = PropsWithChildren & {
|
||||
nodeProps: NodeProps<NodeData>;
|
||||
nodeId: string;
|
||||
selected: boolean;
|
||||
width?: NonNullable<ChakraProps['sx']>['w'];
|
||||
};
|
||||
|
||||
const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
const { width, children, nodeProps } = props;
|
||||
const { data, selected } = nodeProps;
|
||||
const nodeId = data.id;
|
||||
const { width, children, nodeId, selected } = props;
|
||||
|
||||
const [
|
||||
nodeSelectedOutlineLight,
|
||||
@ -93,4 +96,4 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeWrapper;
|
||||
export default memo(NodeWrapper);
|
||||
|
@ -1,20 +1,26 @@
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import { InvocationNodeData } from 'features/nodes/types/types';
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCollapseButton from '../Invocation/NodeCollapseButton';
|
||||
import NodeWrapper from '../Invocation/NodeWrapper';
|
||||
|
||||
type Props = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeId: string;
|
||||
isOpen: boolean;
|
||||
label: string;
|
||||
type: string;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
const UnknownNodeFallback = ({ nodeProps }: Props) => {
|
||||
const { data } = nodeProps;
|
||||
const { isOpen, label, type } = data;
|
||||
const UnknownNodeFallback = ({
|
||||
nodeId,
|
||||
isOpen,
|
||||
label,
|
||||
type,
|
||||
selected,
|
||||
}: Props) => {
|
||||
return (
|
||||
<NodeWrapper nodeProps={nodeProps}>
|
||||
<NodeWrapper nodeId={nodeId} selected={selected}>
|
||||
<Flex
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
layerStyle="nodeHeader"
|
||||
@ -27,7 +33,7 @@ const UnknownNodeFallback = ({ nodeProps }: Props) => {
|
||||
fontSize: 'sm',
|
||||
}}
|
||||
>
|
||||
<NodeCollapseButton nodeProps={nodeProps} />
|
||||
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
|
||||
<Text
|
||||
sx={{
|
||||
w: 'full',
|
||||
|
@ -1,19 +1,12 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { CSSProperties, memo, useMemo } from 'react';
|
||||
import { Handle, HandleType, NodeProps, Position } from 'reactflow';
|
||||
import { Handle, HandleType, Position } from 'reactflow';
|
||||
import {
|
||||
FIELDS,
|
||||
HANDLE_TOOLTIP_OPEN_DELAY,
|
||||
colorTokenToCssVar,
|
||||
} from '../../types/constants';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
OutputFieldTemplate,
|
||||
OutputFieldValue,
|
||||
} from '../../types/types';
|
||||
import { InputFieldTemplate, OutputFieldTemplate } from '../../types/types';
|
||||
|
||||
export const handleBaseStyles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
@ -32,9 +25,6 @@ export const outputHandleStyles: CSSProperties = {
|
||||
};
|
||||
|
||||
type FieldHandleProps = {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue | OutputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate | OutputFieldTemplate;
|
||||
handleType: HandleType;
|
||||
isConnectionInProgress: boolean;
|
||||
|
@ -8,13 +8,11 @@ import {
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIDraggable from 'common/components/IAIDraggable';
|
||||
import { NodeFieldDraggableData } from 'features/dnd/types';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
useFieldData,
|
||||
useFieldTemplate,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
MouseEvent,
|
||||
memo,
|
||||
@ -25,41 +23,43 @@ import {
|
||||
} from 'react';
|
||||
|
||||
interface Props {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
isDraggable?: boolean;
|
||||
kind: 'input' | 'output';
|
||||
}
|
||||
|
||||
const FieldTitle = (props: Props) => {
|
||||
const { nodeData, field, fieldTemplate, isDraggable = false } = props;
|
||||
const { label } = field;
|
||||
const { title, input } = fieldTemplate;
|
||||
const { id: nodeId } = nodeData;
|
||||
const { nodeId, fieldName, isDraggable = false, kind } = props;
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||
const field = useFieldData(nodeId, fieldName);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [localTitle, setLocalTitle] = useState(label || title);
|
||||
const [localTitle, setLocalTitle] = useState(
|
||||
field?.label || fieldTemplate?.title || 'Unknown Field'
|
||||
);
|
||||
|
||||
const draggableData: NodeFieldDraggableData | undefined = useMemo(
|
||||
() =>
|
||||
input !== 'connection' && isDraggable
|
||||
field &&
|
||||
fieldTemplate?.fieldKind === 'input' &&
|
||||
fieldTemplate?.input !== 'connection' &&
|
||||
isDraggable
|
||||
? {
|
||||
id: `${nodeId}-${field.name}`,
|
||||
id: `${nodeId}-${fieldName}`,
|
||||
payloadType: 'NODE_FIELD',
|
||||
payload: { nodeId, field, fieldTemplate },
|
||||
}
|
||||
: undefined,
|
||||
[field, fieldTemplate, input, isDraggable, nodeId]
|
||||
[field, fieldName, fieldTemplate, isDraggable, nodeId]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (newTitle: string) => {
|
||||
dispatch(
|
||||
fieldLabelChanged({ nodeId, fieldName: field.name, label: newTitle })
|
||||
);
|
||||
setLocalTitle(newTitle || title);
|
||||
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
|
||||
setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field');
|
||||
},
|
||||
[dispatch, nodeId, field.name, title]
|
||||
[dispatch, nodeId, fieldName, fieldTemplate?.title]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((newTitle: string) => {
|
||||
@ -68,8 +68,8 @@ const FieldTitle = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Another component may change the title; sync local title with global state
|
||||
setLocalTitle(label || title);
|
||||
}, [label, title]);
|
||||
setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field');
|
||||
}, [field?.label, fieldTemplate?.title]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -120,7 +120,7 @@ type EditableControlsProps = {
|
||||
draggableData?: NodeFieldDraggableData;
|
||||
};
|
||||
|
||||
function EditableControls(props: EditableControlsProps) {
|
||||
const EditableControls = memo((props: EditableControlsProps) => {
|
||||
const { isEditing, getEditButtonProps } = useEditableControls();
|
||||
const handleDoubleClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
@ -158,4 +158,6 @@ function EditableControls(props: EditableControlsProps) {
|
||||
cursor="text"
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
EditableControls.displayName = 'EditableControls';
|
||||
|
@ -1,38 +1,53 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import {
|
||||
useFieldData,
|
||||
useFieldTemplate,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { FIELDS } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
OutputFieldTemplate,
|
||||
OutputFieldValue,
|
||||
isInputFieldTemplate,
|
||||
isInputFieldValue,
|
||||
} from 'features/nodes/types/types';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface Props {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue | OutputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate | OutputFieldTemplate;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'input' | 'output';
|
||||
}
|
||||
|
||||
const FieldTooltipContent = ({ field, fieldTemplate }: Props) => {
|
||||
const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
|
||||
const field = useFieldData(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||
const isInputTemplate = isInputFieldTemplate(fieldTemplate);
|
||||
const fieldTitle = useMemo(() => {
|
||||
if (isInputFieldValue(field)) {
|
||||
if (field.label && fieldTemplate) {
|
||||
return `${field.label} (${fieldTemplate.title})`;
|
||||
}
|
||||
|
||||
if (field.label && !fieldTemplate) {
|
||||
return field.label;
|
||||
}
|
||||
|
||||
if (!field.label && fieldTemplate) {
|
||||
return fieldTemplate.title;
|
||||
}
|
||||
|
||||
return 'Unknown Field';
|
||||
}
|
||||
}, [field, fieldTemplate]);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDir: 'column' }}>
|
||||
<Text sx={{ fontWeight: 600 }}>
|
||||
{isInputFieldValue(field) && field.label
|
||||
? `${field.label} (${fieldTemplate.title})`
|
||||
: fieldTemplate.title}
|
||||
</Text>
|
||||
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
<Text>Type: {FIELDS[fieldTemplate.type].title}</Text>
|
||||
<Text sx={{ fontWeight: 600 }}>{fieldTitle}</Text>
|
||||
{fieldTemplate && (
|
||||
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
|
||||
{fieldTemplate.description}
|
||||
</Text>
|
||||
)}
|
||||
{fieldTemplate && <Text>Type: {FIELDS[fieldTemplate.type].title}</Text>}
|
||||
{isInputTemplate && <Text>Input: {startCase(fieldTemplate.input)}</Text>}
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,27 +1,24 @@
|
||||
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { PropsWithChildren, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
useDoesInputHaveValue,
|
||||
useFieldTemplate,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { PropsWithChildren, memo, useMemo } from 'react';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldTitle from './FieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
|
||||
interface Props {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const InputField = (props: Props) => {
|
||||
const { nodeProps, nodeTemplate, field } = props;
|
||||
const { id: nodeId } = nodeProps.data;
|
||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
|
||||
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
@ -29,15 +26,10 @@ const InputField = (props: Props) => {
|
||||
isConnectionStartField,
|
||||
connectionError,
|
||||
shouldDim,
|
||||
} = useConnectionState({ nodeId, field, kind: 'input' });
|
||||
|
||||
const fieldTemplate = useMemo(
|
||||
() => nodeTemplate.inputs[field.name],
|
||||
[field.name, nodeTemplate.inputs]
|
||||
);
|
||||
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
|
||||
|
||||
const isMissingInput = useMemo(() => {
|
||||
if (!fieldTemplate) {
|
||||
if (fieldTemplate?.fieldKind !== 'input') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -49,18 +41,18 @@ const InputField = (props: Props) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!field.value && !isConnected && fieldTemplate.input === 'any') {
|
||||
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input === 'any') {
|
||||
return true;
|
||||
}
|
||||
}, [fieldTemplate, isConnected, field.value]);
|
||||
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
|
||||
|
||||
if (!fieldTemplate) {
|
||||
if (fieldTemplate?.fieldKind !== 'input') {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl
|
||||
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
|
||||
>
|
||||
Unknown input: {field.name}
|
||||
Unknown input: {fieldName}
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
@ -82,10 +74,9 @@ const InputField = (props: Props) => {
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeData={nodeProps.data}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
@ -95,27 +86,18 @@ const InputField = (props: Props) => {
|
||||
>
|
||||
<FormLabel sx={{ mb: 0 }}>
|
||||
<FieldTitle
|
||||
nodeData={nodeProps.data}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
isDraggable
|
||||
/>
|
||||
</FormLabel>
|
||||
</Tooltip>
|
||||
<InputFieldRenderer
|
||||
nodeData={nodeProps.data}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormControl>
|
||||
|
||||
{fieldTemplate.input !== 'direct' && (
|
||||
<FieldHandle
|
||||
nodeProps={nodeProps}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="target"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
@ -133,21 +115,25 @@ type InputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
}>;
|
||||
|
||||
const InputFieldWrapper = ({ shouldDim, children }: InputFieldWrapperProps) => (
|
||||
<Flex
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
const InputFieldWrapper = memo(
|
||||
({ shouldDim, children }: InputFieldWrapperProps) => (
|
||||
<Flex
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
|
||||
InputFieldWrapper.displayName = 'InputFieldWrapper';
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from '../../types/types';
|
||||
useFieldData,
|
||||
useFieldTemplate,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { memo } from 'react';
|
||||
import BooleanInputField from './fieldTypes/BooleanInputField';
|
||||
import ClipInputField from './fieldTypes/ClipInputField';
|
||||
import CollectionInputField from './fieldTypes/CollectionInputField';
|
||||
@ -29,33 +27,33 @@ import VaeInputField from './fieldTypes/VaeInputField';
|
||||
import VaeModelInputField from './fieldTypes/VaeModelInputField';
|
||||
|
||||
type InputFieldProps = {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
// build an individual input element based on the schema
|
||||
const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
const { nodeData, nodeTemplate, field, fieldTemplate } = props;
|
||||
const { type } = field;
|
||||
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
|
||||
const field = useFieldData(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
|
||||
|
||||
if (type === 'string' && fieldTemplate.type === 'string') {
|
||||
if (fieldTemplate?.fieldKind === 'output') {
|
||||
return <Box p={2}>Output field in input: {field?.type}</Box>;
|
||||
}
|
||||
|
||||
if (field?.type === 'string' && fieldTemplate?.type === 'string') {
|
||||
return (
|
||||
<StringInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'boolean' && fieldTemplate.type === 'boolean') {
|
||||
if (field?.type === 'boolean' && fieldTemplate?.type === 'boolean') {
|
||||
return (
|
||||
<BooleanInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
@ -63,46 +61,32 @@ const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
(type === 'integer' && fieldTemplate.type === 'integer') ||
|
||||
(type === 'float' && fieldTemplate.type === 'float')
|
||||
(field?.type === 'integer' && fieldTemplate?.type === 'integer') ||
|
||||
(field?.type === 'float' && fieldTemplate?.type === 'float')
|
||||
) {
|
||||
return (
|
||||
<NumberInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'enum' && fieldTemplate.type === 'enum') {
|
||||
if (field?.type === 'enum' && fieldTemplate?.type === 'enum') {
|
||||
return (
|
||||
<EnumInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'ImageField' && fieldTemplate.type === 'ImageField') {
|
||||
if (field?.type === 'ImageField' && fieldTemplate?.type === 'ImageField') {
|
||||
return (
|
||||
<ImageInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'LatentsField' && fieldTemplate.type === 'LatentsField') {
|
||||
return (
|
||||
<LatentsInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
@ -110,68 +94,55 @@ const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'ConditioningField' &&
|
||||
fieldTemplate.type === 'ConditioningField'
|
||||
field?.type === 'LatentsField' &&
|
||||
fieldTemplate?.type === 'LatentsField'
|
||||
) {
|
||||
return (
|
||||
<LatentsInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'ConditioningField' &&
|
||||
fieldTemplate?.type === 'ConditioningField'
|
||||
) {
|
||||
return (
|
||||
<ConditioningInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'UNetField' && fieldTemplate.type === 'UNetField') {
|
||||
if (field?.type === 'UNetField' && fieldTemplate?.type === 'UNetField') {
|
||||
return (
|
||||
<UnetInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'ClipField' && fieldTemplate.type === 'ClipField') {
|
||||
if (field?.type === 'ClipField' && fieldTemplate?.type === 'ClipField') {
|
||||
return (
|
||||
<ClipInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'VaeField' && fieldTemplate.type === 'VaeField') {
|
||||
if (field?.type === 'VaeField' && fieldTemplate?.type === 'VaeField') {
|
||||
return (
|
||||
<VaeInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'ControlField' && fieldTemplate.type === 'ControlField') {
|
||||
return (
|
||||
<ControlInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'MainModelField' && fieldTemplate.type === 'MainModelField') {
|
||||
return (
|
||||
<MainModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
@ -179,35 +150,38 @@ const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'SDXLRefinerModelField' &&
|
||||
fieldTemplate.type === 'SDXLRefinerModelField'
|
||||
field?.type === 'ControlField' &&
|
||||
fieldTemplate?.type === 'ControlField'
|
||||
) {
|
||||
return (
|
||||
<ControlInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'MainModelField' &&
|
||||
fieldTemplate?.type === 'MainModelField'
|
||||
) {
|
||||
return (
|
||||
<MainModelInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'SDXLRefinerModelField' &&
|
||||
fieldTemplate?.type === 'SDXLRefinerModelField'
|
||||
) {
|
||||
return (
|
||||
<RefinerModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'VaeModelField' && fieldTemplate.type === 'VaeModelField') {
|
||||
return (
|
||||
<VaeModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'LoRAModelField' && fieldTemplate.type === 'LoRAModelField') {
|
||||
return (
|
||||
<LoRAModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
@ -215,57 +189,48 @@ const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'ControlNetModelField' &&
|
||||
fieldTemplate.type === 'ControlNetModelField'
|
||||
field?.type === 'VaeModelField' &&
|
||||
fieldTemplate?.type === 'VaeModelField'
|
||||
) {
|
||||
return (
|
||||
<VaeModelInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'LoRAModelField' &&
|
||||
fieldTemplate?.type === 'LoRAModelField'
|
||||
) {
|
||||
return (
|
||||
<LoRAModelInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'ControlNetModelField' &&
|
||||
fieldTemplate?.type === 'ControlNetModelField'
|
||||
) {
|
||||
return (
|
||||
<ControlNetModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'Collection' && fieldTemplate.type === 'Collection') {
|
||||
if (field?.type === 'Collection' && fieldTemplate?.type === 'Collection') {
|
||||
return (
|
||||
<CollectionInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'CollectionItem' && fieldTemplate.type === 'CollectionItem') {
|
||||
return (
|
||||
<CollectionItemInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'ColorField' && fieldTemplate.type === 'ColorField') {
|
||||
return (
|
||||
<ColorInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'ImageCollection' && fieldTemplate.type === 'ImageCollection') {
|
||||
return (
|
||||
<ImageCollectionInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
@ -273,20 +238,55 @@ const InputFieldRenderer = (props: InputFieldProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'SDXLMainModelField' &&
|
||||
fieldTemplate.type === 'SDXLMainModelField'
|
||||
field?.type === 'CollectionItem' &&
|
||||
fieldTemplate?.type === 'CollectionItem'
|
||||
) {
|
||||
return (
|
||||
<SDXLMainModelInputField
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
<CollectionItemInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Box p={2}>Unknown field type: {type}</Box>;
|
||||
if (field?.type === 'ColorField' && fieldTemplate?.type === 'ColorField') {
|
||||
return (
|
||||
<ColorInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'ImageCollection' &&
|
||||
fieldTemplate?.type === 'ImageCollection'
|
||||
) {
|
||||
return (
|
||||
<ImageCollectionInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field?.type === 'SDXLMainModelField' &&
|
||||
fieldTemplate?.type === 'SDXLMainModelField'
|
||||
) {
|
||||
return (
|
||||
<SDXLMainModelInputField
|
||||
nodeId={nodeId}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Box p={2}>Unknown field type: {field?.type}</Box>;
|
||||
};
|
||||
|
||||
export default memo(InputFieldRenderer);
|
||||
|
@ -1,39 +1,16 @@
|
||||
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
import { memo } from 'react';
|
||||
import FieldTitle from './FieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
|
||||
type Props = {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const LinearViewField = ({
|
||||
nodeData,
|
||||
nodeTemplate,
|
||||
field,
|
||||
fieldTemplate,
|
||||
}: Props) => {
|
||||
// const dispatch = useAppDispatch();
|
||||
// const handleRemoveField = useCallback(() => {
|
||||
// dispatch(
|
||||
// workflowExposedFieldRemoved({
|
||||
// nodeId: nodeData.id,
|
||||
// fieldName: field.name,
|
||||
// })
|
||||
// );
|
||||
// }, [dispatch, field.name, nodeData.id]);
|
||||
|
||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="second"
|
||||
@ -48,10 +25,9 @@ const LinearViewField = ({
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
@ -66,20 +42,10 @@ const LinearViewField = ({
|
||||
mb: 0,
|
||||
}}
|
||||
>
|
||||
<FieldTitle
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
||||
</FormLabel>
|
||||
</Tooltip>
|
||||
<InputFieldRenderer
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -6,25 +6,19 @@ import {
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { useFieldTemplate } from 'features/nodes/hooks/useNodeData';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
OutputFieldValue,
|
||||
} from 'features/nodes/types/types';
|
||||
import { PropsWithChildren, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
|
||||
interface Props {
|
||||
nodeProps: NodeProps<InvocationNodeData>;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: OutputFieldValue;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const OutputField = (props: Props) => {
|
||||
const { nodeTemplate, nodeProps, field } = props;
|
||||
const OutputField = ({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output');
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
@ -32,20 +26,15 @@ const OutputField = (props: Props) => {
|
||||
isConnectionStartField,
|
||||
connectionError,
|
||||
shouldDim,
|
||||
} = useConnectionState({ nodeId: nodeProps.data.id, field, kind: 'output' });
|
||||
} = useConnectionState({ nodeId, fieldName, kind: 'output' });
|
||||
|
||||
const fieldTemplate = useMemo(
|
||||
() => nodeTemplate.outputs[field.name],
|
||||
[field.name, nodeTemplate]
|
||||
);
|
||||
|
||||
if (!fieldTemplate) {
|
||||
if (fieldTemplate?.fieldKind !== 'output') {
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl
|
||||
sx={{ color: 'error.400', textAlign: 'right', fontSize: 'sm' }}
|
||||
>
|
||||
Unknown output: {field.name}
|
||||
Unknown output: {fieldName}
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
@ -57,10 +46,9 @@ const OutputField = (props: Props) => {
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeData={nodeProps.data}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="output"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
@ -75,9 +63,6 @@ const OutputField = (props: Props) => {
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
<FieldHandle
|
||||
nodeProps={nodeProps}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="source"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
@ -88,27 +73,28 @@ const OutputField = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default OutputField;
|
||||
export default memo(OutputField);
|
||||
|
||||
type OutputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
}>;
|
||||
|
||||
const OutputFieldWrapper = ({
|
||||
shouldDim,
|
||||
children,
|
||||
}: OutputFieldWrapperProps) => (
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
const OutputFieldWrapper = memo(
|
||||
({ shouldDim, children }: OutputFieldWrapperProps) => (
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
|
||||
OutputFieldWrapper.displayName = 'OutputFieldWrapper';
|
||||
|
@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
|
||||
const BooleanInputFieldComponent = (
|
||||
props: FieldComponentProps<BooleanInputFieldValue, BooleanInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
|
||||
const ColorInputFieldComponent = (
|
||||
props: FieldComponentProps<ColorInputFieldValue, ColorInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -19,8 +19,7 @@ const ControlNetModelInputFieldComponent = (
|
||||
ControlNetModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const controlNetModel = field.value;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
|
||||
const EnumInputFieldComponent = (
|
||||
props: FieldComponentProps<EnumInputFieldValue, EnumInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeData, field, fieldTemplate } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -19,8 +19,7 @@ const ImageCollectionInputFieldComponent = (
|
||||
ImageCollectionInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
|
||||
// const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -21,8 +21,7 @@ import { FieldComponentProps } from './types';
|
||||
const ImageInputFieldComponent = (
|
||||
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(
|
||||
|
@ -21,8 +21,7 @@ const LoRAModelInputFieldComponent = (
|
||||
LoRAModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const lora = field.value;
|
||||
const dispatch = useAppDispatch();
|
||||
const { data: loraModels } = useGetLoRAModelsQuery();
|
||||
|
@ -26,8 +26,7 @@ const MainModelInputFieldComponent = (
|
||||
MainModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
|
||||
|
||||
|
@ -23,8 +23,7 @@ const NumberInputFieldComponent = (
|
||||
IntegerInputFieldTemplate | FloatInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field, fieldTemplate } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const [valueAsString, setValueAsString] = useState<string>(
|
||||
String(field.value)
|
||||
|
@ -24,8 +24,7 @@ const RefinerModelInputFieldComponent = (
|
||||
SDXLRefinerModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
|
||||
|
@ -27,8 +27,7 @@ const ModelInputFieldComponent = (
|
||||
SDXLMainModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
|
||||
|
@ -12,8 +12,7 @@ import { FieldComponentProps } from './types';
|
||||
const StringInputFieldComponent = (
|
||||
props: FieldComponentProps<StringInputFieldValue, StringInputFieldTemplate>
|
||||
) => {
|
||||
const { nodeData, field, fieldTemplate } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleValueChanged = useCallback(
|
||||
|
@ -20,8 +20,7 @@ const VaeModelInputFieldComponent = (
|
||||
VaeModelInputFieldTemplate
|
||||
>
|
||||
) => {
|
||||
const { nodeData, field } = props;
|
||||
const nodeId = nodeData.id;
|
||||
const { nodeId, field } = props;
|
||||
const vae = field.value;
|
||||
const dispatch = useAppDispatch();
|
||||
const { data: vaeModels } = useGetVaeModelsQuery();
|
||||
|
@ -1,16 +1,13 @@
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/types';
|
||||
|
||||
export type FieldComponentProps<
|
||||
V extends InputFieldValue,
|
||||
T extends InputFieldTemplate
|
||||
> = {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
nodeId: string;
|
||||
field: V;
|
||||
fieldTemplate: T;
|
||||
};
|
||||
|
@ -55,7 +55,11 @@ const CurrentImageNode = (props: NodeProps) => {
|
||||
export default memo(CurrentImageNode);
|
||||
|
||||
const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => (
|
||||
<NodeWrapper nodeProps={props.nodeProps} width={384}>
|
||||
<NodeWrapper
|
||||
nodeId={props.nodeProps.data.id}
|
||||
selected={props.nodeProps.selected}
|
||||
width={384}
|
||||
>
|
||||
<Flex
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
sx={{
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { makeTemplateSelector } from 'features/nodes/store/util/makeTemplateSelector';
|
||||
import { InvocationNodeData } from 'features/nodes/types/types';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
@ -7,18 +8,40 @@ import InvocationNode from '../Invocation/InvocationNode';
|
||||
import UnknownNodeFallback from '../Invocation/UnknownNodeFallback';
|
||||
|
||||
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
|
||||
const { data } = props;
|
||||
const { type } = data;
|
||||
const { data, selected } = props;
|
||||
const { id: nodeId, type, isOpen, label } = data;
|
||||
|
||||
const templateSelector = useMemo(() => makeTemplateSelector(type), [type]);
|
||||
const hasTemplateSelector = useMemo(
|
||||
() =>
|
||||
createSelector(stateSelector, ({ nodes }) =>
|
||||
Boolean(nodes.nodeTemplates[type])
|
||||
),
|
||||
[type]
|
||||
);
|
||||
|
||||
const nodeTemplate = useAppSelector(templateSelector);
|
||||
const nodeTemplate = useAppSelector(hasTemplateSelector);
|
||||
|
||||
if (!nodeTemplate) {
|
||||
return <UnknownNodeFallback nodeProps={props} />;
|
||||
return (
|
||||
<UnknownNodeFallback
|
||||
nodeId={nodeId}
|
||||
isOpen={isOpen}
|
||||
label={label}
|
||||
type={type}
|
||||
selected={selected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <InvocationNode nodeProps={props} nodeTemplate={nodeTemplate} />;
|
||||
return (
|
||||
<InvocationNode
|
||||
nodeId={nodeId}
|
||||
isOpen={isOpen}
|
||||
label={label}
|
||||
type={type}
|
||||
selected={selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InvocationNodeWrapper);
|
||||
|
@ -10,7 +10,7 @@ import NodeTitle from '../Invocation/NodeTitle';
|
||||
import NodeWrapper from '../Invocation/NodeWrapper';
|
||||
|
||||
const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
||||
const { id: nodeId, data } = props;
|
||||
const { id: nodeId, data, selected } = props;
|
||||
const { notes, isOpen } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChange = useCallback(
|
||||
@ -21,7 +21,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeWrapper nodeProps={props}>
|
||||
<NodeWrapper nodeId={nodeId} selected={selected}>
|
||||
<Flex
|
||||
layerStyle="nodeHeader"
|
||||
sx={{
|
||||
@ -32,8 +32,8 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
||||
h: 8,
|
||||
}}
|
||||
>
|
||||
<NodeCollapseButton nodeProps={props} />
|
||||
<NodeTitle nodeData={props.data} title="Notes" />
|
||||
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
|
||||
<NodeTitle nodeId={nodeId} title="Notes" />
|
||||
<Box minW={8} />
|
||||
</Flex>
|
||||
{isOpen && (
|
||||
|
@ -6,39 +6,11 @@ import {
|
||||
TabPanels,
|
||||
Tabs,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const lastSelectedNodeId =
|
||||
nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
||||
|
||||
const lastSelectedNode = nodes.nodes.find(
|
||||
(node) => node.id === lastSelectedNodeId
|
||||
);
|
||||
|
||||
const lastSelectedNodeTemplate = lastSelectedNode
|
||||
? nodes.nodeTemplates[lastSelectedNode.data.type]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
node: lastSelectedNode,
|
||||
template: lastSelectedNodeTemplate,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
import NodeDataInspector from './NodeDataInspector';
|
||||
import NodeTemplateInspector from './NodeTemplateInspector';
|
||||
|
||||
const InspectorPanel = () => {
|
||||
const { node, template } = useAppSelector(selector);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
@ -60,37 +32,10 @@ const InspectorPanel = () => {
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
{template ? (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<ImageMetadataJSON
|
||||
jsonObject={template}
|
||||
label="Node Template"
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<IAINoContentFallback
|
||||
label={
|
||||
node
|
||||
? 'No template found for selected node'
|
||||
: 'No node selected'
|
||||
}
|
||||
icon={null}
|
||||
/>
|
||||
)}
|
||||
<NodeTemplateInspector />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{node ? (
|
||||
<ImageMetadataJSON jsonObject={node.data} label="Node Data" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No node selected" icon={null} />
|
||||
)}
|
||||
<NodeDataInspector />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
@ -17,20 +17,20 @@ const selector = createSelector(
|
||||
);
|
||||
|
||||
return {
|
||||
node: lastSelectedNode,
|
||||
data: lastSelectedNode?.data,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const NodeDataInspector = () => {
|
||||
const { node } = useAppSelector(selector);
|
||||
const { data } = useAppSelector(selector);
|
||||
|
||||
return node ? (
|
||||
<ImageMetadataJSON jsonObject={node.data} label="Node Data" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No node data" icon={null} />
|
||||
);
|
||||
if (!data) {
|
||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||
}
|
||||
|
||||
return <ImageMetadataJSON jsonObject={data} label="Node Data" />;
|
||||
};
|
||||
|
||||
export default memo(NodeDataInspector);
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const lastSelectedNodeId =
|
||||
nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
||||
|
||||
const lastSelectedNode = nodes.nodes.find(
|
||||
(node) => node.id === lastSelectedNodeId
|
||||
);
|
||||
|
||||
const lastSelectedNodeTemplate = lastSelectedNode
|
||||
? nodes.nodeTemplates[lastSelectedNode.data.type]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
template: lastSelectedNodeTemplate,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const NodeTemplateInspector = () => {
|
||||
const { template } = useAppSelector(selector);
|
||||
|
||||
if (!template) {
|
||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||
}
|
||||
|
||||
return <ImageMetadataJSON jsonObject={template} label="Node Template" />;
|
||||
};
|
||||
|
||||
export default memo(NodeTemplateInspector);
|
@ -6,14 +6,6 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { AddFieldToLinearViewDropData } from 'features/dnd/types';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
isInvocationNode,
|
||||
} from 'features/nodes/types/types';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
import LinearViewField from '../../fields/LinearViewField';
|
||||
import ScrollableContent from '../ScrollableContent';
|
||||
@ -21,41 +13,8 @@ import ScrollableContent from '../ScrollableContent';
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const fields: {
|
||||
nodeData: InvocationNodeData;
|
||||
nodeTemplate: InvocationTemplate;
|
||||
field: InputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate;
|
||||
}[] = [];
|
||||
const { exposedFields } = nodes.workflow;
|
||||
nodes.nodes.filter(isInvocationNode).forEach((node) => {
|
||||
const nodeTemplate = nodes.nodeTemplates[node.data.type];
|
||||
if (!nodeTemplate) {
|
||||
return;
|
||||
}
|
||||
forEach(node.data.inputs, (field) => {
|
||||
if (
|
||||
!exposedFields.some(
|
||||
(f) => f.nodeId === node.id && f.fieldName === field.name
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const fieldTemplate = nodeTemplate.inputs[field.name];
|
||||
if (!fieldTemplate) {
|
||||
return;
|
||||
}
|
||||
fields.push({
|
||||
nodeData: node.data,
|
||||
nodeTemplate,
|
||||
field,
|
||||
fieldTemplate,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
fields,
|
||||
fields: nodes.workflow.exposedFields,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
@ -89,13 +48,11 @@ const LinearTabContent = () => {
|
||||
}}
|
||||
>
|
||||
{fields.length ? (
|
||||
fields.map(({ nodeData, nodeTemplate, field, fieldTemplate }) => (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<LinearViewField
|
||||
key={field.id}
|
||||
nodeData={nodeData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
field={field}
|
||||
fieldTemplate={fieldTemplate}
|
||||
key={`${nodeId}-${fieldName}`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector';
|
||||
import { InputFieldValue, OutputFieldValue } from 'features/nodes/types/types';
|
||||
import { useMemo } from 'react';
|
||||
import { useFieldType } from './useNodeData';
|
||||
|
||||
const selectIsConnectionInProgress = createSelector(
|
||||
stateSelector,
|
||||
@ -12,23 +12,19 @@ const selectIsConnectionInProgress = createSelector(
|
||||
nodes.connectionStartParams !== null
|
||||
);
|
||||
|
||||
export type UseConnectionStateProps =
|
||||
| {
|
||||
nodeId: string;
|
||||
field: InputFieldValue;
|
||||
kind: 'input';
|
||||
}
|
||||
| {
|
||||
nodeId: string;
|
||||
field: OutputFieldValue;
|
||||
kind: 'output';
|
||||
};
|
||||
export type UseConnectionStateProps = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'input' | 'output';
|
||||
};
|
||||
|
||||
export const useConnectionState = ({
|
||||
nodeId,
|
||||
field,
|
||||
fieldName,
|
||||
kind,
|
||||
}: UseConnectionStateProps) => {
|
||||
const fieldType = useFieldType(nodeId, fieldName, kind);
|
||||
|
||||
const selectIsConnected = useMemo(
|
||||
() =>
|
||||
createSelector(stateSelector, ({ nodes }) =>
|
||||
@ -37,23 +33,23 @@ export const useConnectionState = ({
|
||||
return (
|
||||
(kind === 'input' ? edge.target : edge.source) === nodeId &&
|
||||
(kind === 'input' ? edge.targetHandle : edge.sourceHandle) ===
|
||||
field.name
|
||||
fieldName
|
||||
);
|
||||
}).length
|
||||
)
|
||||
),
|
||||
[field.name, kind, nodeId]
|
||||
[fieldName, kind, nodeId]
|
||||
);
|
||||
|
||||
const selectConnectionError = useMemo(
|
||||
() =>
|
||||
makeConnectionErrorSelector(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldName,
|
||||
kind === 'input' ? 'target' : 'source',
|
||||
field.type
|
||||
fieldType
|
||||
),
|
||||
[nodeId, field.name, field.type, kind]
|
||||
[nodeId, fieldName, kind, fieldType]
|
||||
);
|
||||
|
||||
const selectIsConnectionStartField = useMemo(
|
||||
@ -61,12 +57,12 @@ export const useConnectionState = ({
|
||||
createSelector(stateSelector, ({ nodes }) =>
|
||||
Boolean(
|
||||
nodes.connectionStartParams?.nodeId === nodeId &&
|
||||
nodes.connectionStartParams?.handleId === field.name &&
|
||||
nodes.connectionStartParams?.handleId === fieldName &&
|
||||
nodes.connectionStartParams?.handleType ===
|
||||
{ input: 'target', output: 'source' }[kind]
|
||||
)
|
||||
),
|
||||
[field.name, kind, nodeId]
|
||||
[fieldName, kind, nodeId]
|
||||
);
|
||||
|
||||
const isConnected = useAppSelector(selectIsConnected);
|
||||
|
289
invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts
Normal file
289
invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts
Normal file
@ -0,0 +1,289 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { map, some } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
FOOTER_FIELDS,
|
||||
IMAGE_FIELDS,
|
||||
} from '../components/Invocation/NodeFooter';
|
||||
import { isInvocationNode } from '../types/types';
|
||||
|
||||
const KIND_MAP = {
|
||||
input: 'inputs' as const,
|
||||
output: 'outputs' as const,
|
||||
};
|
||||
|
||||
export const useNodeTemplate = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||
return nodeTemplate;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const nodeTemplate = useAppSelector(selector);
|
||||
|
||||
return nodeTemplate;
|
||||
};
|
||||
|
||||
export const useNodeData = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
return node?.data;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const nodeData = useAppSelector(selector);
|
||||
|
||||
return nodeData;
|
||||
};
|
||||
|
||||
export const useFieldData = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return node?.data.inputs[fieldName];
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const fieldData = useAppSelector(selector);
|
||||
|
||||
return fieldData;
|
||||
};
|
||||
|
||||
export const useFieldType = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
kind: 'input' | 'output'
|
||||
) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return node?.data[KIND_MAP[kind]][fieldName]?.type;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, kind, nodeId]
|
||||
);
|
||||
|
||||
const fieldType = useAppSelector(selector);
|
||||
|
||||
return fieldType;
|
||||
};
|
||||
|
||||
export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return [];
|
||||
}
|
||||
return map(node.data[KIND_MAP[kind]], (field) => field.name).filter(
|
||||
(fieldName) => fieldName !== 'is_intermediate'
|
||||
);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[kind, nodeId]
|
||||
);
|
||||
|
||||
const fieldNames = useAppSelector(selector);
|
||||
return fieldNames;
|
||||
};
|
||||
|
||||
export const useWithFooter = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
return some(node.data.outputs, (output) =>
|
||||
FOOTER_FIELDS.includes(output.type)
|
||||
);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const withFooter = useAppSelector(selector);
|
||||
return withFooter;
|
||||
};
|
||||
|
||||
export const useHasImageOutput = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
return some(node.data.outputs, (output) =>
|
||||
IMAGE_FIELDS.includes(output.type)
|
||||
);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const hasImageOutput = useAppSelector(selector);
|
||||
return hasImageOutput;
|
||||
};
|
||||
|
||||
export const useIsIntermediate = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(node.data.inputs.is_intermediate?.value);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const is_intermediate = useAppSelector(selector);
|
||||
return is_intermediate;
|
||||
};
|
||||
|
||||
export const useNodeLabel = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.data.label;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const label = useAppSelector(selector);
|
||||
return label;
|
||||
};
|
||||
|
||||
export const useNodeTemplateTitle = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
const nodeTemplate = node
|
||||
? nodes.nodeTemplates[node.data.type]
|
||||
: undefined;
|
||||
|
||||
return nodeTemplate?.title;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const title = useAppSelector(selector);
|
||||
return title;
|
||||
};
|
||||
|
||||
export const useFieldTemplate = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
kind: 'input' | 'output'
|
||||
) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||
return nodeTemplate?.[KIND_MAP[kind]][fieldName];
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, kind, nodeId]
|
||||
);
|
||||
|
||||
const fieldTemplate = useAppSelector(selector);
|
||||
|
||||
return fieldTemplate;
|
||||
};
|
||||
|
||||
export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return Boolean(node?.data.inputs[fieldName]?.value);
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const doesFieldHaveValue = useAppSelector(selector);
|
||||
|
||||
return doesFieldHaveValue;
|
||||
};
|
@ -9,9 +9,13 @@ export const makeConnectionErrorSelector = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
handleType: HandleType,
|
||||
fieldType: FieldType
|
||||
fieldType?: FieldType
|
||||
) =>
|
||||
createSelector(stateSelector, (state) => {
|
||||
if (!fieldType) {
|
||||
return 'No field type';
|
||||
}
|
||||
|
||||
const { currentConnectionFieldType, connectionStartParams, nodes, edges } =
|
||||
state.nodes;
|
||||
|
||||
|
@ -457,12 +457,13 @@ export type ColorInputFieldTemplate = InputFieldTemplateBase & {
|
||||
};
|
||||
|
||||
export const isInputFieldValue = (
|
||||
field: InputFieldValue | OutputFieldValue
|
||||
): field is InputFieldValue => field.fieldKind === 'input';
|
||||
field?: InputFieldValue | OutputFieldValue
|
||||
): field is InputFieldValue => Boolean(field && field.fieldKind === 'input');
|
||||
|
||||
export const isInputFieldTemplate = (
|
||||
fieldTemplate: InputFieldTemplate | OutputFieldTemplate
|
||||
): fieldTemplate is InputFieldTemplate => fieldTemplate.fieldKind === 'input';
|
||||
fieldTemplate?: InputFieldTemplate | OutputFieldTemplate
|
||||
): fieldTemplate is InputFieldTemplate =>
|
||||
Boolean(fieldTemplate && fieldTemplate.fieldKind === 'input');
|
||||
|
||||
/**
|
||||
* JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES
|
||||
@ -632,20 +633,22 @@ export type NodeData =
|
||||
|
||||
export const isInvocationNode = (
|
||||
node?: Node<NodeData>
|
||||
): node is Node<InvocationNodeData> => node?.type === 'invocation';
|
||||
): node is Node<InvocationNodeData> =>
|
||||
Boolean(node && node.type === 'invocation');
|
||||
|
||||
export const isInvocationNodeData = (
|
||||
node?: NodeData
|
||||
): node is InvocationNodeData =>
|
||||
!['notes', 'current_image'].includes(node?.type ?? '');
|
||||
Boolean(node && !['notes', 'current_image'].includes(node.type));
|
||||
|
||||
export const isNotesNode = (
|
||||
node?: Node<NodeData>
|
||||
): node is Node<NotesNodeData> => node?.type === 'notes';
|
||||
): node is Node<NotesNodeData> => Boolean(node && node.type === 'notes');
|
||||
|
||||
export const isProgressImageNode = (
|
||||
node?: Node<NodeData>
|
||||
): node is Node<CurrentImageNodeData> => node?.type === 'current_image';
|
||||
): node is Node<CurrentImageNodeData> =>
|
||||
Boolean(node && node.type === 'current_image');
|
||||
|
||||
export enum NodeStatus {
|
||||
PENDING,
|
||||
|
Loading…
Reference in New Issue
Block a user