feat(ui): ux improvements & redesign

This is a squash merge of a bajillion messy small commits created while iterating on the UI component library and redesign.
This commit is contained in:
psychedelicious
2023-12-29 00:03:21 +11:00
committed by Kent Keirsey
parent a47d91f0e7
commit f0b102d830
889 changed files with 16645 additions and 15595 deletions

View File

@ -1,16 +1,18 @@
import 'reactflow/dist/style.css';
import { Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import { AnimatePresence, motion } from 'framer-motion';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md';
import 'reactflow/dist/style.css';
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
import { useTranslation } from 'react-i18next';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
const NodeEditor = () => {
const isReady = useAppSelector((state) => state.nodes.isReady);

View File

@ -1,15 +1,22 @@
import {
Flex,
Popover,
PopoverAnchor,
PopoverBody,
PopoverContent,
} from '@chakra-ui/react';
import 'reactflow/dist/style.css';
import { Flex } from '@chakra-ui/react';
import { useAppToaster } from 'app/components/Toaster';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import type { SelectInstance } from 'chakra-react-select';
import {
InvPopover,
InvPopoverAnchor,
InvPopoverBody,
InvPopoverContent,
} from 'common/components/InvPopover/wrapper';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type {
InvSelectOnChange,
InvSelectOption,
} from 'common/components/InvSelect/types';
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
import {
addNodePopoverClosed,
@ -17,43 +24,47 @@ import {
nodeAdded,
} from 'features/nodes/store/nodesSlice';
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
import { filter, map, some } from 'lodash-es';
import { filter, map, memoize, some } from 'lodash-es';
import type { KeyboardEventHandler } from 'react';
import { memo, useCallback, useRef } from 'react';
import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import { useTranslation } from 'react-i18next';
import 'reactflow/dist/style.css';
import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem';
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
type NodeTemplate = {
label: string;
value: string;
description: string;
tags: string[];
};
const createRegex = memoize(
(inputValue: string) =>
new RegExp(
inputValue
.trim()
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
.split(' ')
.join('.*'),
'gi'
)
);
const selectFilter = (value: string, item: NodeTemplate) => {
const regex = new RegExp(
value
.trim()
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
.split(' ')
.join('.*'),
'gi'
);
return (
regex.test(item.label) ||
regex.test(item.description) ||
item.tags.some((tag) => regex.test(tag))
);
};
const filterOption = memoize(
(option: FilterOptionOption<InvSelectOption>, inputValue: string) => {
if (!inputValue) {
return true;
}
const regex = createRegex(inputValue);
return (
regex.test(option.label) ||
regex.test(option.data.description ?? '') ||
(option.data.tags ?? []).some((tag) => regex.test(tag))
);
}
);
const AddNodePopover = () => {
const dispatch = useAppDispatch();
const buildInvocation = useBuildNode();
const toaster = useAppToaster();
const { t } = useTranslation();
const selectRef = useRef<SelectInstance<InvSelectOption> | null>(null);
const fieldFilter = useAppSelector(
(state) => state.nodes.connectionStartFieldType
);
@ -79,25 +90,28 @@ const AddNodePopover = () => {
})
: map(nodes.nodeTemplates);
const data: NodeTemplate[] = map(filteredNodeTemplates, (template) => {
return {
label: template.title,
value: template.type,
description: template.description,
tags: template.tags,
};
});
const options: InvSelectOption[] = map(
filteredNodeTemplates,
(template) => {
return {
label: template.title,
value: template.type,
description: template.description,
tags: template.tags,
};
}
);
//We only want these nodes if we're not filtered
if (fieldFilter === null) {
data.push({
options.push({
label: t('nodes.currentImage'),
value: 'current_image',
description: t('nodes.currentImageDescription'),
tags: ['progress'],
});
data.push({
options.push({
label: t('nodes.notes'),
value: 'notes',
description: t('nodes.notesDescription'),
@ -105,14 +119,13 @@ const AddNodePopover = () => {
});
}
data.sort((a, b) => a.label.localeCompare(b.label));
options.sort((a, b) => a.label.localeCompare(b.label));
return { data };
return { options };
});
const { data } = useAppSelector(selector);
const { options } = useAppSelector(selector);
const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen);
const inputRef = useRef<HTMLInputElement>(null);
const addNode = useCallback(
(nodeType: string) => {
@ -133,13 +146,12 @@ const AddNodePopover = () => {
[dispatch, buildInvocation, toaster, t]
);
const handleChange = useCallback(
(v: string | null) => {
const onChange = useCallback<InvSelectOnChange>(
(v) => {
if (!v) {
return;
}
addNode(v);
addNode(v.value);
},
[addNode]
);
@ -156,9 +168,9 @@ const AddNodePopover = () => {
(e) => {
e.preventDefault();
onOpen();
setTimeout(() => {
inputRef.current?.focus();
}, 0);
flushSync(() => {
selectRef.current?.inputRef?.focus();
});
},
[onOpen]
);
@ -169,10 +181,19 @@ const AddNodePopover = () => {
useHotkeys(['shift+a', 'space'], handleHotkeyOpen);
useHotkeys(['escape'], handleHotkeyClose);
const onKeyDown: KeyboardEventHandler = useCallback(
(e) => {
if (e.key === 'Escape') {
onClose();
}
},
[onClose]
);
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
return (
<Popover
initialFocusRef={inputRef}
<InvPopover
isOpen={isOpen}
onClose={onClose}
placement="bottom"
@ -181,51 +202,41 @@ const AddNodePopover = () => {
closeOnBlur={true}
returnFocusOnClose={true}
>
<PopoverAnchor>
<InvPopoverAnchor>
<Flex
sx={{
position: 'absolute',
top: '15%',
insetInlineStart: '50%',
pointerEvents: 'none',
}}
position="absolute"
top="15%"
insetInlineStart="50%"
pointerEvents="none"
/>
</PopoverAnchor>
<PopoverContent
sx={{
p: 0,
top: -1,
shadow: 'dark-lg',
borderColor: 'accent.300',
borderWidth: '2px',
borderStyle: 'solid',
_dark: { borderColor: 'accent.400' },
}}
</InvPopoverAnchor>
<InvPopoverContent
p={0}
top={-1}
shadow="dark-lg"
borderColor="blue.400"
borderWidth="2px"
borderStyle="solid"
>
<PopoverBody sx={{ p: 0 }}>
<IAIMantineSearchableSelect
inputRef={inputRef}
selectOnBlur={false}
placeholder={t('nodes.nodeSearch')}
<InvPopoverBody w="32rem" p={0}>
<InvSelect
menuIsOpen={isOpen}
selectRef={selectRef}
value={null}
data={data}
maxDropdownHeight={400}
nothingFound={t('nodes.noMatchingNodes')}
itemComponent={AddNodePopoverSelectItem}
filter={selectFilter}
onChange={handleChange}
hoverOnSearchChange={true}
onDropdownClose={onClose}
placeholder={t('nodes.nodeSearch')}
options={options}
noOptionsMessage={noOptionsMessage}
filterOption={filterOption}
onChange={onChange}
onMenuClose={onClose}
onKeyDown={onKeyDown}
sx={{
width: '32rem',
input: {
padding: '0.5rem',
},
width: 'full',
}}
/>
</PopoverBody>
</PopoverContent>
</Popover>
</InvPopoverBody>
</InvPopoverContent>
</InvPopover>
);
};

View File

@ -1,29 +0,0 @@
import { Text } from '@chakra-ui/react';
import { forwardRef } from 'react';
import 'reactflow/dist/style.css';
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
value: string;
label: string;
description: string;
}
export const AddNodePopoverSelectItem = forwardRef<HTMLDivElement, ItemProps>(
({ label, description, ...others }: ItemProps, ref) => {
return (
<div ref={ref} {...others}>
<div>
<Text fontWeight={600}>{label}</Text>
<Text
size="xs"
sx={{ color: 'base.600', _dark: { color: 'base.500' } }}
>
{description}
</Text>
</div>
</div>
);
}
);
AddNodePopoverSelectItem.displayName = 'AddNodePopoverSelectItem';

View File

@ -23,26 +23,27 @@ import {
} from 'features/nodes/store/nodesSlice';
import { $flow } from 'features/nodes/store/reactFlowInstance';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import { MouseEvent, useCallback, useRef } from 'react';
import type { MouseEvent } from 'react';
import { useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
Background,
import type {
OnConnect,
OnConnectEnd,
OnConnectStart,
OnEdgeUpdateFunc,
OnEdgesChange,
OnEdgesDelete,
OnEdgeUpdateFunc,
OnInit,
OnMoveEnd,
OnNodesChange,
OnNodesDelete,
OnSelectionChangeFunc,
ProOptions,
ReactFlow,
ReactFlowProps,
XYPosition,
} from 'reactflow';
import { Background, ReactFlow } from 'reactflow';
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
@ -272,6 +273,7 @@ export const Flow = () => {
onPaneClick={handlePaneClick}
deleteKeyCode={DELETE_KEYS}
selectionMode={selectionMode}
onlyRenderVisibleElements
>
<Background />
</ReactFlow>

View File

@ -4,7 +4,8 @@ import { useAppSelector } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { memo } from 'react';
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow';
import type { ConnectionLineComponentProps } from 'reactflow';
import { getBezierPath } from 'reactflow';
const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
const { shouldAnimateEdges, connectionStartFieldType, shouldColorEdges } =

View File

@ -2,12 +2,9 @@ import { Badge, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo, useMemo } from 'react';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from 'reactflow';
import type { EdgeProps } from 'reactflow';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow';
import { makeEdgeSelector } from './util/makeEdgeSelector';
const InvocationCollapsedEdge = ({

View File

@ -1,6 +1,8 @@
import { useAppSelector } from 'app/store/storeHooks';
import { memo, useMemo } from 'react';
import { BaseEdge, EdgeProps, getBezierPath } from 'reactflow';
import type { EdgeProps } from 'reactflow';
import { BaseEdge, getBezierPath } from 'reactflow';
import { makeEdgeSelector } from './util/makeEdgeSelector';
const InvocationDefaultEdge = ({

View File

@ -1,6 +1,6 @@
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { FIELD_COLORS } from 'features/nodes/types/constants';
import { FieldType } from 'features/nodes/types/field';
import type { FieldType } from 'features/nodes/types/field';
export const getFieldColor = (fieldType: FieldType | null): string => {
if (!fieldType) {

View File

@ -2,6 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getFieldColor } from './getEdgeColor';
export const makeEdgeSelector = (

View File

@ -1,16 +1,18 @@
import { Flex, Image, Text } from '@chakra-ui/react';
import { Flex, Image } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { InvText } from 'common/components/InvText/wrapper';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { motion } from 'framer-motion';
import { PropsWithChildren, memo, useCallback, useState } from 'react';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { NodeProps } from 'reactflow';
import type { NodeProps } from 'reactflow';
const selector = createMemoizedSelector(
stateSelector,
@ -95,16 +97,15 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
h: 8,
}}
>
<Text
<InvText
sx={{
fontSize: 'sm',
fontWeight: 600,
color: 'base.700',
_dark: { color: 'base.200' },
fontWeight: 'semibold',
color: 'base.200',
}}
>
{t('nodes.currentImage')}
</Text>
</InvText>
</Flex>
<Flex
layerStyle="nodeBody"

View File

@ -1,14 +1,15 @@
import { Flex, Grid, GridItem } from '@chakra-ui/react';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import { memo } from 'react';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import InvocationNodeFooter from './InvocationNodeFooter';
import InvocationNodeHeader from './InvocationNodeHeader';
import InputField from './fields/InputField';
import OutputField from './fields/OutputField';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import InvocationNodeFooter from './InvocationNodeFooter';
import InvocationNodeHeader from './InvocationNodeHeader';
type Props = {
nodeId: string;

View File

@ -1,9 +1,10 @@
import { Icon, Tooltip } from '@chakra-ui/react';
import { Icon } from '@chakra-ui/react';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { useNodeClassification } from 'features/nodes/hooks/useNodeClassification';
import type { Classification } from 'features/nodes/types/common';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaFlask } from 'react-icons/fa';
import { useNodeClassification } from 'features/nodes/hooks/useNodeClassification';
import { Classification } from 'features/nodes/types/common';
import { FaHammer } from 'react-icons/fa6';
interface Props {
@ -18,7 +19,7 @@ const InvocationNodeClassificationIcon = ({ nodeId }: Props) => {
}
return (
<Tooltip
<InvTooltip
label={<ClassificationTooltipContent classification={classification} />}
placement="top"
shouldWrapChildren
@ -31,7 +32,7 @@ const InvocationNodeClassificationIcon = ({ nodeId }: Props) => {
color: 'base.400',
}}
/>
</Tooltip>
</InvTooltip>
);
};

View File

@ -1,9 +1,9 @@
import { useColorModeValue } from '@chakra-ui/react';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { isInvocationNodeData } from 'features/nodes/types/invocation';
import { map } from 'lodash-es';
import { CSSProperties, memo, useMemo } from 'react';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { Handle, Position } from 'reactflow';
interface Props {
@ -12,8 +12,7 @@ interface Props {
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
const data = useNodeData(nodeId);
const { base400, base600 } = useChakraThemeTokens();
const backgroundColor = useColorModeValue(base400, base600);
const { base600 } = useChakraThemeTokens();
const dummyHandleStyles: CSSProperties = useMemo(
() => ({
@ -21,10 +20,10 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
borderRadius: '3px',
width: '1rem',
height: '1rem',
backgroundColor,
backgroundColor: base600,
zIndex: -1,
}),
[backgroundColor]
[base600]
);
if (!isInvocationNodeData(data)) {

View File

@ -3,6 +3,7 @@ import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import SaveToGalleryCheckbox from './SaveToGalleryCheckbox';
import UseCacheCheckbox from './UseCacheCheckbox';
@ -22,7 +23,7 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
borderBottomRadius: 'base',
px: 2,
py: 0,
h: 6,
h: 8,
justifyContent: 'space-between',
}}
>

View File

@ -1,11 +1,12 @@
import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon';
import { memo } from 'react';
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
import InvocationNodeInfoIcon from './InvocationNodeInfoIcon';
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon';
type Props = {
nodeId: string;
@ -26,9 +27,7 @@ const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
justifyContent: 'space-between',
h: 8,
textAlign: 'center',
fontWeight: 500,
color: 'base.700',
_dark: { color: 'base.200' },
color: 'base.200',
}}
>
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />

View File

@ -1,8 +1,10 @@
import { Flex, Icon, Text, Tooltip } from '@chakra-ui/react';
import { Flex, Icon } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { compare } from 'compare-versions';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { isInvocationNodeData } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -16,7 +18,7 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
const needsUpdate = useNodeNeedsUpdate(nodeId);
return (
<Tooltip
<InvTooltip
label={<TooltipContent nodeId={nodeId} />}
placement="top"
shouldWrapChildren
@ -30,7 +32,7 @@ const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
color: needsUpdate ? 'error.400' : 'base.400',
}}
/>
</Tooltip>
</InvTooltip>
);
};
@ -64,62 +66,66 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
if (!data.version) {
return (
<Text as="span" sx={{ color: 'error.500' }}>
<InvText as="span" sx={{ color: 'error.500' }}>
{t('nodes.versionUnknown')}
</Text>
</InvText>
);
}
if (!nodeTemplate.version) {
return (
<Text as="span" sx={{ color: 'error.500' }}>
<InvText as="span" sx={{ color: 'error.500' }}>
{t('nodes.version')} {data.version} ({t('nodes.unknownTemplate')})
</Text>
</InvText>
);
}
if (compare(data.version, nodeTemplate.version, '<')) {
return (
<Text as="span" sx={{ color: 'error.500' }}>
<InvText as="span" sx={{ color: 'error.500' }}>
{t('nodes.version')} {data.version} ({t('nodes.updateNode')})
</Text>
</InvText>
);
}
if (compare(data.version, nodeTemplate.version, '>')) {
return (
<Text as="span" sx={{ color: 'error.500' }}>
<InvText as="span" sx={{ color: 'error.500' }}>
{t('nodes.version')} {data.version} ({t('nodes.updateApp')})
</Text>
</InvText>
);
}
return (
<Text as="span">
<InvText as="span">
{t('nodes.version')} {data.version}
</Text>
</InvText>
);
}, [data, nodeTemplate, t]);
if (!isInvocationNodeData(data)) {
return <Text sx={{ fontWeight: 600 }}>{t('nodes.unknownNode')}</Text>;
return (
<InvText sx={{ fontWeight: 'semibold' }}>
{t('nodes.unknownNode')}
</InvText>
);
}
return (
<Flex sx={{ flexDir: 'column' }}>
<Text as="span" sx={{ fontWeight: 600 }}>
<InvText as="span" sx={{ fontWeight: 'semibold' }}>
{title}
</Text>
</InvText>
{nodeTemplate?.nodePack && (
<Text opacity={0.7}>
<InvText opacity={0.7}>
{t('nodes.nodePack')}: {nodeTemplate.nodePack}
</Text>
</InvText>
)}
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
<InvText sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
{nodeTemplate?.description}
</Text>
</InvText>
{versionComponent}
{data?.notes && <Text>{data.notes}</Text>}
{data?.notes && <InvText>{data.notes}</InvText>}
</Flex>
);
});

View File

@ -1,20 +1,12 @@
import {
Badge,
CircularProgress,
Flex,
Icon,
Image,
Text,
Tooltip,
} from '@chakra-ui/react';
import { Badge, CircularProgress, Flex, Icon, Image } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { InvText } from 'common/components/InvText/wrapper';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import {
NodeExecutionState,
zNodeStatus,
} from 'features/nodes/types/invocation';
import type { NodeExecutionState } from 'features/nodes/types/invocation';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa';
@ -49,7 +41,7 @@ const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
}
return (
<Tooltip
<InvTooltip
label={<TooltipLabel nodeExecutionState={nodeExecutionState} />}
placement="top"
>
@ -64,7 +56,7 @@ const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
>
<StatusIcon nodeExecutionState={nodeExecutionState} />
</Flex>
</Tooltip>
</InvTooltip>
);
};
@ -78,7 +70,7 @@ const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
const { status, progress, progressImage } = nodeExecutionState;
const { t } = useTranslation();
if (status === zNodeStatus.enum.PENDING) {
return <Text>{t('queue.pending')}</Text>;
return <InvText>{t('queue.pending')}</InvText>;
}
if (status === zNodeStatus.enum.IN_PROGRESS) {
if (progressImage) {
@ -102,21 +94,21 @@ const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
if (progress !== null) {
return (
<Text>
<InvText>
{t('nodes.executionStateInProgress')} ({Math.round(progress * 100)}%)
</Text>
</InvText>
);
}
return <Text>{t('nodes.executionStateInProgress')}</Text>;
return <InvText>{t('nodes.executionStateInProgress')}</InvText>;
}
if (status === zNodeStatus.enum.COMPLETED) {
return <Text>{t('nodes.executionStateCompleted')}</Text>;
return <InvText>{t('nodes.executionStateCompleted')}</InvText>;
}
if (status === zNodeStatus.enum.FAILED) {
return <Text>{t('nodes.executionStateError')}</Text>;
return <InvText>{t('nodes.executionStateError')}</InvText>;
}
return null;
@ -136,8 +128,7 @@ const StatusIcon = memo((props: StatusIconProps) => {
as={FaEllipsisH}
sx={{
boxSize: iconBoxSize,
color: 'base.600',
_dark: { color: 'base.300' },
color: 'base.300',
}}
/>
);
@ -167,8 +158,7 @@ const StatusIcon = memo((props: StatusIconProps) => {
as={FaCheck}
sx={{
boxSize: iconBoxSize,
color: 'ok.600',
_dark: { color: 'ok.300' },
color: 'ok.300',
}}
/>
);
@ -179,8 +169,7 @@ const StatusIcon = memo((props: StatusIconProps) => {
as={FaExclamation}
sx={{
boxSize: iconBoxSize,
color: 'error.600',
_dark: { color: 'error.300' },
color: 'error.300',
}}
/>
);

View File

@ -1,10 +1,11 @@
import { Flex, Text } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { useNodePack } from 'features/nodes/hooks/useNodePack';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
type Props = {
nodeId: string;
@ -33,22 +34,21 @@ const InvocationNodeUnknownFallback = ({
borderBottomRadius: isOpen ? 0 : 'base',
alignItems: 'center',
h: 8,
fontWeight: 600,
fontWeight: 'semibold',
fontSize: 'sm',
}}
>
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<Text
<InvText
sx={{
w: 'full',
textAlign: 'center',
pe: 8,
color: 'error.500',
_dark: { color: 'error.300' },
color: 'error.300',
}}
>
{label ? `${label} (${type})` : type}
</Text>
</InvText>
</Flex>
{isOpen && (
<Flex
@ -65,19 +65,19 @@ const InvocationNodeUnknownFallback = ({
}}
>
<Flex gap={2} flexDir="column">
<Text as="span">
<InvText as="span">
{t('nodes.unknownNodeType')}:{' '}
<Text as="span" fontWeight={600}>
<InvText as="span" fontWeight="semibold">
{type}
</Text>
</Text>
</InvText>
</InvText>
{nodePack && (
<Text as="span">
<InvText as="span">
{t('nodes.nodePack')}:{' '}
<Text as="span" fontWeight={600}>
<InvText as="span" fontWeight="semibold">
{nodePack}
</Text>
</Text>
</InvText>
</InvText>
)}
</Flex>
</Flex>

View File

@ -2,9 +2,10 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode';
import { InvocationNodeData } from 'features/nodes/types/invocation';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
import { memo, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import type { NodeProps } from 'reactflow';
import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback';
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {

View File

@ -1,10 +1,11 @@
import { FormControl, FormLabel } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvTextarea } from 'common/components/InvTextarea/InvTextarea';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNodeData } from 'features/nodes/types/invocation';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
@ -21,14 +22,14 @@ const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
return null;
}
return (
<FormControl>
<FormLabel>{t('nodes.notes')}</FormLabel>
<IAITextarea
<InvControl label={t('nodes.notes')} orientation="vertical" h="full">
<InvTextarea
value={data?.notes}
onChange={handleNotesChanged}
rows={10}
resize="none"
/>
</FormControl>
</InvControl>
);
};

View File

@ -1,9 +1,11 @@
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { InvCheckbox } from 'common/components/InvCheckbox/wrapper';
import { InvControl } from 'common/components/InvControl/InvControl';
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
@ -28,17 +30,9 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
}
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
{t('hotkeys.saveToGallery.title')}
</FormLabel>
<Checkbox
className="nopan"
size="sm"
onChange={handleChange}
isChecked={!isIntermediate}
/>
</FormControl>
<InvControl label={t('hotkeys.saveToGallery.title')} className="nopan">
<InvCheckbox onChange={handleChange} isChecked={!isIntermediate} />
</InvControl>
);
};

View File

@ -1,8 +1,10 @@
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { InvCheckbox } from 'common/components/InvCheckbox/wrapper';
import { InvControl } from 'common/components/InvControl/InvControl';
import { useUseCache } from 'features/nodes/hooks/useUseCache';
import { nodeUseCacheChanged } from 'features/nodes/store/nodesSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
@ -21,17 +23,13 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
);
const { t } = useTranslation();
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
{t('invocationCache.useCache')}
</FormLabel>
<Checkbox
<InvControl label={t('invocationCache.useCache')}>
<InvCheckbox
className="nopan"
size="sm"
onChange={handleChange}
isChecked={useCache}
/>
</FormControl>
</InvControl>
);
};

View File

@ -3,19 +3,21 @@ import {
EditableInput,
EditablePreview,
Flex,
Tooltip,
forwardRef,
useEditableControls,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
import FieldTooltipContent from './FieldTooltipContent';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { MouseEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FieldTooltipContent from './FieldTooltipContent';
interface Props {
nodeId: string;
fieldName: string;
@ -62,7 +64,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
}, [label, fieldTemplateTitle, t]);
return (
<Tooltip
<InvTooltip
label={
withTooltip ? (
<FieldTooltipContent
@ -73,8 +75,6 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
) : undefined
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
hasArrow
>
<Flex
ref={ref}
@ -101,10 +101,10 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
<EditablePreview
sx={{
p: 0,
fontWeight: isMissingInput ? 600 : 400,
fontWeight: isMissingInput ? 'bold' : 'normal',
textAlign: 'left',
_hover: {
fontWeight: '600 !important',
fontWeight: 'semibold !important',
},
}}
noOfLines={1}
@ -114,11 +114,8 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
sx={{
p: 0,
w: 'full',
fontWeight: 600,
color: 'base.900',
_dark: {
color: 'base.100',
},
fontWeight: 'semibold',
color: 'base.100',
_focusVisible: {
p: 0,
textAlign: 'left',
@ -129,7 +126,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
<EditableControls />
</Editable>
</Flex>
</Tooltip>
</InvTooltip>
);
});

View File

@ -1,11 +1,11 @@
import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
IAIContextMenu,
IAIContextMenuProps,
} from 'common/components/IAIContextMenu';
import type { InvContextMenuProps } from 'common/components/InvContextMenu/InvContextMenu';
import { InvContextMenu } from 'common/components/InvContextMenu/InvContextMenu';
import { InvMenuItem } from 'common/components/InvMenu/InvMenuItem';
import { InvMenuList } from 'common/components/InvMenu/InvMenuList';
import { InvMenuGroup } from 'common/components/InvMenu/wrapper';
import { useFieldInputKind } from 'features/nodes/hooks/useFieldInputKind';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
@ -13,16 +13,16 @@ import {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
} from 'features/nodes/store/workflowSlice';
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
import type { MouseEvent, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaMinus, FaPlus } from 'react-icons/fa';
import { menuListMotionProps } from 'theme/components/menu';
type Props = {
nodeId: string;
fieldName: string;
kind: 'input' | 'output';
children: IAIContextMenuProps<HTMLDivElement>['children'];
children: InvContextMenuProps<HTMLDivElement>['children'];
};
const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
@ -69,24 +69,24 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
const menuItems: ReactNode[] = [];
if (mayExpose && !isExposed) {
menuItems.push(
<MenuItem
<InvMenuItem
key={`${nodeId}.${fieldName}.expose-field`}
icon={<FaPlus />}
onClick={handleExposeField}
>
{t('nodes.addLinearView')}
</MenuItem>
</InvMenuItem>
);
}
if (mayExpose && isExposed) {
menuItems.push(
<MenuItem
<InvMenuItem
key={`${nodeId}.${fieldName}.unexpose-field`}
icon={<FaMinus />}
onClick={handleUnexposeField}
>
{t('nodes.removeLinearView')}
</MenuItem>
</InvMenuItem>
);
}
return menuItems;
@ -103,35 +103,22 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
const renderMenuFunc = useCallback(
() =>
!menuItems.length ? null : (
<MenuList
<InvMenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup
<InvMenuGroup
title={label || fieldTemplateTitle || t('nodes.unknownField')}
>
{menuItems}
</MenuGroup>
</MenuList>
</InvMenuGroup>
</InvMenuList>
),
[fieldTemplateTitle, label, menuItems, skipEvent, t]
);
return (
<IAIContextMenu<HTMLDivElement>
menuProps={{
size: 'sm',
isLazy: true,
}}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={renderMenuFunc}
>
{children}
</IAIContextMenu>
<InvContextMenu renderMenu={renderMenuFunc}>{children}</InvContextMenu>
);
};

View File

@ -1,17 +1,19 @@
import { Tooltip } from '@chakra-ui/react';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import {
HANDLE_TOOLTIP_OPEN_DELAY,
MODEL_TYPES,
} from 'features/nodes/types/constants';
import {
import type {
FieldInputTemplate,
FieldOutputTemplate,
} from 'features/nodes/types/field';
import { CSSProperties, memo, useMemo } from 'react';
import { Handle, HandleType, Position } from 'reactflow';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import type { HandleType } from 'reactflow';
import { Handle, Position } from 'reactflow';
export const handleBaseStyles: CSSProperties = {
position: 'absolute',
@ -104,10 +106,9 @@ const FieldHandle = (props: FieldHandleProps) => {
}, [connectionError, fieldTypeName, isConnectionInProgress]);
return (
<Tooltip
<InvTooltip
label={tooltip}
placement={handleType === 'target' ? 'start' : 'end'}
hasArrow
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
<Handle
@ -116,7 +117,7 @@ const FieldHandle = (props: FieldHandleProps) => {
position={handleType === 'target' ? Position.Left : Position.Right}
style={styles}
/>
</Tooltip>
</InvTooltip>
);
};

View File

@ -1,4 +1,5 @@
import { Flex, Text, forwardRef } from '@chakra-ui/react';
import { Flex, forwardRef } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { memo } from 'react';
@ -28,9 +29,9 @@ const FieldTitle = forwardRef((props: Props, ref) => {
w: 'full',
}}
>
<Text sx={{ fontWeight: isMissingInput ? 600 : 400 }}>
<InvText sx={{ fontWeight: isMissingInput ? 'bold' : 'normal' }}>
{label || fieldTemplateTitle}
</Text>
</InvText>
</Flex>
);
});

View File

@ -1,4 +1,5 @@
import { Flex, Text } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper';
import { useFieldInstance } from 'features/nodes/hooks/useFieldData';
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
@ -43,21 +44,21 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
return (
<Flex sx={{ flexDir: 'column' }}>
<Text sx={{ fontWeight: 600 }}>{fieldTitle}</Text>
<InvText sx={{ fontWeight: 'semibold' }}>{fieldTitle}</InvText>
{fieldTemplate && (
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
<InvText sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
{fieldTemplate.description}
</Text>
</InvText>
)}
{fieldTypeName && (
<Text>
<InvText>
{t('parameters.type')}: {fieldTypeName}
</Text>
</InvText>
)}
{isInputTemplate && (
<Text>
<InvText>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
</InvText>
)}
</Flex>
);

View File

@ -1,10 +1,14 @@
import { Box, Flex, FormControl, FormLabel } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvLabel } from 'common/components/InvControl/InvLabel';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { PropsWithChildren, memo, useMemo } from 'react';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import EditableFieldTitle from './EditableFieldTitle';
import FieldContextMenu from './FieldContextMenu';
import FieldHandle from './FieldHandle';
@ -50,78 +54,67 @@ const InputField = ({ nodeId, fieldName }: Props) => {
if (!fieldTemplate || !fieldInstance) {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
sx={{
alignItems: 'stretch',
justifyContent: 'space-between',
gap: 2,
h: 'full',
w: 'full',
}}
<InvControl
alignItems="stretch"
justifyContent="space-between"
flexDir="column"
gap={2}
h="full"
w="full"
>
<FormLabel
sx={{
display: 'flex',
alignItems: 'center',
mb: 0,
px: 1,
gap: 2,
h: 'full',
fontWeight: 600,
color: 'error.400',
_dark: { color: 'error.300' },
}}
<InvLabel
display="flex"
alignItems="center"
mb={0}
px={1}
gap={2}
h="full"
>
{t('nodes.unknownInput', {
name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
})}
</FormLabel>
</FormControl>
</InvLabel>
</InvControl>
</InputFieldWrapper>
);
}
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
isInvalid={isMissingInput}
isDisabled={isConnected}
sx={{
alignItems: 'stretch',
justifyContent: 'space-between',
ps: fieldTemplate.input === 'direct' ? 0 : 2,
gap: 2,
h: 'full',
w: 'full',
}}
>
<FieldContextMenu nodeId={nodeId} fieldName={fieldName} kind="input">
{(ref) => (
<FormLabel
sx={{
display: 'flex',
alignItems: 'center',
mb: 0,
px: 1,
gap: 2,
h: 'full',
}}
<FieldContextMenu nodeId={nodeId} fieldName={fieldName} kind="input">
{(ref) => (
<InvControl
isInvalid={isMissingInput}
isDisabled={isConnected}
ps={fieldTemplate.input === 'direct' ? 0 : 2}
alignItems="stretch"
justifyContent="space-between"
flexDir="column"
gap={2}
h="full"
w="full"
>
<InvLabel
ref={ref}
display="flex"
alignItems="center"
mb={0}
px={1}
gap={2}
h="full"
>
<EditableFieldTitle
ref={ref}
nodeId={nodeId}
fieldName={fieldName}
kind="input"
isMissingInput={isMissingInput}
withTooltip
/>
</FormLabel>
)}
</FieldContextMenu>
<Box>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Box>
</FormControl>
</InvLabel>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</InvControl>
)}
</FieldContextMenu>
{fieldTemplate.input !== 'direct' && (
<FieldHandle
@ -146,17 +139,15 @@ const InputFieldWrapper = memo(
({ shouldDim, children }: InputFieldWrapperProps) => {
return (
<Flex
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
w: 'full',
h: 'full',
}}
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>

View File

@ -1,4 +1,5 @@
import { Box, Text } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { InvText } from 'common/components/InvText/wrapper';
import { useFieldInstance } from 'features/nodes/hooks/useFieldData';
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
import {
@ -14,22 +15,22 @@ import {
isEnumFieldInputTemplate,
isFloatFieldInputInstance,
isFloatFieldInputTemplate,
isIPAdapterModelFieldInputInstance,
isIPAdapterModelFieldInputTemplate,
isImageFieldInputInstance,
isImageFieldInputTemplate,
isIntegerFieldInputInstance,
isIntegerFieldInputTemplate,
isIPAdapterModelFieldInputInstance,
isIPAdapterModelFieldInputTemplate,
isLoRAModelFieldInputInstance,
isLoRAModelFieldInputTemplate,
isMainModelFieldInputInstance,
isMainModelFieldInputTemplate,
isSchedulerFieldInputInstance,
isSchedulerFieldInputTemplate,
isSDXLMainModelFieldInputInstance,
isSDXLMainModelFieldInputTemplate,
isSDXLRefinerModelFieldInputInstance,
isSDXLRefinerModelFieldInputTemplate,
isSchedulerFieldInputInstance,
isSchedulerFieldInputTemplate,
isStringFieldInputInstance,
isStringFieldInputTemplate,
isT2IAdapterModelFieldInputInstance,
@ -39,19 +40,20 @@ import {
} from 'features/nodes/types/field';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
import ColorFieldInputComponent from './inputs/ColorFieldInputComponent';
import ControlNetModelFieldInputComponent from './inputs/ControlNetModelFieldInputComponent';
import EnumFieldInputComponent from './inputs/EnumFieldInputComponent';
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
import LoRAModelFieldInputComponent from './inputs/LoRAModelFieldInputComponent';
import MainModelFieldInputComponent from './inputs/MainModelFieldInputComponent';
import NumberFieldInputComponent from './inputs/NumberFieldInputComponent';
import RefinerModelFieldInputComponent from './inputs/RefinerModelFieldInputComponent';
import SDXLMainModelFieldInputComponent from './inputs/SDXLMainModelFieldInputComponent';
import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent';
import SDXLMainModelFieldInputComponent from './inputs/SDXLMainModelFieldInputComponent';
import StringFieldInputComponent from './inputs/StringFieldInputComponent';
import T2IAdapterModelFieldInputComponent from './inputs/T2IAdapterModelFieldInputComponent';
import VAEModelFieldInputComponent from './inputs/VAEModelFieldInputComponent';
@ -290,16 +292,15 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
return (
<Box p={1}>
<Text
<InvText
sx={{
fontSize: 'sm',
fontWeight: 600,
color: 'error.400',
_dark: { color: 'error.300' },
fontWeight: 'semibold',
color: 'error.300',
}}
>
{t('nodes.unknownFieldType', { type: fieldInstance?.type.name })}
</Text>
</InvText>
</Box>
);
};

View File

@ -1,23 +1,18 @@
import {
Flex,
FormControl,
FormLabel,
Icon,
Spacer,
Tooltip,
} from '@chakra-ui/react';
import { Flex, Icon, Spacer } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaInfoCircle, FaTrash } from 'react-icons/fa';
import EditableFieldTitle from './EditableFieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
import { useTranslation } from 'react-i18next';
type Props = {
nodeId: string;
@ -38,54 +33,44 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
onMouseEnter={handleMouseOver}
onMouseLeave={handleMouseOut}
layerStyle="second"
sx={{
position: 'relative',
borderRadius: 'base',
w: 'full',
p: 2,
}}
position="relative"
borderRadius="base"
w="full"
p={4}
flexDir="column"
>
<FormControl as={Flex} sx={{ flexDir: 'column', gap: 1, flexShrink: 1 }}>
<FormLabel
sx={{
display: 'flex',
alignItems: 'center',
mb: 0,
}}
<Flex>
<EditableFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
<Spacer />
<InvTooltip
label={
<FieldTooltipContent
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<EditableFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
<Spacer />
<Tooltip
label={
<FieldTooltipContent
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
hasArrow
>
<Flex h="full" alignItems="center">
<Icon as={FaInfoCircle} />
</Flex>
</Tooltip>
<IAIIconButton
aria-label={t('nodes.removeLinearView')}
tooltip={t('nodes.removeLinearView')}
variant="ghost"
size="sm"
onClick={handleRemoveField}
icon={<FaTrash />}
/>
</FormLabel>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</FormControl>
<Flex h="full" alignItems="center">
<Icon as={FaInfoCircle} />
</Flex>
</InvTooltip>
<InvIconButton
aria-label={t('nodes.removeLinearView')}
tooltip={t('nodes.removeLinearView')}
variant="ghost"
size="sm"
onClick={handleRemoveField}
icon={<FaTrash />}
/>
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
</Flex>
);

View File

@ -1,10 +1,15 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvLabel } from 'common/components/InvControl/InvLabel';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldOutputInstance } from 'features/nodes/hooks/useFieldOutputInstance';
import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { PropsWithChildren, memo } from 'react';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import FieldHandle from './FieldHandle';
import FieldTooltipContent from './FieldTooltipContent';
@ -29,40 +34,34 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
if (!fieldTemplate || !fieldInstance) {
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<FormControl
sx={{
alignItems: 'stretch',
justifyContent: 'space-between',
gap: 2,
h: 'full',
w: 'full',
}}
<InvControl
alignItems="stretch"
justifyContent="space-between"
gap={2}
h="full"
w="full"
>
<FormLabel
sx={{
display: 'flex',
alignItems: 'center',
mb: 0,
px: 1,
gap: 2,
h: 'full',
fontWeight: 600,
color: 'error.400',
_dark: { color: 'error.300' },
}}
<InvLabel
display="flex"
alignItems="center"
h="full"
color="error.300"
mb={0}
px={1}
gap={2}
>
{t('nodes.unknownOutput', {
name: fieldTemplate?.title ?? fieldName,
})}
</FormLabel>
</FormControl>
</InvLabel>
</InvControl>
</OutputFieldWrapper>
);
}
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<Tooltip
<InvTooltip
label={
<FieldTooltipContent
nodeId={nodeId}
@ -73,14 +72,11 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
hasArrow
>
<FormControl isDisabled={isConnected} pe={2}>
<FormLabel sx={{ mb: 0, fontWeight: 500 }}>
{fieldTemplate?.title}
</FormLabel>
</FormControl>
</Tooltip>
<InvControl isDisabled={isConnected} pe={2}>
<InvLabel mb={0}>{fieldTemplate?.title}</InvLabel>
</InvControl>
</InvTooltip>
<FieldHandle
fieldTemplate={fieldTemplate}
handleType="source"

View File

@ -1,63 +1,80 @@
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type {
InvSelectOnChange,
InvSelectOption,
} from 'common/components/InvSelect/types';
import { fieldBoardValueChanged } from 'features/nodes/store/nodesSlice';
import {
BoardFieldInputTemplate,
import type {
BoardFieldInputInstance,
BoardFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import type { FieldComponentProps } from './types';
const BoardFieldInputComponent = (
props: FieldComponentProps<BoardFieldInputInstance, BoardFieldInputTemplate>
) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { data, hasBoards } = useListAllBoardsQuery(undefined, {
const { t } = useTranslation();
const { options, hasBoards } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
const boards: SelectItem[] = [
const options: InvSelectOption[] = [
{
label: 'None',
value: 'none',
},
];
data?.forEach(({ board_id, board_name }) => {
boards.push({
].concat(
(data ?? []).map(({ board_id, board_name }) => ({
label: board_name,
value: board_id,
});
});
}))
);
return {
data: boards,
hasBoards: boards.length > 1,
options,
hasBoards: options.length > 1,
};
},
});
const handleChange = useCallback(
(v: string | null) => {
const onChange = useCallback<InvSelectOnChange>(
(v) => {
if (!v) {
return;
}
dispatch(
fieldBoardValueChanged({
nodeId,
fieldName: field.name,
value: v && v !== 'none' ? { board_id: v } : undefined,
value: v.value !== 'none' ? { board_id: v.value } : undefined,
})
);
},
[dispatch, field.name, nodeId]
);
const value = useMemo(
() => options.find((o) => o.value === field.value?.board_id),
[options, field.value]
);
const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]);
return (
<IAIMantineSearchableSelect
className="nowheel nodrag"
value={field.value?.board_id ?? 'none'}
data={data}
onChange={handleChange}
disabled={!hasBoards}
/>
<InvControl className="nowheel nodrag" isDisabled={!hasBoards}>
<InvSelect
value={value}
options={options}
onChange={onChange}
placeholder={t('boards.selectBoard')}
noOptionsMessage={noOptionsMessage}
/>
</InvControl>
);
};

View File

@ -1,12 +1,14 @@
import { Switch } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
BooleanFieldInputInstance,
BooleanFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { FieldComponentProps } from './types';
const BooleanFieldInputComponent = (
props: FieldComponentProps<

View File

@ -1,12 +1,14 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldColorValueChanged } from 'features/nodes/store/nodesSlice';
import {
ColorFieldInputTemplate,
import type {
ColorFieldInputInstance,
ColorFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { memo, useCallback } from 'react';
import { RgbaColor, RgbaColorPicker } from 'react-colorful';
import type { RgbaColor } from 'react-colorful';
import { RgbaColorPicker } from 'react-colorful';
import type { FieldComponentProps } from './types';
const ColorFieldInputComponent = (
props: FieldComponentProps<ColorFieldInputInstance, ColorFieldInputTemplate>

View File

@ -1,99 +1,67 @@
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { fieldControlNetModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
ControlNetModelFieldInputTemplate,
import type {
ControlNetModelFieldInputInstance,
ControlNetModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToControlNetModelParam } from 'features/parameters/util/modelIdToControlNetModelParam';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import type { ControlNetModelConfigEntity } from 'services/api/endpoints/models';
import { useGetControlNetModelsQuery } from 'services/api/endpoints/models';
const ControlNetModelFieldInputComponent = (
props: FieldComponentProps<
ControlNetModelFieldInputInstance,
ControlNetModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
ControlNetModelFieldInputInstance,
ControlNetModelFieldInputTemplate
>;
const ControlNetModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const controlNetModel = field.value;
const dispatch = useAppDispatch();
const { data, isLoading } = useGetControlNetModelsQuery();
const { data: controlNetModels } = useGetControlNetModelsQuery();
// grab the full model entity from the RTK Query cache
const selectedModel = useMemo(
() =>
controlNetModels?.entities[
`${controlNetModel?.base_model}/controlnet/${controlNetModel?.model_name}`
] ?? null,
[
controlNetModel?.base_model,
controlNetModel?.model_name,
controlNetModels?.entities,
]
);
const data = useMemo(() => {
if (!controlNetModels) {
return [];
}
const data: SelectItem[] = [];
forEach(controlNetModels.entities, (model, id) => {
if (!model) {
const _onChange = useCallback(
(value: ControlNetModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
return data;
}, [controlNetModels]);
const handleValueChanged = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newControlNetModel = modelIdToControlNetModelParam(v);
if (!newControlNetModel) {
return;
}
dispatch(
fieldControlNetModelValueChanged({
nodeId,
fieldName: field.name,
value: newControlNetModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
selectedModel: field.value
? { ...field.value, model_type: 'controlnet' }
: undefined,
isLoading,
});
return (
<IAIMantineSelect
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id ?? null}
placeholder="Pick one"
error={!selectedModel}
data={data}
onChange={handleValueChanged}
sx={{ width: '100%' }}
/>
<InvTooltip label={value?.description}>
<InvControl className="nowheel nodrag" isInvalid={!value}>
<InvSelect
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</InvControl>
</InvTooltip>
);
};

View File

@ -1,12 +1,14 @@
import { Select } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldEnumModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
EnumFieldInputInstance,
EnumFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { FieldComponentProps } from './types';
const EnumFieldInputComponent = (
props: FieldComponentProps<EnumFieldInputInstance, EnumFieldInputTemplate>

View File

@ -1,18 +1,19 @@
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { fieldIPAdapterModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
IPAdapterModelFieldInputTemplate,
import type {
IPAdapterModelFieldInputInstance,
IPAdapterModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToIPAdapterModelParam } from 'features/parameters/util/modelIdToIPAdapterModelParams';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import type { IPAdapterModelConfigEntity } from 'services/api/endpoints/models';
import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models';
import type { FieldComponentProps } from './types';
const IPAdapterModelFieldInputComponent = (
props: FieldComponentProps<
IPAdapterModelFieldInputInstance,
@ -20,80 +21,45 @@ const IPAdapterModelFieldInputComponent = (
>
) => {
const { nodeId, field } = props;
const ipAdapterModel = field.value;
const dispatch = useAppDispatch();
const { data: ipAdapterModels } = useGetIPAdapterModelsQuery();
// grab the full model entity from the RTK Query cache
const selectedModel = useMemo(
() =>
ipAdapterModels?.entities[
`${ipAdapterModel?.base_model}/ip_adapter/${ipAdapterModel?.model_name}`
] ?? null,
[
ipAdapterModel?.base_model,
ipAdapterModel?.model_name,
ipAdapterModels?.entities,
]
);
const data = useMemo(() => {
if (!ipAdapterModels) {
return [];
}
const data: SelectItem[] = [];
forEach(ipAdapterModels.entities, (model, id) => {
if (!model) {
const _onChange = useCallback(
(value: IPAdapterModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
return data;
}, [ipAdapterModels]);
const handleValueChanged = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newIPAdapterModel = modelIdToIPAdapterModelParam(v);
if (!newIPAdapterModel) {
return;
}
dispatch(
fieldIPAdapterModelValueChanged({
nodeId,
fieldName: field.name,
value: newIPAdapterModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange } = useGroupedModelInvSelect({
modelEntities: ipAdapterModels,
onChange: _onChange,
selectedModel: field.value
? { ...field.value, model_type: 'ip_adapter' }
: undefined,
});
return (
<IAIMantineSelect
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id ?? null}
placeholder="Pick one"
error={!selectedModel}
data={data}
onChange={handleValueChanged}
sx={{ width: '100%' }}
/>
<InvTooltip label={value?.description}>
<InvControl className="nowheel nodrag" isInvalid={!value}>
<InvSelect
value={value}
placeholder="Pick one"
options={options}
onChange={onChange}
sx={{ width: '100%' }}
/>
</InvControl>
</InvTooltip>
);
};

View File

@ -1,23 +1,25 @@
import { Flex, Text } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import {
import { InvText } from 'common/components/InvText/wrapper';
import type {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
ImageFieldInputInstance,
ImageFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUndo } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/types';
import type { PostUploadAction } from 'services/api/types';
import type { FieldComponentProps } from './types';
const ImageFieldInputComponent = (
props: FieldComponentProps<ImageFieldInputInstance, ImageFieldInputTemplate>
@ -108,9 +110,9 @@ export default memo(ImageFieldInputComponent);
const UploadElement = memo(() => {
const { t } = useTranslation();
return (
<Text fontSize={16} fontWeight={600}>
<InvText fontSize={16} fontWeight="semibold">
{t('gallery.dropOrUpload')}
</Text>
</InvText>
);
});
@ -119,9 +121,9 @@ UploadElement.displayName = 'UploadElement';
const DropLabel = memo(() => {
const { t } = useTranslation();
return (
<Text fontSize={16} fontWeight={600}>
<InvText fontSize={16} fontWeight="semibold">
{t('gallery.drop')}
</Text>
</InvText>
);
});

View File

@ -1,123 +1,67 @@
import { Flex, Text } from '@chakra-ui/react';
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { fieldLoRAModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
LoRAModelFieldInputTemplate,
import type {
LoRAModelFieldInputInstance,
LoRAModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToLoRAModelParam } from 'features/parameters/util/modelIdToLoRAModelParam';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import type { LoRAModelConfigEntity } from 'services/api/endpoints/models';
import { useGetLoRAModelsQuery } from 'services/api/endpoints/models';
import { useTranslation } from 'react-i18next';
const LoRAModelFieldInputComponent = (
props: FieldComponentProps<
LoRAModelFieldInputInstance,
LoRAModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
LoRAModelFieldInputInstance,
LoRAModelFieldInputTemplate
>;
const LoRAModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const lora = field.value;
const dispatch = useAppDispatch();
const { data: loraModels } = useGetLoRAModelsQuery();
const { t } = useTranslation();
const data = useMemo(() => {
if (!loraModels) {
return [];
}
const data: SelectItem[] = [];
forEach(loraModels.entities, (lora, id) => {
if (!lora) {
const { data, isLoading } = useGetLoRAModelsQuery();
const _onChange = useCallback(
(value: LoRAModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: lora.model_name,
group: MODEL_TYPE_MAP[lora.base_model],
});
});
return data.sort((a, b) => (a.disabled && !b.disabled ? 1 : -1));
}, [loraModels]);
const selectedLoRAModel = useMemo(
() =>
loraModels?.entities[`${lora?.base_model}/lora/${lora?.model_name}`] ??
null,
[loraModels?.entities, lora?.base_model, lora?.model_name]
);
const handleChange = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newLoRAModel = modelIdToLoRAModelParam(v);
if (!newLoRAModel) {
return;
}
dispatch(
fieldLoRAModelValueChanged({
nodeId,
fieldName: field.name,
value: newLoRAModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const filterFunc = useCallback(
(value: string, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()),
[]
);
if (loraModels?.ids.length === 0) {
return (
<Flex sx={{ justifyContent: 'center', p: 2 }}>
<Text sx={{ fontSize: 'sm', color: 'base.500', _dark: 'base.700' }}>
{t('models.noLoRAsLoaded')}
</Text>
</Flex>
);
}
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
selectedModel: field.value
? { ...field.value, model_type: 'lora' }
: undefined,
isLoading,
});
return (
<IAIMantineSearchableSelect
<InvControl
className="nowheel nodrag"
value={selectedLoRAModel?.id ?? null}
placeholder={
data.length > 0 ? t('models.selectLoRA') : t('models.noLoRAsAvailable')
}
data={data}
nothingFound={t('models.noMatchingLoRAs')}
itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0}
filter={filterFunc}
error={!selectedLoRAModel}
onChange={handleChange}
sx={{
width: '100%',
'.mantine-Select-dropdown': {
width: '16rem !important',
},
}}
/>
isInvalid={!value}
isDisabled={!options.length}
>
<InvSelect
value={value}
placeholder={placeholder}
noOptionsMessage={noOptionsMessage}
options={options}
onChange={onChange}
/>
</InvControl>
);
};

View File

@ -1,150 +1,68 @@
import { Flex, Text } from '@chakra-ui/react';
import { SelectItem } from '@mantine/core';
import { Flex } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
MainModelFieldInputTemplate,
import type {
MainModelFieldInputInstance,
MainModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import { NON_SDXL_MAIN_MODELS } from 'services/api/constants';
import {
useGetMainModelsQuery,
useGetOnnxModelsQuery,
} from 'services/api/endpoints/models';
import { useTranslation } from 'react-i18next';
import type { MainModelConfigEntity } from 'services/api/endpoints/models';
import { useGetMainModelsQuery } from 'services/api/endpoints/models';
const MainModelFieldInputComponent = (
props: FieldComponentProps<
MainModelFieldInputInstance,
MainModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
MainModelFieldInputInstance,
MainModelFieldInputTemplate
>;
const MainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
const { t } = useTranslation();
const { data: onnxModels, isLoading: isLoadingOnnxModels } =
useGetOnnxModelsQuery(NON_SDXL_MAIN_MODELS);
const { data: mainModels, isLoading: isLoadingMainModels } =
useGetMainModelsQuery(NON_SDXL_MAIN_MODELS);
const isLoadingModels = useMemo(
() => isLoadingOnnxModels || isLoadingMainModels,
[isLoadingOnnxModels, isLoadingMainModels]
);
const data = useMemo(() => {
if (!mainModels) {
return [];
}
const data: SelectItem[] = [];
forEach(mainModels.entities, (model, id) => {
if (!model) {
const { data, isLoading } = useGetMainModelsQuery(NON_SDXL_MAIN_MODELS);
const _onChange = useCallback(
(value: MainModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
if (onnxModels) {
forEach(onnxModels.entities, (model, id) => {
if (!model) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
}
return data;
}, [mainModels, onnxModels]);
// grab the full model entity from the RTK Query cache
// TODO: maybe we should just store the full model entity in state?
const selectedModel = useMemo(
() =>
(mainModels?.entities[
`${field.value?.base_model}/main/${field.value?.model_name}`
] ||
onnxModels?.entities[
`${field.value?.base_model}/onnx/${field.value?.model_name}`
]) ??
null,
[
field.value?.base_model,
field.value?.model_name,
mainModels?.entities,
onnxModels?.entities,
]
);
const handleChangeModel = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newModel = modelIdToMainModelParam(v);
if (!newModel) {
return;
}
dispatch(
fieldMainModelValueChanged({
nodeId,
fieldName: field.name,
value: newModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
isLoading,
});
return (
<Flex sx={{ w: 'full', alignItems: 'center', gap: 2 }}>
{isLoadingModels ? (
<Text variant="subtext">Loading...</Text>
) : (
<IAIMantineSearchableSelect
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={
data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
}
data={data}
error={!selectedModel}
disabled={data.length === 0}
onChange={handleChangeModel}
sx={{
width: '100%',
'.mantine-Select-dropdown': {
width: '16rem !important',
},
}}
<InvControl
className="nowheel nodrag"
isDisabled={!options.length}
isInvalid={!value}
>
<InvSelect
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
)}
{isSyncModelEnabled && <SyncModelsButton className="nodrag" iconMode />}
</InvControl>
<SyncModelsIconButton className="nodrag" />
</Flex>
);
};

View File

@ -1,21 +1,17 @@
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
} from '@chakra-ui/react';
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import { numberStringRegex } from 'common/components/IAINumberInput';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { fieldNumberValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
FloatFieldInputInstance,
FloatFieldInputTemplate,
IntegerFieldInputInstance,
IntegerFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { isNil } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import type { FieldComponentProps } from './types';
const NumberFieldInputComponent = (
props: FieldComponentProps<
@ -25,54 +21,53 @@ const NumberFieldInputComponent = (
) => {
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const [valueAsString, setValueAsString] = useState<string>(
String(field.value)
);
const isIntegerField = useMemo(
() => fieldTemplate.type.name === 'IntegerField',
[fieldTemplate.type]
);
const handleValueChanged = useCallback(
(v: string) => {
setValueAsString(v);
// This allows negatives and decimals e.g. '-123', `.5`, `-0.2`, etc.
if (!v.match(numberStringRegex)) {
// Cast the value to number. Floor it if it should be an integer.
dispatch(
fieldNumberValueChanged({
nodeId,
fieldName: field.name,
value: isIntegerField ? Math.floor(Number(v)) : Number(v),
})
);
}
(v: number) => {
dispatch(
fieldNumberValueChanged({
nodeId,
fieldName: field.name,
value: isIntegerField ? Math.floor(Number(v)) : Number(v),
})
);
},
[dispatch, field.name, isIntegerField, nodeId]
);
useEffect(() => {
if (
!valueAsString.match(numberStringRegex) &&
field.value !== Number(valueAsString)
) {
setValueAsString(String(field.value));
const min = useMemo(() => {
if (!isNil(fieldTemplate.minimum)) {
return fieldTemplate.minimum;
}
}, [field.value, valueAsString]);
if (!isNil(fieldTemplate.exclusiveMinimum)) {
return fieldTemplate.exclusiveMinimum + 0.01;
}
return;
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
const max = useMemo(() => {
if (!isNil(fieldTemplate.maximum)) {
return fieldTemplate.maximum;
}
if (!isNil(fieldTemplate.exclusiveMaximum)) {
return fieldTemplate.exclusiveMaximum - 0.01;
}
return;
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
return (
<NumberInput
<InvNumberInput
onChange={handleValueChanged}
value={valueAsString}
value={field.value}
min={min ?? NUMPY_RAND_MIN}
max={max ?? NUMPY_RAND_MAX}
step={isIntegerField ? 1 : 0.1}
precision={isIntegerField ? 0 : 3}
>
<NumberInputField className="nodrag" />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
className="nodrag"
/>
);
};

View File

@ -1,121 +1,68 @@
import { Flex } from '@chakra-ui/react';
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton';
import { fieldRefinerModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
SDXLRefinerModelFieldInputTemplate,
import type {
SDXLRefinerModelFieldInputInstance,
SDXLRefinerModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { memo, useCallback } from 'react';
import { REFINER_BASE_MODELS } from 'services/api/constants';
import type { MainModelConfigEntity } from 'services/api/endpoints/models';
import { useGetMainModelsQuery } from 'services/api/endpoints/models';
const RefinerModelFieldInputComponent = (
props: FieldComponentProps<
SDXLRefinerModelFieldInputInstance,
SDXLRefinerModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
SDXLRefinerModelFieldInputInstance,
SDXLRefinerModelFieldInputTemplate
>;
const RefinerModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
const { data: refinerModels, isLoading } =
useGetMainModelsQuery(REFINER_BASE_MODELS);
const data = useMemo(() => {
if (!refinerModels) {
return [];
}
const data: SelectItem[] = [];
forEach(refinerModels.entities, (model, id) => {
if (!model) {
const { data, isLoading } = useGetMainModelsQuery(REFINER_BASE_MODELS);
const _onChange = useCallback(
(value: MainModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
return data;
}, [refinerModels]);
// grab the full model entity from the RTK Query cache
// TODO: maybe we should just store the full model entity in state?
const selectedModel = useMemo(
() =>
refinerModels?.entities[
`${field.value?.base_model}/main/${field.value?.model_name}`
] ?? null,
[field.value?.base_model, field.value?.model_name, refinerModels?.entities]
);
const handleChangeModel = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newModel = modelIdToMainModelParam(v);
if (!newModel) {
return;
}
dispatch(
fieldRefinerModelValueChanged({
nodeId,
fieldName: field.name,
value: newModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
isLoading,
});
return isLoading ? (
<IAIMantineSearchableSelect
label={t('modelManager.model')}
placeholder={t('models.loading')}
disabled={true}
data={[]}
/>
) : (
<Flex w="100%" alignItems="center" gap={2}>
<IAIMantineSearchableSelect
return (
<Flex sx={{ w: 'full', alignItems: 'center', gap: 2 }}>
<InvControl
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={
data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
}
data={data}
error={!selectedModel}
disabled={data.length === 0}
onChange={handleChangeModel}
sx={{
width: '100%',
'.mantine-Select-dropdown': {
width: '16rem !important',
},
}}
/>
{isSyncModelEnabled && <SyncModelsButton className="nodrag" iconMode />}
isDisabled={!options.length}
isInvalid={!value}
>
<InvSelect
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</InvControl>
<SyncModelsIconButton className="nodrag" />
</Flex>
);
};

View File

@ -1,148 +1,68 @@
import { Flex } from '@chakra-ui/react';
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton';
import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
SDXLMainModelFieldInputTemplate,
import type {
SDXLMainModelFieldInputInstance,
SDXLMainModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToMainModelParam } from 'features/parameters/util/modelIdToMainModelParam';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import SyncModelsButton from 'features/modelManager/subpanels/ModelManagerSettingsPanel/SyncModelsButton';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { memo, useCallback } from 'react';
import { SDXL_MAIN_MODELS } from 'services/api/constants';
import {
useGetMainModelsQuery,
useGetOnnxModelsQuery,
} from 'services/api/endpoints/models';
import type { MainModelConfigEntity } from 'services/api/endpoints/models';
import { useGetMainModelsQuery } from 'services/api/endpoints/models';
const SDXLMainModelFieldInputComponent = (
props: FieldComponentProps<
SDXLMainModelFieldInputInstance,
SDXLMainModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
SDXLMainModelFieldInputInstance,
SDXLMainModelFieldInputTemplate
>;
const SDXLMainModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;
const { data: onnxModels } = useGetOnnxModelsQuery(SDXL_MAIN_MODELS);
const { data: mainModels, isLoading } =
useGetMainModelsQuery(SDXL_MAIN_MODELS);
const data = useMemo(() => {
if (!mainModels) {
return [];
}
const data: SelectItem[] = [];
forEach(mainModels.entities, (model, id) => {
if (!model || model.base_model !== 'sdxl') {
const { data, isLoading } = useGetMainModelsQuery(SDXL_MAIN_MODELS);
const _onChange = useCallback(
(value: MainModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
if (onnxModels) {
forEach(onnxModels.entities, (model, id) => {
if (!model || model.base_model !== 'sdxl') {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
}
return data;
}, [mainModels, onnxModels]);
// grab the full model entity from the RTK Query cache
// TODO: maybe we should just store the full model entity in state?
const selectedModel = useMemo(
() =>
(mainModels?.entities[
`${field.value?.base_model}/main/${field.value?.model_name}`
] ||
onnxModels?.entities[
`${field.value?.base_model}/onnx/${field.value?.model_name}`
]) ??
null,
[
field.value?.base_model,
field.value?.model_name,
mainModels?.entities,
onnxModels?.entities,
]
);
const handleChangeModel = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newModel = modelIdToMainModelParam(v);
if (!newModel) {
return;
}
dispatch(
fieldMainModelValueChanged({
nodeId,
fieldName: field.name,
value: newModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
isLoading,
});
return isLoading ? (
<IAIMantineSearchableSelect
label={t('modelManager.model')}
placeholder={t('models.loading')}
disabled={true}
data={[]}
/>
) : (
<Flex w="100%" alignItems="center" gap={2}>
<IAIMantineSearchableSelect
return (
<Flex sx={{ w: 'full', alignItems: 'center', gap: 2 }}>
<InvControl
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id}
placeholder={
data.length > 0
? t('models.selectModel')
: t('models.noModelsAvailable')
}
data={data}
error={!selectedModel}
disabled={data.length === 0}
onChange={handleChangeModel}
sx={{
width: '100%',
'.mantine-Select-dropdown': {
width: '16rem !important',
},
}}
/>
{isSyncModelEnabled && <SyncModelsButton className="nodrag" iconMode />}
isDisabled={!options.length}
isInvalid={!value}
>
<InvSelect
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</InvControl>
<SyncModelsIconButton className="nodrag" />
</Flex>
);
};

View File

@ -1,67 +1,56 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { useAppDispatch } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import type { InvSelectOnChange } from 'common/components/InvSelect/types';
import { fieldSchedulerValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
SchedulerFieldInputInstance,
SchedulerFieldInputTemplate,
} from 'features/nodes/types/field';
import { SCHEDULER_LABEL_MAP } from 'features/parameters/types/constants';
import { ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import { map } from 'lodash-es';
import { memo, useCallback } from 'react';
import { FieldComponentProps } from './types';
import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants';
import { isParameterScheduler } from 'features/parameters/types/parameterSchemas';
import { memo, useCallback, useMemo } from 'react';
const selector = createMemoizedSelector([stateSelector], ({ ui }) => {
const { favoriteSchedulers: enabledSchedulers } = ui;
import type { FieldComponentProps } from './types';
const data = map(SCHEDULER_LABEL_MAP, (label, name) => ({
value: name,
label: label,
group: enabledSchedulers.includes(name as ParameterScheduler)
? 'Favorites'
: undefined,
})).sort((a, b) => a.label.localeCompare(b.label));
type Props = FieldComponentProps<
SchedulerFieldInputInstance,
SchedulerFieldInputTemplate
>;
return {
data,
};
});
const SchedulerFieldInputComponent = (
props: FieldComponentProps<
SchedulerFieldInputInstance,
SchedulerFieldInputTemplate
>
) => {
const SchedulerFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { data } = useAppSelector(selector);
const handleChange = useCallback(
(value: string | null) => {
if (!value) {
const onChange = useCallback<InvSelectOnChange>(
(v) => {
if (!isParameterScheduler(v?.value)) {
return;
}
dispatch(
fieldSchedulerValueChanged({
nodeId,
fieldName: field.name,
value: value as ParameterScheduler,
value: v.value,
})
);
},
[dispatch, field.name, nodeId]
);
const value = useMemo(
() => SCHEDULER_OPTIONS.find((o) => o.value === field?.value),
[field?.value]
);
return (
<IAIMantineSearchableSelect
className="nowheel nodrag"
value={field.value}
data={data}
onChange={handleChange}
/>
<InvControl className="nowheel nodrag">
<InvSelect
value={value}
options={SCHEDULER_OPTIONS}
onChange={onChange}
/>
</InvControl>
);
};

View File

@ -1,13 +1,15 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIInput from 'common/components/IAIInput';
import IAITextarea from 'common/components/IAITextarea';
import { InvInput } from 'common/components/InvInput/InvInput';
import { InvTextarea } from 'common/components/InvTextarea/InvTextarea';
import { fieldStringValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
StringFieldInputInstance,
StringFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { FieldComponentProps } from './types';
const StringFieldInputComponent = (
props: FieldComponentProps<StringFieldInputInstance, StringFieldInputTemplate>
@ -30,7 +32,7 @@ const StringFieldInputComponent = (
if (fieldTemplate.ui_component === 'textarea') {
return (
<IAITextarea
<InvTextarea
className="nodrag"
onChange={handleValueChanged}
value={field.value}
@ -40,7 +42,7 @@ const StringFieldInputComponent = (
);
}
return <IAIInput onChange={handleValueChanged} value={field.value} />;
return <InvInput onChange={handleValueChanged} value={field.value} />;
};
export default memo(StringFieldInputComponent);

View File

@ -1,18 +1,19 @@
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { fieldT2IAdapterModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
import type {
T2IAdapterModelFieldInputInstance,
T2IAdapterModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToT2IAdapterModelParam } from 'features/parameters/util/modelIdToT2IAdapterModelParam';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import type { T2IAdapterModelConfigEntity } from 'services/api/endpoints/models';
import { useGetT2IAdapterModelsQuery } from 'services/api/endpoints/models';
import type { FieldComponentProps } from './types';
const T2IAdapterModelFieldInputComponent = (
props: FieldComponentProps<
T2IAdapterModelFieldInputInstance,
@ -20,80 +21,46 @@ const T2IAdapterModelFieldInputComponent = (
>
) => {
const { nodeId, field } = props;
const t2iAdapterModel = field.value;
const dispatch = useAppDispatch();
const { data: t2iAdapterModels } = useGetT2IAdapterModelsQuery();
// grab the full model entity from the RTK Query cache
const selectedModel = useMemo(
() =>
t2iAdapterModels?.entities[
`${t2iAdapterModel?.base_model}/t2i_adapter/${t2iAdapterModel?.model_name}`
] ?? null,
[
t2iAdapterModel?.base_model,
t2iAdapterModel?.model_name,
t2iAdapterModels?.entities,
]
);
const data = useMemo(() => {
if (!t2iAdapterModels) {
return [];
}
const data: SelectItem[] = [];
forEach(t2iAdapterModels.entities, (model, id) => {
if (!model) {
const _onChange = useCallback(
(value: T2IAdapterModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: model.model_name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
return data;
}, [t2iAdapterModels]);
const handleValueChanged = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newT2IAdapterModel = modelIdToT2IAdapterModelParam(v);
if (!newT2IAdapterModel) {
return;
}
dispatch(
fieldT2IAdapterModelValueChanged({
nodeId,
fieldName: field.name,
value: newT2IAdapterModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange } = useGroupedModelInvSelect({
modelEntities: t2iAdapterModels,
onChange: _onChange,
selectedModel: field.value
? { ...field.value, model_type: 't2i_adapter' }
: undefined,
});
return (
<IAIMantineSelect
className="nowheel nodrag"
tooltip={selectedModel?.description}
value={selectedModel?.id ?? null}
placeholder="Pick one"
error={!selectedModel}
data={data}
onChange={handleValueChanged}
sx={{ width: '100%' }}
/>
<InvTooltip label={value?.description}>
<InvControl className="nowheel nodrag" isInvalid={!value}>
<InvSelect
value={value}
placeholder="Pick one"
options={options}
onChange={onChange}
sx={{ width: '100%' }}
/>
</InvControl>
</InvTooltip>
);
};

View File

@ -1,107 +1,69 @@
import { SelectItem } from '@mantine/core';
import { Flex } from '@chakra-ui/layout';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSelect } from 'common/components/InvSelect/InvSelect';
import { useGroupedModelInvSelect } from 'common/components/InvSelect/useGroupedModelInvSelect';
import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton';
import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice';
import {
VAEModelFieldInputTemplate,
import type {
VAEModelFieldInputInstance,
VAEModelFieldInputTemplate,
} from 'features/nodes/types/field';
import { FieldComponentProps } from './types';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam';
import { forEach } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import type { VaeModelConfigEntity } from 'services/api/endpoints/models';
import { useGetVaeModelsQuery } from 'services/api/endpoints/models';
const VAEModelFieldInputComponent = (
props: FieldComponentProps<
VAEModelFieldInputInstance,
VAEModelFieldInputTemplate
>
) => {
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<
VAEModelFieldInputInstance,
VAEModelFieldInputTemplate
>;
const VAEModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const vae = field.value;
const dispatch = useAppDispatch();
const { data: vaeModels } = useGetVaeModelsQuery();
const data = useMemo(() => {
if (!vaeModels) {
return [];
}
const data: SelectItem[] = [
{
value: 'default',
label: 'Default',
group: 'Default',
},
];
forEach(vaeModels.entities, (vae, id) => {
if (!vae) {
const { data, isLoading } = useGetVaeModelsQuery();
const _onChange = useCallback(
(value: VaeModelConfigEntity | null) => {
if (!value) {
return;
}
data.push({
value: id,
label: vae.model_name,
group: MODEL_TYPE_MAP[vae.base_model],
});
});
return data.sort((a, b) => (a.disabled && !b.disabled ? 1 : -1));
}, [vaeModels]);
// grab the full model entity from the RTK Query cache
const selectedVaeModel = useMemo(
() =>
vaeModels?.entities[`${vae?.base_model}/vae/${vae?.model_name}`] ?? null,
[vaeModels?.entities, vae]
);
const handleChangeModel = useCallback(
(v: string | null) => {
if (!v) {
return;
}
const newVaeModel = modelIdToVAEModelParam(v);
if (!newVaeModel) {
return;
}
dispatch(
fieldVaeModelValueChanged({
nodeId,
fieldName: field.name,
value: newVaeModel,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } =
useGroupedModelInvSelect({
modelEntities: data,
onChange: _onChange,
selectedModel: field.value ? { ...field.value, model_type: 'vae' } : null,
isLoading,
});
return (
<IAIMantineSearchableSelect
className="nowheel nodrag"
itemComponent={IAIMantineSelectItemWithTooltip}
tooltip={selectedVaeModel?.description}
value={selectedVaeModel?.id ?? 'default'}
placeholder="Default"
data={data}
onChange={handleChangeModel}
disabled={data.length === 0}
error={!selectedVaeModel}
clearable
sx={{
width: '100%',
'.mantine-Select-dropdown': {
width: '16rem !important',
},
}}
/>
<Flex sx={{ w: 'full', alignItems: 'center', gap: 2 }}>
<InvControl
className="nowheel nodrag"
isDisabled={!options.length}
isInvalid={!value}
>
<InvSelect
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</InvControl>
<SyncModelsIconButton className="nodrag" />
</Flex>
);
};

View File

@ -1,4 +1,4 @@
import {
import type {
FieldInputInstance,
FieldInputTemplate,
} from 'features/nodes/types/field';

View File

@ -1,13 +1,14 @@
import { Box, Flex } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
import { NotesNodeData } from 'features/nodes/types/invocation';
import { ChangeEvent, memo, useCallback } from 'react';
import { NodeProps } from 'reactflow';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { InvTextarea } from 'common/components/InvTextarea/InvTextarea';
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
import type { NotesNodeData } from 'features/nodes/types/invocation';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { NodeProps } from 'reactflow';
const NotesNode = (props: NodeProps<NotesNodeData>) => {
const { id: nodeId, data, selected } = props;
@ -55,7 +56,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
className="nopan"
sx={{ flexDir: 'column', w: 'full', h: 'full' }}
>
<IAITextarea
<InvTextarea
value={notes}
onChange={handleChange}
rows={8}

View File

@ -1,6 +1,6 @@
import { ChevronUpIcon } from '@chakra-ui/icons';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { useUpdateNodeInternals } from 'reactflow';
@ -20,7 +20,7 @@ const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
}, [dispatch, isOpen, nodeId, updateNodeInternals]);
return (
<IAIIconButton
<InvIconButton
className="nodrag"
onClick={handleClick}
aria-label="Minimize"
@ -29,14 +29,8 @@ const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
w: 8,
h: 8,
color: 'base.500',
_dark: {
color: 'base.500',
},
_hover: {
color: 'base.700',
_dark: {
color: 'base.300',
},
color: 'base.300',
},
}}
variant="link"

View File

@ -11,7 +11,8 @@ import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
import type { MouseEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
@ -83,7 +84,7 @@ const NodeTitle = ({ nodeId, title }: Props) => {
fontSize="sm"
sx={{
p: 0,
fontWeight: 700,
fontWeight: 'bold',
_focusVisible: {
p: 0,
boxShadow: 'none',

View File

@ -1,9 +1,5 @@
import {
Box,
ChakraProps,
useColorModeValue,
useToken,
} from '@chakra-ui/react';
import type { ChakraProps } from '@chakra-ui/react';
import { Box, useToken } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -16,13 +12,8 @@ import {
} from 'features/nodes/types/constants';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
import {
MouseEvent,
PropsWithChildren,
memo,
useCallback,
useMemo,
} from 'react';
import type { MouseEvent, PropsWithChildren } from 'react';
import { memo, useCallback, useMemo } from 'react';
type NodeWrapperProps = PropsWithChildren & {
nodeId: string;
@ -48,21 +39,14 @@ const NodeWrapper = (props: NodeWrapperProps) => {
const isInProgress = useAppSelector(selectIsInProgress);
const [nodeInProgressLight, nodeInProgressDark, shadowsXl, shadowsBase] =
useToken('shadows', [
'nodeInProgress.light',
'nodeInProgress.dark',
'shadows.xl',
'shadows.base',
]);
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [
'nodeInProgress',
'shadows.xl',
'shadows.base',
]);
const dispatch = useAppDispatch();
const inProgressShadow = useColorModeValue(
nodeInProgressLight,
nodeInProgressDark
);
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
const handleClick = useCallback(
@ -117,7 +101,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
transitionProperty: 'common',
transitionDuration: '0.1s',
opacity: 0.7,
shadow: isInProgress ? inProgressShadow : undefined,
shadow: isInProgress ? nodeInProgress : undefined,
zIndex: -1,
}}
/>

View File

@ -1,5 +1,6 @@
import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import NodeOpacitySlider from './NodeOpacitySlider';
import ViewportControls from './ViewportControls';

View File

@ -1,11 +1,6 @@
import {
Flex,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
} from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -24,7 +19,7 @@ export default function NodeOpacitySlider() {
return (
<Flex alignItems="center">
<Slider
<InvSlider
aria-label={t('nodes.nodeOpacity')}
value={nodeOpacity}
min={0.5}
@ -34,12 +29,7 @@ export default function NodeOpacitySlider() {
orientation="vertical"
defaultValue={30}
h="calc(100% - 0.5rem)"
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
/>
</Flex>
);
}

View File

@ -1,6 +1,6 @@
import { ButtonGroup } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
@ -47,40 +47,40 @@ const ViewportControls = () => {
}, [shouldShowMinimapPanel, dispatch]);
return (
<ButtonGroup isAttached orientation="vertical">
<IAIIconButton
<InvButtonGroup orientation="vertical">
<InvIconButton
tooltip={t('nodes.zoomInNodes')}
aria-label={t('nodes.zoomInNodes')}
onClick={handleClickedZoomIn}
icon={<FaMagnifyingGlassPlus />}
/>
<IAIIconButton
<InvIconButton
tooltip={t('nodes.zoomOutNodes')}
aria-label={t('nodes.zoomOutNodes')}
onClick={handleClickedZoomOut}
icon={<FaMagnifyingGlassMinus />}
/>
<IAIIconButton
<InvIconButton
tooltip={t('nodes.fitViewportNodes')}
aria-label={t('nodes.fitViewportNodes')}
onClick={handleClickedFitView}
icon={<FaExpand />}
/>
{/* <Tooltip
{/* <InvTooltip
label={
shouldShowFieldTypeLegend
? t('nodes.hideLegendNodes')
: t('nodes.showLegendNodes')
}
>
<IAIIconButton
<InvIconButton
aria-label="Toggle field type legend"
isChecked={shouldShowFieldTypeLegend}
onClick={handleClickedToggleFieldTypeLegend}
icon={<FaInfo />}
/>
</Tooltip> */}
<IAIIconButton
</InvTooltip> */}
<InvIconButton
tooltip={
shouldShowMinimapPanel
? t('nodes.hideMinimapnodes')
@ -95,7 +95,7 @@ const ViewportControls = () => {
onClick={handleClickedToggleMiniMapPanel}
icon={<FaMapMarkerAlt />}
/>
</ButtonGroup>
</InvButtonGroup>
);
};

View File

@ -1,5 +1,5 @@
import { Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { chakra, Flex } from '@chakra-ui/react';
import type { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react';
import { MiniMap } from 'reactflow';
@ -11,16 +11,6 @@ const MinimapPanel = () => {
(state: RootState) => state.nodes.shouldShowMinimapPanel
);
const nodeColor = useColorModeValue(
'var(--invokeai-colors-accent-300)',
'var(--invokeai-colors-accent-600)'
);
const maskColor = useColorModeValue(
'var(--invokeai-colors-blackAlpha-300)',
'var(--invokeai-colors-blackAlpha-600)'
);
return (
<Flex sx={{ gap: 2, position: 'absolute', bottom: 2, insetInlineEnd: 2 }}>
{shouldShowMinimapPanel && (
@ -30,17 +20,14 @@ const MinimapPanel = () => {
nodeBorderRadius={15}
sx={{
m: '0 !important',
backgroundColor: 'base.200 !important',
borderRadius: 'base',
_dark: {
backgroundColor: 'base.500 !important',
},
backgroundColor: 'base.500 !important',
svg: {
borderRadius: 'inherit',
},
}}
nodeColor={nodeColor}
maskColor={maskColor}
nodeColor="var(--invokeai-colors-blue-600)"
maskColor="var(--invokeai-colors-blackAlpha-600)"
/>
)}
</Flex>

View File

@ -1,43 +0,0 @@
import { Flex } from '@chakra-ui/layout';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaExclamationTriangle, FaPlus } from 'react-icons/fa';
const TopLeftPanel = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const nodesNeedUpdate = useGetNodesNeedUpdate();
const handleOpenAddNodePopover = useCallback(() => {
dispatch(addNodePopoverOpened());
}, [dispatch]);
const handleClickUpdateNodes = useCallback(() => {
dispatch(updateAllNodesRequested());
}, [dispatch]);
return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
<IAIIconButton
tooltip={t('nodes.addNodeToolTip')}
aria-label={t('nodes.addNode')}
icon={<FaPlus />}
onClick={handleOpenAddNodePopover}
/>
{nodesNeedUpdate && (
<IAIButton
leftIcon={<FaExclamationTriangle />}
onClick={handleClickUpdateNodes}
>
{t('nodes.updateAllNodes')}
</IAIButton>
)}
</Flex>
);
};
export default memo(TopLeftPanel);

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -13,7 +13,7 @@ const AddNodeButton = () => {
}, [dispatch]);
return (
<IAIIconButton
<InvIconButton
tooltip={t('nodes.addNodeToolTip')}
aria-label={t('nodes.addNode')}
icon={<FaPlus />}

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { InvButton } from 'common/components/InvButton/InvButton';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { memo, useCallback } from 'react';
@ -19,13 +19,13 @@ const UpdateNodesButton = () => {
}
return (
<IAIButton
<InvButton
leftIcon={<FaExclamationTriangle />}
onClick={handleClickUpdateNodes}
pointerEvents="auto"
>
{t('nodes.updateAllNodes')}
</IAIButton>
</InvButton>
);
};

View File

@ -1,8 +1,8 @@
import { Text } from '@chakra-ui/layout';
import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { InvText } from 'common/components/InvText/wrapper';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const TopCenterPanel = () => {
const { t } = useTranslation();
@ -11,19 +11,26 @@ const TopCenterPanel = () => {
const isWorkflowLibraryEnabled =
useFeatureStatus('workflowLibrary').isFeatureEnabled;
const displayName = useMemo(() => {
let _displayName = name || t('workflows.unnamedWorkflow');
if (isTouched && isWorkflowLibraryEnabled) {
_displayName += ` (${t('common.unsaved')})`;
}
return _displayName;
}, [t, name, isTouched, isWorkflowLibraryEnabled]);
return (
<Text
<InvText
m={2}
fontSize="lg"
userSelect="none"
noOfLines={1}
wordBreak="break-all"
fontWeight={600}
fontWeight="semibold"
opacity={0.8}
>
{name || t('workflows.unnamedWorkflow')}
{isTouched && isWorkflowLibraryEnabled ? ` (${t('common.unsaved')})` : ''}
</Text>
{displayName}
</InvText>
);
};

View File

@ -1,5 +1,5 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import { InvButton } from 'common/components/InvButton/InvButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSyncAlt } from 'react-icons/fa';
@ -14,14 +14,14 @@ const ReloadNodeTemplatesButton = () => {
}, [dispatch]);
return (
<IAIButton
<InvButton
leftIcon={<FaSyncAlt />}
tooltip={t('nodes.reloadNodeTemplates')}
aria-label={t('nodes.reloadNodeTemplates')}
onClick={handleReloadSchema}
>
{t('nodes.reloadNodeTemplates')}
</IAIButton>
</InvButton>
);
};

View File

@ -1,8 +1,8 @@
import { Flex } from '@chakra-ui/react';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import { memo } from 'react';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
const TopRightPanel = () => {
const isWorkflowLibraryEnabled =

View File

@ -1,21 +1,18 @@
import {
Divider,
Flex,
FormLabelProps,
Heading,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
useDisclosure,
} from '@chakra-ui/react';
import { Divider, Flex, Heading, useDisclosure } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton';
import { InvControl } from 'common/components/InvControl/InvControl';
import {
InvModal,
InvModalBody,
InvModalCloseButton,
InvModalContent,
InvModalHeader,
InvModalOverlay,
} from 'common/components/InvModal/wrapper';
import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
import {
selectionModeChanged,
shouldAnimateEdgesChanged,
@ -23,14 +20,11 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/nodesSlice';
import { ChangeEvent, ReactNode, memo, useCallback } from 'react';
import type { ChangeEvent, ReactNode } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = {
fontWeight: 600,
};
const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
const {
shouldAnimateEdges,
@ -104,12 +98,12 @@ const WorkflowEditorSettings = ({ children }: Props) => {
<>
{children({ onOpen })}
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<InvModal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
<InvModalOverlay />
<InvModalContent>
<InvModalHeader>{t('nodes.workflowSettings')}</InvModalHeader>
<InvModalCloseButton />
<InvModalBody>
<Flex
sx={{
flexDirection: 'column',
@ -118,52 +112,62 @@ const WorkflowEditorSettings = ({ children }: Props) => {
}}
>
<Heading size="sm">{t('parameters.general')}</Heading>
<IAISwitch
formLabelProps={formLabelProps}
onChange={handleChangeShouldAnimate}
isChecked={shouldAnimateEdges}
<InvControl
label={t('nodes.animatedEdges')}
helperText={t('nodes.animatedEdgesHelp')}
/>
>
<InvSwitch
onChange={handleChangeShouldAnimate}
isChecked={shouldAnimateEdges}
/>
</InvControl>
<Divider />
<IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnap}
<InvControl
label={t('nodes.snapToGrid')}
helperText={t('nodes.snapToGridHelp')}
/>
>
<InvSwitch
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnap}
/>
</InvControl>
<Divider />
<IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldColorEdges}
onChange={handleChangeShouldColor}
<InvControl
label={t('nodes.colorCodeEdges')}
helperText={t('nodes.colorCodeEdgesHelp')}
/>
>
<InvSwitch
isChecked={shouldColorEdges}
onChange={handleChangeShouldColor}
/>
</InvControl>
<Divider />
<IAISwitch
formLabelProps={formLabelProps}
isChecked={selectionModeIsChecked}
onChange={handleChangeSelectionMode}
<InvControl
label={t('nodes.fullyContainNodes')}
helperText={t('nodes.fullyContainNodesHelp')}
/>
>
<InvSwitch
isChecked={selectionModeIsChecked}
onChange={handleChangeSelectionMode}
/>
</InvControl>
<Heading size="sm" pt={4}>
{t('common.advanced')}
</Heading>
<IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldValidateGraph}
onChange={handleChangeShouldValidate}
<InvControl
label={t('nodes.validateConnections')}
helperText={t('nodes.validateConnectionsHelp')}
/>
>
<InvSwitch
isChecked={shouldValidateGraph}
onChange={handleChangeShouldValidate}
/>
</InvControl>
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
</InvModalBody>
</InvModalContent>
</InvModal>
</>
);
};

View File

@ -1,15 +1,13 @@
import 'reactflow/dist/style.css';
import { Flex } from '@chakra-ui/react';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import { memo, useCallback, useRef, useState } from 'react';
import {
ImperativePanelGroupHandle,
Panel,
PanelGroup,
} from 'react-resizable-panels';
import 'reactflow/dist/style.css';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import InspectorPanel from './inspector/InspectorPanel';
import WorkflowPanel from './workflow/WorkflowPanel';
@ -28,20 +26,6 @@ const NodeEditorPanelGroup = () => {
return (
<Flex sx={{ flexDir: 'column', gap: 2, height: '100%', width: '100%' }}>
<QueueControls />
<Flex
layerStyle="first"
sx={{
w: 'full',
position: 'relative',
borderRadius: 'base',
p: 2,
pb: 3,
gap: 2,
flexDir: 'column',
}}
>
<ParamIterations asSlider />
</Flex>
<PanelGroup
ref={panelGroupRef}
id="workflow-panel-group"

View File

@ -1,50 +0,0 @@
import { Box, Flex, StyleProps } from '@chakra-ui/react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { PropsWithChildren, memo } from 'react';
type Props = PropsWithChildren & {
maxHeight?: StyleProps['maxHeight'];
};
const ScrollableContent = ({ children, maxHeight }: Props) => {
return (
<Flex
sx={{
w: 'full',
h: 'full',
maxHeight,
position: 'relative',
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
<OverlayScrollbarsComponent
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'scroll',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
overflow: {
x: 'hidden',
},
}}
>
{children}
</OverlayScrollbarsComponent>
</Box>
</Flex>
);
};
export default memo(ScrollableContent);

View File

@ -1,25 +1,21 @@
import {
Box,
Flex,
FormControl,
FormLabel,
HStack,
Text,
} from '@chakra-ui/react';
import { Box, Flex, HStack } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvText } from 'common/components/InvText/wrapper';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import {
import type {
InvocationNode,
InvocationTemplate,
isInvocationNode,
} from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import EditableNodeTitle from './details/EditableNodeTitle';
const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {
@ -67,44 +63,28 @@ const Content = memo(({ node, template }: ContentProps) => {
[node, template]
);
return (
<Box
sx={{
position: 'relative',
w: 'full',
h: 'full',
}}
>
<Box position="relative" w="full" h="full">
<ScrollableContent>
<Flex
sx={{
flexDir: 'column',
position: 'relative',
p: 1,
gap: 2,
w: 'full',
}}
flexDir="column"
position="relative"
w="full"
h="full"
p={1}
gap={2}
>
<EditableNodeTitle nodeId={node.data.id} />
<HStack>
<FormControl>
<FormLabel>{t('nodes.nodeType')}</FormLabel>
<Text fontSize="sm" fontWeight={600}>
<InvControl label={t('nodes.nodeType')}>
<InvText fontSize="sm" fontWeight="semibold">
{template.title}
</Text>
</FormControl>
<Flex
flexDir="row"
alignItems="center"
justifyContent="space-between"
w="full"
>
<FormControl isInvalid={needsUpdate}>
<FormLabel>{t('nodes.nodeVersion')}</FormLabel>
<Text fontSize="sm" fontWeight={600}>
{node.data.version}
</Text>
</FormControl>
</Flex>
</InvText>
</InvControl>
<InvControl label={t('nodes.nodeVersion')} isInvalid={needsUpdate}>
<InvText fontSize="sm" fontWeight="semibold">
{node.data.version}
</InvText>
</InvControl>
</HStack>
<NotesTextarea nodeId={node.data.id} />
</Flex>

View File

@ -3,13 +3,14 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageOutput } from 'services/api/types';
import { AnyResult } from 'services/events/types';
import type { ImageOutput } from 'services/api/types';
import type { AnyResult } from 'services/events/types';
import ImageOutputPreview from './outputs/ImageOutputPreview';
const selector = createMemoizedSelector(stateSelector, ({ nodes }) => {

View File

@ -7,11 +7,12 @@ import {
Tabs,
} from '@chakra-ui/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import InspectorDataTab from './InspectorDataTab';
import InspectorDetailsTab from './InspectorDetailsTab';
import InspectorOutputsTab from './InspectorOutputsTab';
import InspectorTemplateTab from './InspectorTemplateTab';
import { useTranslation } from 'react-i18next';
import InspectorDetailsTab from './InspectorDetailsTab';
const InspectorPanel = () => {
const { t } = useTranslation();

View File

@ -45,21 +45,14 @@ const EditableNodeTitle = ({ nodeId, title }: Props) => {
}, [label, templateTitle, title, t]);
return (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Flex w="full" alignItems="center" justifyContent="center">
<Editable
as={Flex}
value={localTitle}
onChange={handleChange}
onSubmit={handleSubmit}
w="full"
fontWeight={600}
fontWeight="semibold"
>
<EditablePreview noOfLines={1} />
<EditableInput

View File

@ -1,7 +1,7 @@
import IAIDndImage from 'common/components/IAIDndImage';
import { memo } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { ImageOutput } from 'services/api/types';
import type { ImageOutput } from 'services/api/types';
type Props = {
output: ImageOutput;

View File

@ -1,10 +1,12 @@
import { Flex, FormControl, FormLabel } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIInput from 'common/components/IAIInput';
import IAITextarea from 'common/components/IAITextarea';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvControlGroup } from 'common/components/InvControl/InvControlGroup';
import { InvInput } from 'common/components/InvInput/InvInput';
import { InvTextarea } from 'common/components/InvTextarea/InvTextarea';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import {
workflowAuthorChanged,
workflowContactChanged,
@ -14,7 +16,8 @@ import {
workflowTagsChanged,
workflowVersionChanged,
} from 'features/nodes/store/workflowSlice';
import { ChangeEvent, memo, useCallback } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(stateSelector, ({ workflow }) => {
@ -92,53 +95,48 @@ const WorkflowGeneralTab = () => {
h: 'full',
}}
>
<Flex sx={{ gap: 2, w: 'full' }}>
<IAIInput
label={t('nodes.workflowName')}
value={name}
onChange={handleChangeName}
/>
<IAIInput
label={t('nodes.workflowVersion')}
value={version}
onChange={handleChangeVersion}
/>
</Flex>
<Flex sx={{ gap: 2, w: 'full' }}>
<IAIInput
label={t('nodes.workflowAuthor')}
value={author}
onChange={handleChangeAuthor}
/>
<IAIInput
label={t('nodes.workflowContact')}
value={contact}
onChange={handleChangeContact}
/>
</Flex>
<IAIInput
label={t('nodes.workflowTags')}
value={tags}
onChange={handleChangeTags}
/>
<FormControl as={Flex} sx={{ flexDir: 'column' }}>
<FormLabel>{t('nodes.workflowDescription')}</FormLabel>
<IAITextarea
<InvControlGroup orientation="vertical">
<Flex sx={{ gap: 2, w: 'full' }}>
<InvControl label={t('nodes.workflowName')}>
<InvInput value={name} onChange={handleChangeName} />
</InvControl>
<InvControl label={t('nodes.workflowVersion')}>
<InvInput value={version} onChange={handleChangeVersion} />
</InvControl>
</Flex>
<Flex sx={{ gap: 2, w: 'full' }}>
<InvControl label={t('nodes.workflowAuthor')}>
<InvInput value={author} onChange={handleChangeAuthor} />
</InvControl>
<InvControl label={t('nodes.workflowContact')}>
<InvInput value={contact} onChange={handleChangeContact} />
</InvControl>
</Flex>
<InvControl label={t('nodes.workflowTags')}>
<InvInput value={tags} onChange={handleChangeTags} />
</InvControl>
</InvControlGroup>
<InvControl
label={t('nodes.workflowDescription')}
orientation="vertical"
>
<InvTextarea
onChange={handleChangeDescription}
value={description}
fontSize="sm"
sx={{ resize: 'none' }}
resize="none"
rows={3}
/>
</FormControl>
<FormControl as={Flex} sx={{ flexDir: 'column', h: 'full' }}>
<FormLabel>{t('nodes.workflowNotes')}</FormLabel>
<IAITextarea
</InvControl>
<InvControl label={t('nodes.workflowNotes')} orientation="vertical">
<InvTextarea
onChange={handleChangeNotes}
value={notes}
fontSize="sm"
sx={{ h: 'full', resize: 'none' }}
resize="none"
rows={10}
/>
</FormControl>
</InvControl>
</Flex>
</ScrollableContent>
);

View File

@ -3,8 +3,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -7,10 +7,11 @@ import {
Tabs,
} from '@chakra-ui/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import WorkflowGeneralTab from './WorkflowGeneralTab';
import WorkflowJSONTab from './WorkflowJSONTab';
import WorkflowLinearTab from './WorkflowLinearTab';
import { useTranslation } from 'react-i18next';
const WorkflowPanel = () => {
const { t } = useTranslation();

View File

@ -1,16 +1,20 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { RootState } from 'app/store/store';
import type { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import {
DRAG_HANDLE_CLASSNAME,
NODE_WIDTH,
} from 'features/nodes/types/constants';
import { AnyNode, InvocationTemplate } from 'features/nodes/types/invocation';
import type {
AnyNode,
InvocationTemplate,
} from 'features/nodes/types/invocation';
import { buildCurrentImageNode } from 'features/nodes/util/node/buildCurrentImageNode';
import { buildInvocationNode } from 'features/nodes/util/node/buildInvocationNode';
import { buildNotesNode } from 'features/nodes/util/node/buildNotesNode';
import { useCallback } from 'react';
import { Node, useReactFlow } from 'reactflow';
import type { Node } from 'reactflow';
import { useReactFlow } from 'reactflow';
const templatesSelector = createMemoizedSelector(
[(state: RootState) => state.nodes],

View File

@ -3,6 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector';
import { useMemo } from 'react';
import { useFieldType } from './useFieldType.ts';
const selectIsConnectionInProgress = createMemoizedSelector(

View File

@ -1,17 +0,0 @@
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { useCallback } from 'react';
export const useDownloadWorkflow = () => {
const workflow = useWorkflow();
const downloadWorkflow = useCallback(() => {
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${workflow.name || 'My Workflow'}.json`;
document.body.appendChild(a);
a.click();
a.remove();
}, [workflow]);
return downloadWorkflow;
};

View File

@ -1,10 +1,11 @@
// TODO: enable this at some point
import { useAppSelector } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { Connection, Node, useReactFlow } from 'reactflow';
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic';
import { InvocationNodeData } from 'features/nodes/types/invocation';
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
import { useCallback } from 'react';
import type { Connection, Node } from 'reactflow';
import { useReactFlow } from 'reactflow';
/**
* NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts`

View File

@ -1,7 +1,7 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { InvocationTemplate } from 'features/nodes/types/invocation';
import type { InvocationTemplate } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeTemplateByType = (type: string) => {

View File

@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { FieldType } from 'features/nodes/types/field';
import type { FieldType } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useFieldTypeName = (fieldType?: FieldType): string => {
const { t } = useTranslation();

View File

@ -1,5 +1,6 @@
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useMemo } from 'react';
import { useHasImageOutput } from './useHasImageOutput';
export const useWithFooter = (nodeId: string) => {

View File

@ -1,6 +1,6 @@
import { createAction, isAnyOf } from '@reduxjs/toolkit';
import { WorkflowV2 } from 'features/nodes/types/workflow';
import { Graph } from 'services/api/types';
import type { WorkflowV2 } from 'features/nodes/types/workflow';
import type { Graph } from 'services/api/types';
export const textToImageGraphBuilt = createAction<Graph>(
'nodes/textToImageGraphBuilt'

View File

@ -1,4 +1,4 @@
import { NodesState } from './types';
import type { NodesState } from './types';
/**
* Nodes slice persist denylist

View File

@ -1,7 +1,8 @@
import { createSlice, isAnyOf, PayloadAction } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import { workflowLoaded } from 'features/nodes/store/actions';
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
import {
import type {
BoardFieldValue,
BooleanFieldValue,
ColorFieldValue,
@ -20,6 +21,8 @@ import {
StringFieldValue,
T2IAdapterModelFieldValue,
VAEModelFieldValue,
} from 'features/nodes/types/field';
import {
zBoardFieldValue,
zBooleanFieldValue,
zColorFieldValue,
@ -37,34 +40,38 @@ import {
zT2IAdapterModelFieldValue,
zVAEModelFieldValue,
} from 'features/nodes/types/field';
import {
import type {
AnyNode,
InvocationTemplate,
NodeExecutionState,
} from 'features/nodes/types/invocation';
import {
isInvocationNode,
isNotesNode,
NodeExecutionState,
zNodeStatus,
} from 'features/nodes/types/invocation';
import { cloneDeep, forEach } from 'lodash-es';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
import type {
Connection,
Edge,
EdgeChange,
EdgeRemoveChange,
getConnectedEdges,
getIncomers,
getOutgoers,
Node,
NodeChange,
OnConnectStartParams,
SelectionMode,
updateEdge,
Viewport,
XYPosition,
} from 'reactflow';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
getConnectedEdges,
getIncomers,
getOutgoers,
SelectionMode,
updateEdge,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import {
appSocketGeneratorProgress,
@ -74,8 +81,9 @@ import {
appSocketQueueItemStatusChanged,
} from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
import { NodesState } from './types';
import type { z } from 'zod';
import type { NodesState } from './types';
import { findConnectionToValidHandle } from './util/findConnectionToValidHandle';
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';

View File

@ -1,4 +1,4 @@
import { atom } from 'nanostores';
import { ReactFlowInstance } from 'reactflow';
import type { ReactFlowInstance } from 'reactflow';
export const $flow = atom<ReactFlowInstance | null>(null);

View File

@ -1,17 +1,17 @@
import {
OnConnectStartParams,
SelectionMode,
Viewport,
XYPosition,
} from 'reactflow';
import { FieldIdentifier, FieldType } from 'features/nodes/types/field';
import {
import type { FieldIdentifier, FieldType } from 'features/nodes/types/field';
import type {
AnyNode,
InvocationNodeEdge,
InvocationTemplate,
NodeExecutionState,
} from 'features/nodes/types/invocation';
import { WorkflowV2 } from 'features/nodes/types/workflow';
import type { WorkflowV2 } from 'features/nodes/types/workflow';
import type {
OnConnectStartParams,
SelectionMode,
Viewport,
XYPosition,
} from 'reactflow';
export type NodesState = {
nodes: AnyNode[];

View File

@ -1,9 +1,10 @@
import { Connection, Edge, HandleType, Node } from 'reactflow';
import {
import type {
FieldInputInstance,
FieldOutputInstance,
FieldType,
} from 'features/nodes/types/field';
import type { Connection, Edge, HandleType, Node } from 'reactflow';
import { getIsGraphAcyclic } from './getIsGraphAcyclic';
import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes';

View File

@ -1,4 +1,4 @@
import { Node } from 'reactflow';
import type { Node } from 'reactflow';
export const findUnoccupiedPosition = (nodes: Node[], x: number, y: number) => {
let newX = x;

View File

@ -1,5 +1,5 @@
import graphlib from '@dagrejs/graphlib';
import { Edge, Node } from 'reactflow';
import type { Edge, Node } from 'reactflow';
export const getIsGraphAcyclic = (
source: string,

View File

@ -1,8 +1,9 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { FieldType } from 'features/nodes/types/field';
import type { FieldType } from 'features/nodes/types/field';
import i18n from 'i18next';
import { HandleType } from 'reactflow';
import type { HandleType } from 'reactflow';
import { getIsGraphAcyclic } from './getIsGraphAcyclic';
import { validateSourceAndTargetTypes } from './validateSourceAndTargetTypes';

View File

@ -1,4 +1,4 @@
import { FieldType } from 'features/nodes/types/field';
import type { FieldType } from 'features/nodes/types/field';
import { isEqual } from 'lodash-es';
/**

View File

@ -1,12 +1,13 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { workflowLoaded } from 'features/nodes/store/actions';
import {
isAnyNodeOrEdgeMutation,
nodeEditorReset,
nodesDeleted,
isAnyNodeOrEdgeMutation,
} from 'features/nodes/store/nodesSlice';
import { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
import { FieldIdentifier } from 'features/nodes/types/field';
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
export const initialWorkflowState: WorkflowState = {

View File

@ -1,4 +1,4 @@
import { Node } from 'reactflow';
import type { Node } from 'reactflow';
/**
* How long to wait before showing a tooltip when hovering a field handle.

View File

@ -1,10 +1,11 @@
import { z } from 'zod';
import {
zBoardField,
zColorField,
zControlNetModelField,
zIPAdapterModelField,
zImageField,
zIPAdapterModelField,
zLoRAModelField,
zMainOrONNXModelField,
zSchedulerField,
@ -699,6 +700,10 @@ export const isControlNetModelFieldInputTemplate = (
val: unknown
): val is ControlNetModelFieldInputTemplate =>
zControlNetModelFieldInputTemplate.safeParse(val).success;
export const isControlNetModelFieldValue = (
v: unknown
): v is ControlNetModelFieldValue =>
zControlNetModelFieldValue.safeParse(v).success;
// #endregion
// #region IPAdapterModelField
@ -747,6 +752,10 @@ export const isIPAdapterModelFieldInputTemplate = (
val: unknown
): val is IPAdapterModelFieldInputTemplate =>
zIPAdapterModelFieldInputTemplate.safeParse(val).success;
export const isIPAdapterModelFieldValue = (
val: unknown
): val is IPAdapterModelFieldValue =>
zIPAdapterModelFieldValue.safeParse(val).success;
// #endregion
// #region T2IAdapterField

View File

@ -1,5 +1,6 @@
import { Edge, Node } from 'reactflow';
import type { Edge, Node } from 'reactflow';
import { z } from 'zod';
import { zClassification, zProgressImage } from './common';
import {
zFieldInputInstance,

View File

@ -1,4 +1,5 @@
import { z } from 'zod';
import {
zControlField,
zIPAdapterField,

View File

@ -1,5 +1,5 @@
import { OpenAPIV3_1 } from 'openapi-types';
import {
import type { OpenAPIV3_1 } from 'openapi-types';
import type {
InputFieldJSONSchemaExtra,
InvocationJSONSchemaExtra,
OutputFieldJSONSchemaExtra,

View File

@ -1,5 +1,6 @@
import { FieldType, StatefulFieldType } from 'features/nodes/types/field';
import { FieldTypeV1 } from './workflowV1';
import type { FieldType, StatefulFieldType } from 'features/nodes/types/field';
import type { FieldTypeV1 } from './workflowV1';
/**
* Mapping of V1 field type strings to their *stateful* V2 field type counterparts.

View File

@ -1,4 +1,5 @@
import { z } from 'zod';
import { zFieldIdentifier } from './field';
import { zInvocationNodeData, zNotesNodeData } from './invocation';

View File

@ -1,13 +1,14 @@
import { RootState } from 'app/store/store';
import type { RootState } from 'app/store/store';
import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice';
import { omit } from 'lodash-es';
import {
import type {
CollectInvocation,
ControlField,
ControlNetInvocation,
CoreMetadataInvocation,
NonNullableGraph,
} from 'services/api/types';
import {
CANVAS_COHERENCE_DENOISE_LATENTS,
CONTROL_NET_COLLECT,

View File

@ -1,14 +1,15 @@
import { logger } from 'app/logging/logger';
import { RootState } from 'app/store/store';
import type { RootState } from 'app/store/store';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import {
import type {
DenoiseLatentsInvocation,
ESRGANInvocation,
Edge,
ESRGANInvocation,
LatentsToImageInvocation,
NoiseInvocation,
NonNullableGraph,
} from 'services/api/types';
import {
DENOISE_LATENTS,
DENOISE_LATENTS_HRF,
@ -113,15 +114,15 @@ export const addHrfToGraph = (
): void => {
// Double check hrf is enabled.
if (
!state.generation.hrfEnabled ||
state.config.disabledSDFeatures.includes('hrf') ||
state.generation.model?.model_type === 'onnx' // TODO: ONNX support
!state.hrf.hrfEnabled ||
state.config.disabledSDFeatures.includes('hrf')
) {
return;
}
const log = logger('txt2img');
const { vae, hrfStrength, hrfEnabled, hrfMethod } = state.generation;
const { vae } = state.generation;
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
const isAutoVae = !vae;
const width = state.generation.width;
const height = state.generation.height;
@ -309,7 +310,7 @@ export const addHrfToGraph = (
cfg_scale: originalDenoiseLatentsNode?.cfg_scale,
scheduler: originalDenoiseLatentsNode?.scheduler,
steps: originalDenoiseLatentsNode?.steps,
denoising_start: 1 - state.generation.hrfStrength,
denoising_start: 1 - hrfStrength,
denoising_end: 1,
};
graph.edges.push(

Some files were not shown because too many files have changed in this diff Show More