mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): swath of UI tweaks and improvements
This commit is contained in:
@ -0,0 +1,56 @@
|
|||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isSelected: boolean;
|
||||||
|
isHovered: boolean;
|
||||||
|
};
|
||||||
|
const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
|
||||||
|
const shadow = useMemo(() => {
|
||||||
|
if (isSelected && isHovered) {
|
||||||
|
return 'nodeHoveredSelected.light';
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
return 'nodeSelected.light';
|
||||||
|
}
|
||||||
|
if (isHovered) {
|
||||||
|
return 'nodeHovered.light';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [isHovered, isSelected]);
|
||||||
|
const shadowDark = useMemo(() => {
|
||||||
|
if (isSelected && isHovered) {
|
||||||
|
return 'nodeHoveredSelected.dark';
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
return 'nodeSelected.dark';
|
||||||
|
}
|
||||||
|
if (isHovered) {
|
||||||
|
return 'nodeHovered.dark';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [isHovered, isSelected]);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="selection-box"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineEnd: 0,
|
||||||
|
bottom: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
borderRadius: 'base',
|
||||||
|
opacity: isSelected || isHovered ? 1 : 0.5,
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: '0.1s',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
shadow,
|
||||||
|
_dark: {
|
||||||
|
shadow: shadowDark,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SelectionOverlay);
|
@ -15,6 +15,7 @@ import { BoardDTO } from 'services/api/types';
|
|||||||
import { menuListMotionProps } from 'theme/components/menu';
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||||
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
board?: BoardDTO;
|
board?: BoardDTO;
|
||||||
@ -33,12 +34,16 @@ const BoardContextMenu = ({
|
|||||||
|
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(stateSelector, ({ gallery, system }) => {
|
createSelector(
|
||||||
const isAutoAdd = gallery.autoAddBoardId === board_id;
|
stateSelector,
|
||||||
const isProcessing = system.isProcessing;
|
({ gallery, system }) => {
|
||||||
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
const isAutoAdd = gallery.autoAddBoardId === board_id;
|
||||||
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
|
const isProcessing = system.isProcessing;
|
||||||
}),
|
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
|
||||||
|
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
[board_id]
|
[board_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
h: 'full',
|
h: 'full',
|
||||||
py: 1,
|
py: 2,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
borderBottomRadius: withFooter ? 0 : 'base',
|
borderBottomRadius: withFooter ? 0 : 'base',
|
||||||
}}
|
}}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Icon,
|
Icon,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@ -14,16 +12,14 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import IAITextarea from 'common/components/IAITextarea';
|
|
||||||
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||||
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
|
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
|
||||||
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { isInvocationNodeData } from 'features/nodes/types/types';
|
import { isInvocationNodeData } from 'features/nodes/types/types';
|
||||||
import { ChangeEvent, memo, useCallback } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { FaInfoCircle } from 'react-icons/fa';
|
import { FaInfoCircle } from 'react-icons/fa';
|
||||||
|
import NotesTextarea from './NotesTextarea';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -80,13 +76,29 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
|||||||
const data = useNodeData(nodeId);
|
const data = useNodeData(nodeId);
|
||||||
const nodeTemplate = useNodeTemplate(nodeId);
|
const nodeTemplate = useNodeTemplate(nodeId);
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
if (data?.label && nodeTemplate?.title) {
|
||||||
|
return `${data.label} (${nodeTemplate.title})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.label && !nodeTemplate) {
|
||||||
|
return data.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.label && nodeTemplate) {
|
||||||
|
return nodeTemplate.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown Node';
|
||||||
|
}, [data, nodeTemplate]);
|
||||||
|
|
||||||
if (!isInvocationNodeData(data)) {
|
if (!isInvocationNodeData(data)) {
|
||||||
return <Text sx={{ fontWeight: 600 }}>Unknown Node</Text>;
|
return <Text sx={{ fontWeight: 600 }}>Unknown Node</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ flexDir: 'column' }}>
|
<Flex sx={{ flexDir: 'column' }}>
|
||||||
<Text sx={{ fontWeight: 600 }}>{nodeTemplate?.title}</Text>
|
<Text sx={{ fontWeight: 600 }}>{title}</Text>
|
||||||
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
|
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
|
||||||
{nodeTemplate?.description}
|
{nodeTemplate?.description}
|
||||||
</Text>
|
</Text>
|
||||||
@ -96,29 +108,3 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
TooltipContent.displayName = 'TooltipContent';
|
TooltipContent.displayName = 'TooltipContent';
|
||||||
|
|
||||||
const NotesTextarea = memo(({ nodeId }: { nodeId: string }) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const data = useNodeData(nodeId);
|
|
||||||
const handleNotesChanged = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
|
|
||||||
},
|
|
||||||
[dispatch, nodeId]
|
|
||||||
);
|
|
||||||
if (!isInvocationNodeData(data)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Notes</FormLabel>
|
|
||||||
<IAITextarea
|
|
||||||
value={data?.notes}
|
|
||||||
onChange={handleNotesChanged}
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NotesTextarea.displayName = 'NodesTextarea';
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { FormControl, FormLabel } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import IAITextarea from 'common/components/IAITextarea';
|
||||||
|
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||||
|
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { isInvocationNodeData } from 'features/nodes/types/types';
|
||||||
|
import { ChangeEvent, memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const data = useNodeData(nodeId);
|
||||||
|
const handleNotesChanged = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
|
||||||
|
},
|
||||||
|
[dispatch, nodeId]
|
||||||
|
);
|
||||||
|
if (!isInvocationNodeData(data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Notes</FormLabel>
|
||||||
|
<IAITextarea
|
||||||
|
value={data?.notes}
|
||||||
|
onChange={handleNotesChanged}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(NotesTextarea);
|
@ -0,0 +1,167 @@
|
|||||||
|
import {
|
||||||
|
Editable,
|
||||||
|
EditableInput,
|
||||||
|
EditablePreview,
|
||||||
|
Flex,
|
||||||
|
Tooltip,
|
||||||
|
forwardRef,
|
||||||
|
useEditableControls,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeId: string;
|
||||||
|
fieldName: string;
|
||||||
|
kind: 'input' | 'output';
|
||||||
|
isMissingInput?: boolean;
|
||||||
|
withTooltip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||||
|
const {
|
||||||
|
nodeId,
|
||||||
|
fieldName,
|
||||||
|
kind,
|
||||||
|
isMissingInput = false,
|
||||||
|
withTooltip = false,
|
||||||
|
} = props;
|
||||||
|
const label = useFieldLabel(nodeId, fieldName);
|
||||||
|
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [localTitle, setLocalTitle] = useState(
|
||||||
|
label || fieldTemplateTitle || 'Unknown Field'
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (newTitle: string) => {
|
||||||
|
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field');
|
||||||
|
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
|
||||||
|
},
|
||||||
|
[label, fieldTemplateTitle, dispatch, nodeId, fieldName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback((newTitle: string) => {
|
||||||
|
setLocalTitle(newTitle);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Another component may change the title; sync local title with global state
|
||||||
|
setLocalTitle(label || fieldTemplateTitle || 'Unknown Field');
|
||||||
|
}, [label, fieldTemplateTitle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
withTooltip ? (
|
||||||
|
<FieldTooltipContent
|
||||||
|
nodeId={nodeId}
|
||||||
|
fieldName={fieldName}
|
||||||
|
kind="input"
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||||
|
placement="top"
|
||||||
|
hasArrow
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
gap: 1,
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editable
|
||||||
|
value={localTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
as={Flex}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditablePreview
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
fontWeight: isMissingInput ? 600 : 400,
|
||||||
|
textAlign: 'left',
|
||||||
|
_hover: {
|
||||||
|
fontWeight: '600 !important',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
noOfLines={1}
|
||||||
|
/>
|
||||||
|
<EditableInput
|
||||||
|
className="nodrag"
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
w: 'full',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'base.900',
|
||||||
|
_dark: {
|
||||||
|
color: 'base.100',
|
||||||
|
},
|
||||||
|
_focusVisible: {
|
||||||
|
p: 0,
|
||||||
|
textAlign: 'left',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EditableControls />
|
||||||
|
</Editable>
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default memo(EditableFieldTitle);
|
||||||
|
|
||||||
|
const EditableControls = memo(() => {
|
||||||
|
const { isEditing, getEditButtonProps } = useEditableControls();
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
const { onClick } = getEditButtonProps();
|
||||||
|
if (!onClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick(e);
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
[getEditButtonProps]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
onClick={handleClick}
|
||||||
|
position="absolute"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
cursor="text"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EditableControls.displayName = 'EditableControls';
|
@ -1,16 +1,7 @@
|
|||||||
import {
|
import { Flex, Text, forwardRef } from '@chakra-ui/react';
|
||||||
Editable,
|
|
||||||
EditableInput,
|
|
||||||
EditablePreview,
|
|
||||||
Flex,
|
|
||||||
forwardRef,
|
|
||||||
useEditableControls,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
||||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
import { memo } from 'react';
|
||||||
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -24,31 +15,6 @@ const FieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
const label = useFieldLabel(nodeId, fieldName);
|
const label = useFieldLabel(nodeId, fieldName);
|
||||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [localTitle, setLocalTitle] = useState(
|
|
||||||
label || fieldTemplateTitle || 'Unknown Field'
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
|
||||||
async (newTitle: string) => {
|
|
||||||
if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field');
|
|
||||||
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
|
|
||||||
},
|
|
||||||
[label, fieldTemplateTitle, dispatch, nodeId, fieldName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = useCallback((newTitle: string) => {
|
|
||||||
setLocalTitle(newTitle);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Another component may change the title; sync local title with global state
|
|
||||||
setLocalTitle(label || fieldTemplateTitle || 'Unknown Field');
|
|
||||||
}, [label, fieldTemplateTitle]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -62,82 +28,11 @@ const FieldTitle = forwardRef((props: Props, ref) => {
|
|||||||
w: 'full',
|
w: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Editable
|
<Text sx={{ fontWeight: isMissingInput ? 600 : 400 }}>
|
||||||
value={localTitle}
|
{label || fieldTemplateTitle}
|
||||||
onChange={handleChange}
|
</Text>
|
||||||
onSubmit={handleSubmit}
|
|
||||||
as={Flex}
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
alignItems: 'center',
|
|
||||||
h: 'full',
|
|
||||||
w: 'full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditablePreview
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
fontWeight: isMissingInput ? 600 : 400,
|
|
||||||
textAlign: 'left',
|
|
||||||
_hover: {
|
|
||||||
fontWeight: '600 !important',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
noOfLines={1}
|
|
||||||
/>
|
|
||||||
<EditableInput
|
|
||||||
className="nodrag"
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'base.900',
|
|
||||||
_dark: {
|
|
||||||
color: 'base.100',
|
|
||||||
},
|
|
||||||
_focusVisible: {
|
|
||||||
p: 0,
|
|
||||||
textAlign: 'left',
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<EditableControls />
|
|
||||||
</Editable>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default memo(FieldTitle);
|
export default memo(FieldTitle);
|
||||||
|
|
||||||
const EditableControls = memo(() => {
|
|
||||||
const { isEditing, getEditButtonProps } = useEditableControls();
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
const { onClick } = getEditButtonProps();
|
|
||||||
if (!onClick) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onClick(e);
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
[getEditButtonProps]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isEditing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
onClick={handleClick}
|
|
||||||
position="absolute"
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
top={0}
|
|
||||||
insetInlineStart={0}
|
|
||||||
cursor="text"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
EditableControls.displayName = 'EditableControls';
|
|
||||||
|
@ -34,6 +34,8 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 'Unknown Field';
|
return 'Unknown Field';
|
||||||
|
} else {
|
||||||
|
return fieldTemplate?.title || 'Unknown Field';
|
||||||
}
|
}
|
||||||
}, [field, fieldTemplate]);
|
}, [field, fieldTemplate]);
|
||||||
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { Box, Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
import { Box, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
||||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
|
||||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||||
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
||||||
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
||||||
import { useIsMouseOverField } from 'features/nodes/hooks/useIsMouseOverField';
|
|
||||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
|
||||||
import { PropsWithChildren, memo, useMemo } from 'react';
|
import { PropsWithChildren, memo, useMemo } from 'react';
|
||||||
|
import EditableFieldTitle from './EditableFieldTitle';
|
||||||
import FieldContextMenu from './FieldContextMenu';
|
import FieldContextMenu from './FieldContextMenu';
|
||||||
import FieldHandle from './FieldHandle';
|
import FieldHandle from './FieldHandle';
|
||||||
import FieldTitle from './FieldTitle';
|
|
||||||
import FieldTooltipContent from './FieldTooltipContent';
|
|
||||||
import InputFieldRenderer from './InputFieldRenderer';
|
import InputFieldRenderer from './InputFieldRenderer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -49,11 +45,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
if (fieldTemplate?.fieldKind !== 'input') {
|
if (fieldTemplate?.fieldKind !== 'input') {
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
nodeId={nodeId}
|
|
||||||
fieldName={fieldName}
|
|
||||||
shouldDim={shouldDim}
|
|
||||||
>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
|
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
|
||||||
>
|
>
|
||||||
@ -64,18 +56,14 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
nodeId={nodeId}
|
|
||||||
fieldName={fieldName}
|
|
||||||
shouldDim={shouldDim}
|
|
||||||
>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
isInvalid={isMissingInput}
|
isInvalid={isMissingInput}
|
||||||
isDisabled={isConnected}
|
isDisabled={isConnected}
|
||||||
sx={{
|
sx={{
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
ps: 2,
|
ps: fieldTemplate.input === 'direct' ? 0 : 2,
|
||||||
gap: 2,
|
gap: 2,
|
||||||
h: 'full',
|
h: 'full',
|
||||||
w: 'full',
|
w: 'full',
|
||||||
@ -83,34 +71,24 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
>
|
>
|
||||||
<FieldContextMenu nodeId={nodeId} fieldName={fieldName} kind="input">
|
<FieldContextMenu nodeId={nodeId} fieldName={fieldName} kind="input">
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Tooltip
|
<FormLabel
|
||||||
label={
|
sx={{
|
||||||
<FieldTooltipContent
|
display: 'flex',
|
||||||
nodeId={nodeId}
|
alignItems: 'center',
|
||||||
fieldName={fieldName}
|
mb: 0,
|
||||||
kind="input"
|
px: 1,
|
||||||
/>
|
gap: 2,
|
||||||
}
|
}}
|
||||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
|
||||||
placement="top"
|
|
||||||
hasArrow
|
|
||||||
>
|
>
|
||||||
<FormLabel
|
<EditableFieldTitle
|
||||||
sx={{
|
ref={ref}
|
||||||
mb: 0,
|
nodeId={nodeId}
|
||||||
flexShrink: 0,
|
fieldName={fieldName}
|
||||||
flexGrow: 0,
|
kind="input"
|
||||||
}}
|
isMissingInput={isMissingInput}
|
||||||
>
|
withTooltip
|
||||||
<FieldTitle
|
/>
|
||||||
ref={ref}
|
</FormLabel>
|
||||||
nodeId={nodeId}
|
|
||||||
fieldName={fieldName}
|
|
||||||
kind="input"
|
|
||||||
isMissingInput={isMissingInput}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</FieldContextMenu>
|
</FieldContextMenu>
|
||||||
<Box>
|
<Box>
|
||||||
@ -135,19 +113,12 @@ export default memo(InputField);
|
|||||||
|
|
||||||
type InputFieldWrapperProps = PropsWithChildren<{
|
type InputFieldWrapperProps = PropsWithChildren<{
|
||||||
shouldDim: boolean;
|
shouldDim: boolean;
|
||||||
nodeId: string;
|
|
||||||
fieldName: string;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const InputFieldWrapper = memo(
|
const InputFieldWrapper = memo(
|
||||||
({ shouldDim, nodeId, fieldName, children }: InputFieldWrapperProps) => {
|
({ shouldDim, children }: InputFieldWrapperProps) => {
|
||||||
const { isMouseOverField, handleMouseOver, handleMouseOut } =
|
|
||||||
useIsMouseOverField(nodeId, fieldName);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onMouseOver={handleMouseOver}
|
|
||||||
onMouseOut={handleMouseOut}
|
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
minH: 8,
|
minH: 8,
|
||||||
@ -161,7 +132,6 @@ const InputFieldWrapper = memo(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{/* <SelectionOverlay isSelected={false} isHovered={isMouseOverField} /> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import { Flex, FormControl, FormLabel, Icon, Tooltip } from '@chakra-ui/react';
|
import {
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Icon,
|
||||||
|
Spacer,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||||
import { useIsMouseOverField } from 'features/nodes/hooks/useIsMouseOverField';
|
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice';
|
import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice';
|
||||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { FaInfoCircle, FaTrash } from 'react-icons/fa';
|
import { FaInfoCircle, FaTrash } from 'react-icons/fa';
|
||||||
import FieldTitle from './FieldTitle';
|
import EditableFieldTitle from './EditableFieldTitle';
|
||||||
import FieldTooltipContent from './FieldTooltipContent';
|
import FieldTooltipContent from './FieldTooltipContent';
|
||||||
import InputFieldRenderer from './InputFieldRenderer';
|
import InputFieldRenderer from './InputFieldRenderer';
|
||||||
|
|
||||||
@ -18,8 +25,8 @@ type Props = {
|
|||||||
|
|
||||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isMouseOverField, handleMouseOut, handleMouseOver } =
|
const { isMouseOverNode, handleMouseOut, handleMouseOver } =
|
||||||
useIsMouseOverField(nodeId, fieldName);
|
useMouseOverNode(nodeId);
|
||||||
|
|
||||||
const handleRemoveField = useCallback(() => {
|
const handleRemoveField = useCallback(() => {
|
||||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||||
@ -27,8 +34,8 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
onMouseOver={handleMouseOver}
|
onMouseEnter={handleMouseOver}
|
||||||
onMouseOut={handleMouseOut}
|
onMouseLeave={handleMouseOut}
|
||||||
layerStyle="second"
|
layerStyle="second"
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -42,11 +49,15 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
mb: 0,
|
mb: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
<EditableFieldTitle
|
||||||
|
nodeId={nodeId}
|
||||||
|
fieldName={fieldName}
|
||||||
|
kind="input"
|
||||||
|
/>
|
||||||
|
<Spacer />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={
|
label={
|
||||||
<FieldTooltipContent
|
<FieldTooltipContent
|
||||||
@ -74,7 +85,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{/* <SelectionOverlay isSelected={false} isHovered={isMouseOverField} /> */}
|
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -134,6 +134,7 @@ const MainModelInputFieldComponent = (
|
|||||||
disabled={data.length === 0}
|
disabled={data.length === 0}
|
||||||
onChange={handleChangeModel}
|
onChange={handleChangeModel}
|
||||||
sx={{
|
sx={{
|
||||||
|
width: '100%',
|
||||||
'.mantine-Select-dropdown': {
|
'.mantine-Select-dropdown': {
|
||||||
width: '16rem !important',
|
width: '16rem !important',
|
||||||
},
|
},
|
||||||
|
@ -4,9 +4,9 @@ import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSe
|
|||||||
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
|
||||||
import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice';
|
import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import {
|
import {
|
||||||
|
FieldComponentProps,
|
||||||
VaeModelInputFieldTemplate,
|
VaeModelInputFieldTemplate,
|
||||||
VaeModelInputFieldValue,
|
VaeModelInputFieldValue,
|
||||||
FieldComponentProps,
|
|
||||||
} from 'features/nodes/types/types';
|
} from 'features/nodes/types/types';
|
||||||
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
|
||||||
import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam';
|
import { modelIdToVAEModelParam } from 'features/parameters/util/modelIdToVAEModelParam';
|
||||||
@ -88,10 +88,6 @@ const VaeModelInputFieldComponent = (
|
|||||||
className="nowheel nodrag"
|
className="nowheel nodrag"
|
||||||
itemComponent={IAIMantineSelectItemWithTooltip}
|
itemComponent={IAIMantineSelectItemWithTooltip}
|
||||||
tooltip={selectedVaeModel?.description}
|
tooltip={selectedVaeModel?.description}
|
||||||
label={
|
|
||||||
selectedVaeModel?.base_model &&
|
|
||||||
MODEL_TYPE_MAP[selectedVaeModel?.base_model]
|
|
||||||
}
|
|
||||||
value={selectedVaeModel?.id ?? 'default'}
|
value={selectedVaeModel?.id ?? 'default'}
|
||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -27,9 +27,11 @@ const NodeTitle = ({ nodeId, title }: Props) => {
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (newTitle: string) => {
|
async (newTitle: string) => {
|
||||||
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
|
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
|
||||||
setLocalTitle(newTitle || title || 'Problem Setting Title');
|
setLocalTitle(
|
||||||
|
newTitle || title || templateTitle || 'Problem Setting Title'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[nodeId, dispatch, title]
|
[dispatch, nodeId, title, templateTitle]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback((newTitle: string) => {
|
const handleChange = useCallback((newTitle: string) => {
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||||
|
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
import {
|
import {
|
||||||
DRAG_HANDLE_CLASSNAME,
|
DRAG_HANDLE_CLASSNAME,
|
||||||
NODE_WIDTH,
|
NODE_WIDTH,
|
||||||
@ -23,6 +25,8 @@ type NodeWrapperProps = PropsWithChildren & {
|
|||||||
|
|
||||||
const NodeWrapper = (props: NodeWrapperProps) => {
|
const NodeWrapper = (props: NodeWrapperProps) => {
|
||||||
const { nodeId, width, children, selected } = props;
|
const { nodeId, width, children, selected } = props;
|
||||||
|
const { isMouseOverNode, handleMouseOut, handleMouseOver } =
|
||||||
|
useMouseOverNode(nodeId);
|
||||||
|
|
||||||
const selectIsInProgress = useMemo(
|
const selectIsInProgress = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -36,25 +40,16 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
|
|
||||||
const isInProgress = useAppSelector(selectIsInProgress);
|
const isInProgress = useAppSelector(selectIsInProgress);
|
||||||
|
|
||||||
const [
|
const [nodeInProgressLight, nodeInProgressDark, shadowsXl, shadowsBase] =
|
||||||
nodeSelectedLight,
|
useToken('shadows', [
|
||||||
nodeSelectedDark,
|
'nodeInProgress.light',
|
||||||
nodeInProgressLight,
|
'nodeInProgress.dark',
|
||||||
nodeInProgressDark,
|
'shadows.xl',
|
||||||
shadowsXl,
|
'shadows.base',
|
||||||
shadowsBase,
|
]);
|
||||||
] = useToken('shadows', [
|
|
||||||
'nodeSelected.light',
|
|
||||||
'nodeSelected.dark',
|
|
||||||
'nodeInProgress.light',
|
|
||||||
'nodeInProgress.dark',
|
|
||||||
'shadows.xl',
|
|
||||||
'shadows.base',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const selectedShadow = useColorModeValue(nodeSelectedLight, nodeSelectedDark);
|
|
||||||
const inProgressShadow = useColorModeValue(
|
const inProgressShadow = useColorModeValue(
|
||||||
nodeInProgressLight,
|
nodeInProgressLight,
|
||||||
nodeInProgressDark
|
nodeInProgressDark
|
||||||
@ -69,6 +64,8 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onMouseEnter={handleMouseOver}
|
||||||
|
onMouseLeave={handleMouseOut}
|
||||||
className={DRAG_HANDLE_CLASSNAME}
|
className={DRAG_HANDLE_CLASSNAME}
|
||||||
sx={{
|
sx={{
|
||||||
h: 'full',
|
h: 'full',
|
||||||
@ -77,11 +74,6 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
w: width ?? NODE_WIDTH,
|
w: width ?? NODE_WIDTH,
|
||||||
transitionProperty: 'common',
|
transitionProperty: 'common',
|
||||||
transitionDuration: '0.1s',
|
transitionDuration: '0.1s',
|
||||||
shadow: selected
|
|
||||||
? isInProgress
|
|
||||||
? undefined
|
|
||||||
: selectedShadow
|
|
||||||
: undefined,
|
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
opacity,
|
opacity,
|
||||||
}}
|
}}
|
||||||
@ -116,6 +108,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
|
<NodeSelectionOverlay isSelected={selected} isHovered={isMouseOverNode} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { InvocationTemplate, NodeData } from 'features/nodes/types/types';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
|
||||||
|
import NodeTitle from '../../flow/nodes/common/NodeTitle';
|
||||||
|
import ScrollableContent from '../ScrollableContent';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const lastSelectedNodeId =
|
||||||
|
nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
||||||
|
|
||||||
|
const lastSelectedNode = nodes.nodes.find(
|
||||||
|
(node) => node.id === lastSelectedNodeId
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode
|
||||||
|
? nodes.nodeTemplates[lastSelectedNode.data.type]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: lastSelectedNode?.data,
|
||||||
|
template: lastSelectedNodeTemplate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const InspectorDetailsTab = () => {
|
||||||
|
const { data, template } = useAppSelector(selector);
|
||||||
|
|
||||||
|
if (!template || !data) {
|
||||||
|
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Content data={data} template={template} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(InspectorDetailsTab);
|
||||||
|
|
||||||
|
const Content = (props: { data: NodeData; template: InvocationTemplate }) => {
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollableContent>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDir: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
p: 1,
|
||||||
|
gap: 2,
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NodeTitle nodeId={data.id} />
|
||||||
|
<NotesTextarea nodeId={data.id} />
|
||||||
|
</Flex>
|
||||||
|
</ScrollableContent>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -4,12 +4,13 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
|
import { isInvocationNode } from 'features/nodes/types/types';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import ImageOutputPreview from './outputs/ImageOutputPreview';
|
import { ImageOutput } from 'services/api/types';
|
||||||
import ScrollableContent from '../ScrollableContent';
|
|
||||||
import { AnyResult } from 'services/events/types';
|
import { AnyResult } from 'services/events/types';
|
||||||
import StringOutputPreview from './outputs/StringOutputPreview';
|
import ScrollableContent from '../ScrollableContent';
|
||||||
import NumberOutputPreview from './outputs/NumberOutputPreview';
|
import ImageOutputPreview from './outputs/ImageOutputPreview';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
stateSelector,
|
stateSelector,
|
||||||
@ -21,11 +22,16 @@ const selector = createSelector(
|
|||||||
(node) => node.id === lastSelectedNodeId
|
(node) => node.id === lastSelectedNodeId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode
|
||||||
|
? nodes.nodeTemplates[lastSelectedNode.data.type]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const nes =
|
const nes =
|
||||||
nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__'];
|
nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: lastSelectedNode,
|
node: lastSelectedNode,
|
||||||
|
template: lastSelectedNodeTemplate,
|
||||||
nes,
|
nes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -33,9 +39,9 @@ const selector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const InspectorOutputsTab = () => {
|
const InspectorOutputsTab = () => {
|
||||||
const { node, nes } = useAppSelector(selector);
|
const { node, template, nes } = useAppSelector(selector);
|
||||||
|
|
||||||
if (!node || !nes) {
|
if (!node || !nes || !isInvocationNode(node)) {
|
||||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,33 +69,16 @@ const InspectorOutputsTab = () => {
|
|||||||
w: 'full',
|
w: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nes.outputs.map((result, i) => {
|
{template?.outputType === 'image_output' ? (
|
||||||
if (result.type === 'string_output') {
|
nes.outputs.map((result, i) => (
|
||||||
return (
|
<ImageOutputPreview
|
||||||
<StringOutputPreview key={getKey(result, i)} output={result} />
|
key={getKey(result, i)}
|
||||||
);
|
output={result as ImageOutput}
|
||||||
}
|
/>
|
||||||
if (result.type === 'float_output') {
|
))
|
||||||
return (
|
) : (
|
||||||
<NumberOutputPreview key={getKey(result, i)} output={result} />
|
<DataViewer data={nes.outputs} label="Node Outputs" />
|
||||||
);
|
)}
|
||||||
}
|
|
||||||
if (result.type === 'integer_output') {
|
|
||||||
return (
|
|
||||||
<NumberOutputPreview key={getKey(result, i)} output={result} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (result.type === 'image_output') {
|
|
||||||
return (
|
|
||||||
<ImageOutputPreview key={getKey(result, i)} output={result} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<pre key={getKey(result, i)}>
|
|
||||||
{JSON.stringify(result, null, 2)}
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</ScrollableContent>
|
</ScrollableContent>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -10,6 +10,7 @@ import { memo } from 'react';
|
|||||||
import InspectorDataTab from './InspectorDataTab';
|
import InspectorDataTab from './InspectorDataTab';
|
||||||
import InspectorOutputsTab from './InspectorOutputsTab';
|
import InspectorOutputsTab from './InspectorOutputsTab';
|
||||||
import InspectorTemplateTab from './InspectorTemplateTab';
|
import InspectorTemplateTab from './InspectorTemplateTab';
|
||||||
|
// import InspectorDetailsTab from './InspectorDetailsTab';
|
||||||
|
|
||||||
const InspectorPanel = () => {
|
const InspectorPanel = () => {
|
||||||
return (
|
return (
|
||||||
@ -29,12 +30,16 @@ const InspectorPanel = () => {
|
|||||||
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
|
{/* <Tab>Details</Tab> */}
|
||||||
<Tab>Outputs</Tab>
|
<Tab>Outputs</Tab>
|
||||||
<Tab>Data</Tab>
|
<Tab>Data</Tab>
|
||||||
<Tab>Template</Tab>
|
<Tab>Template</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
|
{/* <TabPanel>
|
||||||
|
<InspectorDetailsTab />
|
||||||
|
</TabPanel> */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<InspectorOutputsTab />
|
<InspectorOutputsTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { FloatOutput, IntegerOutput } from 'services/api/types';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
output: IntegerOutput | FloatOutput;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NumberOutputPreview = ({ output }: Props) => {
|
|
||||||
return <Text>{output.value}</Text>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(NumberOutputPreview);
|
|
@ -1,13 +0,0 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { StringOutput } from 'services/api/types';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
output: StringOutput;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StringOutputPreview = ({ output }: Props) => {
|
|
||||||
return <Text>{output.value}</Text>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(StringOutputPreview);
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { mouseOverNodeChanged } from '../store/nodesSlice';
|
||||||
|
|
||||||
|
export const useMouseOverNode = (nodeId: string) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => nodes.mouseOverNode === nodeId,
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMouseOverNode = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
!isMouseOverNode && dispatch(mouseOverNodeChanged(nodeId));
|
||||||
|
}, [dispatch, nodeId, isMouseOverNode]);
|
||||||
|
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
isMouseOverNode && dispatch(mouseOverNodeChanged(null));
|
||||||
|
}, [dispatch, isMouseOverNode]);
|
||||||
|
|
||||||
|
return { isMouseOverNode, handleMouseOver, handleMouseOut };
|
||||||
|
};
|
@ -102,6 +102,7 @@ export const initialNodesState: NodesState = {
|
|||||||
nodeExecutionStates: {},
|
nodeExecutionStates: {},
|
||||||
viewport: { x: 0, y: 0, zoom: 1 },
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
mouseOverField: null,
|
mouseOverField: null,
|
||||||
|
mouseOverNode: null,
|
||||||
nodesToCopy: [],
|
nodesToCopy: [],
|
||||||
edgesToCopy: [],
|
edgesToCopy: [],
|
||||||
selectionMode: SelectionMode.Partial,
|
selectionMode: SelectionMode.Partial,
|
||||||
@ -665,6 +666,9 @@ const nodesSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
state.mouseOverField = action.payload;
|
state.mouseOverField = action.payload;
|
||||||
},
|
},
|
||||||
|
mouseOverNodeChanged: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.mouseOverNode = action.payload;
|
||||||
|
},
|
||||||
selectedAll: (state) => {
|
selectedAll: (state) => {
|
||||||
state.nodes = applyNodeChanges(
|
state.nodes = applyNodeChanges(
|
||||||
state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })),
|
state.nodes.map((n) => ({ id: n.id, type: 'select', selected: true })),
|
||||||
@ -887,6 +891,7 @@ export const {
|
|||||||
selectionModeChanged,
|
selectionModeChanged,
|
||||||
nodeEmbedWorkflowChanged,
|
nodeEmbedWorkflowChanged,
|
||||||
nodeIsIntermediateChanged,
|
nodeIsIntermediateChanged,
|
||||||
|
mouseOverNodeChanged,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -35,6 +35,7 @@ export type NodesState = {
|
|||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
mouseOverField: FieldIdentifier | null;
|
mouseOverField: FieldIdentifier | null;
|
||||||
|
mouseOverNode: string | null;
|
||||||
nodesToCopy: Node<NodeData>[];
|
nodesToCopy: Node<NodeData>[];
|
||||||
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
||||||
isAddNodePopoverOpen: boolean;
|
isAddNodePopoverOpen: boolean;
|
||||||
|
@ -108,8 +108,16 @@ export const theme: ThemeOverride = {
|
|||||||
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
|
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
|
||||||
},
|
},
|
||||||
nodeSelected: {
|
nodeSelected: {
|
||||||
light: `0 0 0 2px var(--invokeai-colors-accent-400)`,
|
light: `0 0 0 3px var(--invokeai-colors-accent-400)`,
|
||||||
dark: `0 0 0 2px var(--invokeai-colors-accent-500)`,
|
dark: `0 0 0 3px var(--invokeai-colors-accent-500)`,
|
||||||
|
},
|
||||||
|
nodeHovered: {
|
||||||
|
light: `0 0 0 2px var(--invokeai-colors-accent-500)`,
|
||||||
|
dark: `0 0 0 2px var(--invokeai-colors-accent-400)`,
|
||||||
|
},
|
||||||
|
nodeHoveredSelected: {
|
||||||
|
light: `0 0 0 3px var(--invokeai-colors-accent-500)`,
|
||||||
|
dark: `0 0 0 3px var(--invokeai-colors-accent-400)`,
|
||||||
},
|
},
|
||||||
nodeInProgress: {
|
nodeInProgress: {
|
||||||
light:
|
light:
|
||||||
|
Reference in New Issue
Block a user