Merge branch 'main' into feat/batch-graphs

This commit is contained in:
Brandon 2023-08-16 14:03:34 -04:00 committed by GitHub
commit ef8dc2e8c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1318 additions and 951 deletions

View File

@ -48,7 +48,7 @@ class BooleanCollectionOutput(BaseInvocationOutput):
)
@title("Boolean")
@title("Boolean Primitive")
@tags("primitives", "boolean")
class BooleanInvocation(BaseInvocation):
"""A boolean primitive value"""
@ -62,7 +62,7 @@ class BooleanInvocation(BaseInvocation):
return BooleanOutput(a=self.a)
@title("Boolean Collection")
@title("Boolean Primitive Collection")
@tags("primitives", "boolean", "collection")
class BooleanCollectionInvocation(BaseInvocation):
"""A collection of boolean primitive values"""
@ -101,7 +101,7 @@ class IntegerCollectionOutput(BaseInvocationOutput):
)
@title("Integer")
@title("Integer Primitive")
@tags("primitives", "integer")
class IntegerInvocation(BaseInvocation):
"""An integer primitive value"""
@ -115,7 +115,7 @@ class IntegerInvocation(BaseInvocation):
return IntegerOutput(a=self.a)
@title("Integer Collection")
@title("Integer Primitive Collection")
@tags("primitives", "integer", "collection")
class IntegerCollectionInvocation(BaseInvocation):
"""A collection of integer primitive values"""
@ -154,7 +154,7 @@ class FloatCollectionOutput(BaseInvocationOutput):
)
@title("Float")
@title("Float Primitive")
@tags("primitives", "float")
class FloatInvocation(BaseInvocation):
"""A float primitive value"""
@ -168,7 +168,7 @@ class FloatInvocation(BaseInvocation):
return FloatOutput(a=self.param)
@title("Float Collection")
@title("Float Primitive Collection")
@tags("primitives", "float", "collection")
class FloatCollectionInvocation(BaseInvocation):
"""A collection of float primitive values"""
@ -207,7 +207,7 @@ class StringCollectionOutput(BaseInvocationOutput):
)
@title("String")
@title("String Primitive")
@tags("primitives", "string")
class StringInvocation(BaseInvocation):
"""A string primitive value"""
@ -221,7 +221,7 @@ class StringInvocation(BaseInvocation):
return StringOutput(text=self.text)
@title("String Collection")
@title("String Primitive Collection")
@tags("primitives", "string", "collection")
class StringCollectionInvocation(BaseInvocation):
"""A collection of string primitive values"""
@ -289,7 +289,7 @@ class ImageInvocation(BaseInvocation):
)
@title("Image Collection")
@title("Image Primitive Collection")
@tags("primitives", "image", "collection")
class ImageCollectionInvocation(BaseInvocation):
"""A collection of image primitive values"""
@ -357,7 +357,7 @@ class LatentsInvocation(BaseInvocation):
return build_latents_output(self.latents.latents_name, latents)
@title("Latents Collection")
@title("Latents Primitive Collection")
@tags("primitives", "latents", "collection")
class LatentsCollectionInvocation(BaseInvocation):
"""A collection of latents tensor primitive values"""
@ -475,7 +475,7 @@ class ConditioningInvocation(BaseInvocation):
return ConditioningOutput(conditioning=self.conditioning)
@title("Conditioning Collection")
@title("Conditioning Primitive Collection")
@tags("primitives", "conditioning", "collection")
class ConditioningCollectionInvocation(BaseInvocation):
"""A collection of conditioning tensor primitive values"""

View File

@ -0,0 +1,126 @@
/**
* This is a copy-paste of https://github.com/lukasbach/chakra-ui-contextmenu with a small change.
*
* The reactflow background element somehow prevents the chakra `useOutsideClick()` hook from working.
* With a menu open, clicking on the reactflow background element doesn't close the menu.
*
* Reactflow does provide an `onPaneClick` to handle clicks on the background element, but it is not
* straightforward to programatically close the menu.
*
* As a (hopefully temporary) workaround, we will use a dirty hack:
* - create `globalContextMenuCloseTrigger: number` in `ui` slice
* - increment it in `onPaneClick`
* - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes
*/
import {
Menu,
MenuButton,
MenuButtonProps,
MenuProps,
Portal,
PortalProps,
useEventListener,
} from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import * as React from 'react';
import {
MutableRefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
export interface IAIContextMenuProps<T extends HTMLElement> {
renderMenu: () => JSX.Element | null;
children: (ref: MutableRefObject<T | null>) => JSX.Element | null;
menuProps?: Omit<MenuProps, 'children'> & { children?: React.ReactNode };
portalProps?: Omit<PortalProps, 'children'> & { children?: React.ReactNode };
menuButtonProps?: MenuButtonProps;
}
export function IAIContextMenu<T extends HTMLElement = HTMLElement>(
props: IAIContextMenuProps<T>
) {
const [isOpen, setIsOpen] = useState(false);
const [isRendered, setIsRendered] = useState(false);
const [isDeferredOpen, setIsDeferredOpen] = useState(false);
const [position, setPosition] = useState<[number, number]>([0, 0]);
const targetRef = useRef<T>(null);
const globalContextMenuCloseTrigger = useAppSelector(
(state) => state.ui.globalContextMenuCloseTrigger
);
useEffect(() => {
if (isOpen) {
setTimeout(() => {
setIsRendered(true);
setTimeout(() => {
setIsDeferredOpen(true);
});
});
} else {
setIsDeferredOpen(false);
const timeout = setTimeout(() => {
setIsRendered(isOpen);
}, 1000);
return () => clearTimeout(timeout);
}
}, [isOpen]);
useEffect(() => {
setIsOpen(false);
setIsDeferredOpen(false);
setIsRendered(false);
}, [globalContextMenuCloseTrigger]);
useEventListener('contextmenu', (e) => {
if (
targetRef.current?.contains(e.target as HTMLElement) ||
e.target === targetRef.current
) {
e.preventDefault();
setIsOpen(true);
setPosition([e.pageX, e.pageY]);
} else {
setIsOpen(false);
}
});
const onCloseHandler = useCallback(() => {
props.menuProps?.onClose?.();
setIsOpen(false);
}, [props.menuProps]);
return (
<>
{props.children(targetRef)}
{isRendered && (
<Portal {...props.portalProps}>
<Menu
isOpen={isDeferredOpen}
gutter={0}
{...props.menuProps}
onClose={onCloseHandler}
>
<MenuButton
aria-hidden={true}
w={1}
h={1}
style={{
position: 'absolute',
left: position[0],
top: position[1],
cursor: 'default',
}}
{...props.menuButtonProps}
/>
{props.renderMenu()}
</Menu>
</Portal>
)}
</>
);
}

View File

@ -16,6 +16,7 @@ import ImageContextMenu from 'features/gallery/components/ImageContextMenu/Image
import {
MouseEvent,
ReactElement,
ReactNode,
SyntheticEvent,
memo,
useCallback,
@ -32,6 +33,17 @@ import {
TypesafeDroppableData,
} from 'features/dnd/types';
const defaultUploadElement = (
<Icon
as={FaUpload}
sx={{
boxSize: 16,
}}
/>
);
const defaultNoContentFallback = <IAINoContentFallback icon={FaImage} />;
type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
@ -47,13 +59,14 @@ type IAIDndImageProps = FlexProps & {
fitContainer?: boolean;
droppableData?: TypesafeDroppableData;
draggableData?: TypesafeDraggableData;
dropLabel?: string;
dropLabel?: ReactNode;
isSelected?: boolean;
thumbnail?: boolean;
noContentFallback?: ReactElement;
useThumbailFallback?: boolean;
withHoverOverlay?: boolean;
children?: JSX.Element;
uploadElement?: ReactNode;
};
const IAIDndImage = (props: IAIDndImageProps) => {
@ -74,7 +87,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel,
isSelected = false,
thumbnail = false,
noContentFallback = <IAINoContentFallback icon={FaImage} />,
noContentFallback = defaultNoContentFallback,
uploadElement = defaultUploadElement,
useThumbailFallback,
withHoverOverlay = false,
children,
@ -193,12 +207,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
{...getUploadButtonProps()}
>
<input {...getUploadInputProps()} />
<Icon
as={FaUpload}
sx={{
boxSize: 16,
}}
/>
{uploadElement}
</Flex>
</>
)}
@ -210,6 +219,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
onClick={onClick}
/>
)}
{children}
{!isDropDisabled && (
<IAIDroppable
data={droppableData}
@ -217,7 +227,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel={dropLabel}
/>
)}
{children}
</Flex>
)}
</ImageContextMenu>

View File

@ -1,5 +1,8 @@
import { MenuList } from '@chakra-ui/react';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import {
IAIContextMenu,
IAIContextMenuProps,
} from 'common/components/IAIContextMenu';
import { MouseEvent, memo, useCallback } from 'react';
import { ImageDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
@ -12,7 +15,7 @@ import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
type Props = {
imageDTO: ImageDTO | undefined;
children: ContextMenuProps<HTMLDivElement>['children'];
children: IAIContextMenuProps<HTMLDivElement>['children'];
};
const selector = createSelector(
@ -33,7 +36,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
}, []);
return (
<ContextMenu<HTMLDivElement>
<IAIContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
menuButtonProps={{
bg: 'transparent',
@ -68,7 +71,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
}}
>
{children}
</ContextMenu>
</IAIContextMenu>
);
};

View File

@ -2,8 +2,9 @@ import { Badge, Flex } 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 { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import {
BaseEdge,
EdgeLabelRenderer,
@ -20,78 +21,165 @@ const makeEdgeSelector = (
targetHandleId: string | null | undefined,
selected?: boolean
) =>
createSelector(stateSelector, ({ nodes }) => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
createSelector(
stateSelector,
({ nodes }) => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
const isInvocationToInvocationEdge =
isInvocationNode(sourceNode) && isInvocationNode(targetNode);
const isInvocationToInvocationEdge =
isInvocationNode(sourceNode) && isInvocationNode(targetNode);
const isSelected = sourceNode?.selected || targetNode?.selected || selected;
const sourceType = isInvocationToInvocationEdge
? sourceNode?.data?.outputs[sourceHandleId || '']?.type
: undefined;
const isSelected =
sourceNode?.selected || targetNode?.selected || selected;
const sourceType = isInvocationToInvocationEdge
? sourceNode?.data?.outputs[sourceHandleId || '']?.type
: undefined;
const stroke =
sourceType && nodes.shouldColorEdges
? colorTokenToCssVar(FIELDS[sourceType].color)
: colorTokenToCssVar('base.500');
const stroke =
sourceType && nodes.shouldColorEdges
? colorTokenToCssVar(FIELDS[sourceType].color)
: colorTokenToCssVar('base.500');
return {
isSelected,
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
stroke,
};
});
const CollapsedEdge = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
data,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps<{ count: number }>) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[selected, source, sourceHandleId, target, targetHandleId]
return {
isSelected,
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
stroke,
};
},
defaultSelectorOptions
);
const { isSelected, shouldAnimate } = useAppSelector(selector);
const [edgePath, labelX, labelY] = getBezierPath({
const CollapsedEdge = memo(
({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
sourcePosition,
targetPosition,
});
markerEnd,
data,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps<{ count: number }>) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[selected, source, sourceHandleId, target, targetHandleId]
);
const { base500 } = useChakraThemeTokens();
const { isSelected, shouldAnimate } = useAppSelector(selector);
return (
<>
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const { base500 } = useChakraThemeTokens();
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
{data?.count && data.count > 1 && (
<EdgeLabelRenderer>
<Flex
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
className="nodrag nopan"
>
<Badge
variant="solid"
sx={{
bg: 'base.500',
opacity: isSelected ? 0.8 : 0.5,
boxShadow: 'base',
}}
>
{data.count}
</Badge>
</Flex>
</EdgeLabelRenderer>
)}
</>
);
}
);
CollapsedEdge.displayName = 'CollapsedEdge';
const DefaultEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[source, sourceHandleId, target, targetHandleId, selected]
);
const { isSelected, shouldAnimate, stroke } = useAppSelector(selector);
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
stroke,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
@ -99,83 +187,11 @@ const CollapsedEdge = ({
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
{data?.count && data.count > 1 && (
<EdgeLabelRenderer>
<Flex
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
className="nodrag nopan"
>
<Badge
variant="solid"
sx={{
bg: 'base.500',
opacity: isSelected ? 0.8 : 0.5,
boxShadow: 'base',
}}
>
{data.count}
</Badge>
</Flex>
</EdgeLabelRenderer>
)}
</>
);
};
);
}
);
const DefaultEdge = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[source, sourceHandleId, target, targetHandleId, selected]
);
const { isSelected, shouldAnimate, stroke } = useAppSelector(selector);
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
);
};
DefaultEdge.displayName = 'DefaultEdge';
export const edgeTypes = {
collapsed: CollapsedEdge,

View File

@ -1,5 +1,6 @@
import { useToken } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { contextMenusClosed } from 'features/ui/store/uiSlice';
import { useCallback } from 'react';
import {
Background,
@ -114,6 +115,10 @@ export const Flow = () => {
[dispatch]
);
const handlePaneClick = useCallback(() => {
dispatch(contextMenusClosed());
}, [dispatch]);
return (
<ReactFlow
defaultViewport={viewport}
@ -132,12 +137,13 @@ export const Flow = () => {
connectionLineComponent={CustomConnectionLine}
onSelectionChange={handleSelectionChange}
isValidConnection={isValidConnection}
minZoom={0.2}
minZoom={0.1}
snapToGrid={shouldSnapToGrid}
snapGrid={[25, 25]}
connectionRadius={30}
proOptions={proOptions}
style={{ borderRadius }}
onPaneClick={handlePaneClick}
>
<TopLeftPanel />
<TopCenterPanel />

View File

@ -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>

View File

@ -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();

View File

@ -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' }}
/>

View File

@ -6,49 +6,19 @@ 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';
export const IMAGE_FIELDS = ['ImageField', 'ImageCollection'];
export const FOOTER_FIELDS = IMAGE_FIELDS;
import { ChangeEvent, memo, useCallback } from 'react';
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 +32,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';

View File

@ -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 }: 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>
);
};

View File

@ -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 <Text sx={{ fontWeight: 600 }}>Unknown Node</Text>;
}
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';

View File

@ -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';

View File

@ -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

View File

@ -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);

View File

@ -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',

View File

@ -46,7 +46,6 @@ const NodeEditor = () => {
<AnimatePresence>
{isReady && (
<motion.div
layoutId="node-editor-flow"
initial={{
opacity: 0,
}}
@ -67,7 +66,6 @@ const NodeEditor = () => {
<AnimatePresence>
{!isReady && (
<motion.div
layoutId="node-editor-loading"
initial={{
opacity: 0,
}}

View File

@ -15,7 +15,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch';
import { ChangeEvent, useCallback } from 'react';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa';
import {
shouldAnimateEdgesChanged,
@ -23,21 +23,26 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from '../store/nodesSlice';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
const selector = createSelector(stateSelector, ({ nodes }) => {
const {
shouldAnimateEdges,
shouldValidateGraph,
shouldSnapToGrid,
shouldColorEdges,
} = nodes;
return {
shouldAnimateEdges,
shouldValidateGraph,
shouldSnapToGrid,
shouldColorEdges,
};
});
const selector = createSelector(
stateSelector,
({ nodes }) => {
const {
shouldAnimateEdges,
shouldValidateGraph,
shouldSnapToGrid,
shouldColorEdges,
} = nodes;
return {
shouldAnimateEdges,
shouldValidateGraph,
shouldSnapToGrid,
shouldColorEdges,
};
},
defaultSelectorOptions
);
const NodeEditorSettings = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
@ -136,4 +141,4 @@ const NodeEditorSettings = () => {
);
};
export default NodeEditorSettings;
export default memo(NodeEditorSettings);

View File

@ -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;

View File

@ -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';

View File

@ -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>
);

View File

@ -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';

View File

@ -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);

View File

@ -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>
);

View File

@ -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';

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -19,8 +19,7 @@ const ImageCollectionInputFieldComponent = (
ImageCollectionInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
// const dispatch = useAppDispatch();

View File

@ -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(

View File

@ -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();

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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(

View File

@ -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();

View File

@ -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;
};

View File

@ -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={{

View File

@ -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);

View File

@ -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 && (

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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}
/>
))
) : (

View File

@ -25,7 +25,9 @@ const ClearGraphButton = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef<HTMLButtonElement | null>(null);
const nodes = useAppSelector((state: RootState) => state.nodes.nodes);
const nodesCount = useAppSelector(
(state: RootState) => state.nodes.nodes.length
);
const handleConfirmClear = useCallback(() => {
dispatch(nodeEditorReset());
@ -49,7 +51,7 @@ const ClearGraphButton = () => {
tooltip={t('nodes.clearGraph')}
aria-label={t('nodes.clearGraph')}
onClick={onOpen}
isDisabled={nodes.length === 0}
isDisabled={!nodesCount}
/>
<AlertDialog

View File

@ -8,7 +8,7 @@ import IAIIconButton, {
import { selectIsReadyNodes } from 'features/nodes/store/selectors';
import ProgressBar from 'features/system/components/ProgressBar';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa';
@ -18,7 +18,7 @@ interface InvokeButton
iconButton?: boolean;
}
export default function NodeInvokeButton(props: InvokeButton) {
const NodeInvokeButton = (props: InvokeButton) => {
const { iconButton = false, ...rest } = props;
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
@ -92,4 +92,6 @@ export default function NodeInvokeButton(props: InvokeButton) {
</Box>
</Box>
);
}
};
export default memo(NodeInvokeButton);

View File

@ -1,11 +1,11 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSyncAlt } from 'react-icons/fa';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
export default function ReloadSchemaButton() {
const ReloadSchemaButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@ -21,4 +21,6 @@ export default function ReloadSchemaButton() {
onClick={handleReloadSchema}
/>
);
}
};
export default memo(ReloadSchemaButton);

View File

@ -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);

View File

@ -0,0 +1,286 @@
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 '../types/constants';
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;
};

View File

@ -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;

View File

@ -6,6 +6,9 @@ export const NODE_WIDTH = 320;
export const NODE_MIN_WIDTH = 320;
export const DRAG_HANDLE_CLASSNAME = 'node-drag-handle';
export const IMAGE_FIELDS = ['ImageField', 'ImageCollection'];
export const FOOTER_FIELDS = IMAGE_FIELDS;
export const COLLECTION_TYPES: FieldType[] = [
'Collection',
'IntegerCollection',

View File

@ -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,

View File

@ -3,4 +3,7 @@ import { UIState } from './uiTypes';
/**
* UI slice persist denylist
*/
export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails'];
export const uiPersistDenylist: (keyof UIState)[] = [
'shouldShowImageDetails',
'globalContextMenuCloseTrigger',
];

View File

@ -20,6 +20,7 @@ export const initialUIState: UIState = {
shouldShowProgressInViewer: true,
shouldShowEmbeddingPicker: false,
favoriteSchedulers: [],
globalContextMenuCloseTrigger: 0,
};
export const uiSlice = createSlice({
@ -96,6 +97,9 @@ export const uiSlice = createSlice({
toggleEmbeddingPicker: (state) => {
state.shouldShowEmbeddingPicker = !state.shouldShowEmbeddingPicker;
},
contextMenusClosed: (state) => {
state.globalContextMenuCloseTrigger += 1;
},
},
extraReducers(builder) {
builder.addCase(initialImageChanged, (state) => {
@ -122,6 +126,7 @@ export const {
setShouldShowProgressInViewer,
favoriteSchedulersChanged,
toggleEmbeddingPicker,
contextMenusClosed,
} = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -26,4 +26,5 @@ export interface UIState {
shouldShowProgressInViewer: boolean;
shouldShowEmbeddingPicker: boolean;
favoriteSchedulers: SchedulerParam[];
globalContextMenuCloseTrigger: number;
}

View File

@ -573,7 +573,7 @@ export type components = {
file: Blob;
};
/**
* Boolean Collection
* Boolean Primitive Collection
* @description A collection of boolean primitive values
*/
BooleanCollectionInvocation: {
@ -619,7 +619,7 @@ export type components = {
collection?: (boolean)[];
};
/**
* Boolean
* Boolean Primitive
* @description A boolean primitive value
*/
BooleanInvocation: {
@ -1002,7 +1002,7 @@ export type components = {
clip?: components["schemas"]["ClipField"];
};
/**
* Conditioning Collection
* Conditioning Primitive Collection
* @description A collection of conditioning tensor primitive values
*/
ConditioningCollectionInvocation: {
@ -1770,7 +1770,7 @@ export type components = {
field: string;
};
/**
* Float Collection
* Float Primitive Collection
* @description A collection of float primitive values
*/
FloatCollectionInvocation: {
@ -1816,7 +1816,7 @@ export type components = {
collection?: (number)[];
};
/**
* Float
* Float Primitive
* @description A float primitive value
*/
FloatInvocation: {
@ -2161,7 +2161,7 @@ export type components = {
channel?: "A" | "R" | "G" | "B";
};
/**
* Image Collection
* Image Primitive Collection
* @description A collection of image primitive values
*/
ImageCollectionInvocation: {
@ -3113,7 +3113,7 @@ export type components = {
seed?: number;
};
/**
* Integer Collection
* Integer Primitive Collection
* @description A collection of integer primitive values
*/
IntegerCollectionInvocation: {
@ -3159,7 +3159,7 @@ export type components = {
collection?: (number)[];
};
/**
* Integer
* Integer Primitive
* @description An integer primitive value
*/
IntegerInvocation: {
@ -3256,7 +3256,7 @@ export type components = {
item?: unknown;
};
/**
* Latents Collection
* Latents Primitive Collection
* @description A collection of latents tensor primitive values
*/
LatentsCollectionInvocation: {
@ -5786,7 +5786,7 @@ export type components = {
show_easing_plot?: boolean;
};
/**
* String Collection
* String Primitive Collection
* @description A collection of string primitive values
*/
StringCollectionInvocation: {
@ -5832,7 +5832,7 @@ export type components = {
collection?: (string)[];
};
/**
* String
* String Primitive
* @description A string primitive value
*/
StringInvocation: {
@ -6193,24 +6193,6 @@ export type components = {
ui_hidden: boolean;
ui_type?: components["schemas"]["UIType"];
};
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/**
* ControlNetModelFormat
* @description An enumeration.
* @enum {string}
*/
ControlNetModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusionXLModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusionOnnxModelFormat
* @description An enumeration.
@ -6223,6 +6205,24 @@ export type components = {
* @enum {string}
*/
StableDiffusion1ModelFormat: "checkpoint" | "diffusers";
/**
* ControlNetModelFormat
* @description An enumeration.
* @enum {string}
*/
ControlNetModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusion2ModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusion2ModelFormat: "checkpoint" | "diffusers";
/**
* StableDiffusionXLModelFormat
* @description An enumeration.
* @enum {string}
*/
StableDiffusionXLModelFormat: "checkpoint" | "diffusers";
};
responses: never;
parameters: never;