feat(ui): node buttons and shadow

This commit is contained in:
psychedelicious 2023-08-23 18:29:30 +10:00
parent 2ec8fd3dc7
commit 0cb886b915
19 changed files with 215 additions and 148 deletions

View File

@ -718,12 +718,12 @@
"swapSizes": "Swap Sizes"
},
"nodes": {
"reloadSchema": "Reload Node Templates",
"saveGraph": "Save Workflow",
"loadGraph": "Load Workflow",
"resetGraph": "Reset Workflow",
"clearGraph": "Reset Graph",
"clearGraphDesc": "Are you sure you want to clear all nodes?",
"reloadNodeTemplates": "Reload Node Templates",
"saveWorkflow": "Save Workflow",
"loadWorkflow": "Load Workflow",
"resetWorkflow": "Reset Workflow",
"resetWorkflowDesc": "Are you sure you want to reset this workflow?",
"resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out",
"fitViewportNodes": "Fit View",

View File

@ -24,34 +24,40 @@ type NodeWrapperProps = PropsWithChildren & {
const NodeWrapper = (props: NodeWrapperProps) => {
const { nodeId, width, children, selected } = props;
const selectNodeExecutionState = useMemo(
const selectIsInProgress = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => nodes.nodeExecutionStates[nodeId]
({ nodes }) =>
nodes.nodeExecutionStates[nodeId]?.status === NodeStatus.IN_PROGRESS
),
[nodeId]
);
const nodeExecutionState = useAppSelector(selectNodeExecutionState);
const isInProgress = useAppSelector(selectIsInProgress);
const [
nodeSelectedOutlineLight,
nodeSelectedOutlineDark,
nodeSelectedLight,
nodeSelectedDark,
nodeInProgressLight,
nodeInProgressDark,
shadowsXl,
shadowsBase,
] = useToken('shadows', [
'nodeSelectedOutline.light',
'nodeSelectedOutline.dark',
'nodeSelected.light',
'nodeSelected.dark',
'nodeInProgress.light',
'nodeInProgress.dark',
'shadows.xl',
'shadows.base',
]);
const dispatch = useAppDispatch();
const shadow = useColorModeValue(
nodeSelectedOutlineLight,
nodeSelectedOutlineDark
const selectedShadow = useColorModeValue(nodeSelectedLight, nodeSelectedDark);
const inProgressShadow = useColorModeValue(
nodeInProgressLight,
nodeInProgressDark
);
const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
@ -71,24 +77,9 @@ const NodeWrapper = (props: NodeWrapperProps) => {
w: width ?? NODE_WIDTH,
transitionProperty: 'common',
transitionDuration: '0.1s',
shadow: selected
? nodeExecutionState?.status === NodeStatus.IN_PROGRESS
? undefined
: shadow
: undefined,
shadow: selected ? selectedShadow : undefined,
cursor: 'grab',
opacity,
borderWidth: 2,
borderColor:
nodeExecutionState?.status === NodeStatus.IN_PROGRESS
? 'warning.300'
: 'base.200',
_dark: {
borderColor:
nodeExecutionState?.status === NodeStatus.IN_PROGRESS
? 'warning.500'
: 'base.900',
},
}}
>
<Box
@ -104,6 +95,22 @@ const NodeWrapper = (props: NodeWrapperProps) => {
zIndex: -1,
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
insetInlineEnd: 0,
bottom: 0,
insetInlineStart: 0,
borderRadius: 'md',
pointerEvents: 'none',
transitionProperty: 'common',
transitionDuration: 'normal',
opacity: 0.7,
shadow: isInProgress ? inProgressShadow : undefined,
zIndex: -1,
}}
/>
{children}
</Box>
);

View File

@ -1,20 +1,19 @@
import { ButtonGroup, Tooltip } from '@chakra-ui/react';
import { ButtonGroup } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
FaExpand,
// FaInfo,
FaMapMarkerAlt,
FaMinus,
FaPlus,
} from 'react-icons/fa';
import { FaMagnifyingGlassMinus, FaMagnifyingGlassPlus } from 'react-icons/fa6';
import { useReactFlow } from 'reactflow';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/nodesSlice';
const ViewportControls = () => {
const { t } = useTranslation();
@ -49,27 +48,24 @@ const ViewportControls = () => {
return (
<ButtonGroup isAttached orientation="vertical">
<Tooltip label={t('nodes.zoomInNodes')}>
<IAIIconButton
aria-label="Zoom in "
onClick={handleClickedZoomIn}
icon={<FaPlus />}
/>
</Tooltip>
<Tooltip label={t('nodes.zoomOutNodes')}>
<IAIIconButton
aria-label="Zoom out"
onClick={handleClickedZoomOut}
icon={<FaMinus />}
/>
</Tooltip>
<Tooltip label={t('nodes.fitViewportNodes')}>
<IAIIconButton
aria-label="Fit viewport"
onClick={handleClickedFitView}
icon={<FaExpand />}
/>
</Tooltip>
<IAIIconButton
tooltip={t('nodes.zoomInNodes')}
aria-label={t('nodes.zoomInNodes')}
onClick={handleClickedZoomIn}
icon={<FaMagnifyingGlassPlus />}
/>
<IAIIconButton
tooltip={t('nodes.zoomOutNodes')}
aria-label={t('nodes.zoomOutNodes')}
onClick={handleClickedZoomOut}
icon={<FaMagnifyingGlassMinus />}
/>
<IAIIconButton
tooltip={t('nodes.fitViewportNodes')}
aria-label={t('nodes.fitViewportNodes')}
onClick={handleClickedFitView}
icon={<FaExpand />}
/>
{/* <Tooltip
label={
shouldShowFieldTypeLegend
@ -84,20 +80,21 @@ const ViewportControls = () => {
icon={<FaInfo />}
/>
</Tooltip> */}
<Tooltip
label={
<IAIIconButton
tooltip={
shouldShowMinimapPanel
? t('nodes.hideMinimapnodes')
: t('nodes.showMinimapnodes')
}
>
<IAIIconButton
aria-label="Toggle minimap"
isChecked={shouldShowMinimapPanel}
onClick={handleClickedToggleMiniMapPanel}
icon={<FaMapMarkerAlt />}
/>
</Tooltip>
aria-label={
shouldShowMinimapPanel
? t('nodes.hideMinimapnodes')
: t('nodes.showMinimapnodes')
}
isChecked={shouldShowMinimapPanel}
onClick={handleClickedToggleMiniMapPanel}
icon={<FaMapMarkerAlt />}
/>
</ButtonGroup>
);
};

View File

@ -2,9 +2,11 @@ import { FileButton } from '@mantine/core';
import IAIIconButton from 'common/components/IAIIconButton';
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
const LoadWorkflowButton = () => {
const { t } = useTranslation();
const resetRef = useRef<() => void>(null);
const loadWorkflowFromFile = useLoadWorkflowFromFile();
return (
@ -16,8 +18,8 @@ const LoadWorkflowButton = () => {
{(props) => (
<IAIIconButton
icon={<FaUpload />}
tooltip="Load Workflow"
aria-label="Load Workflow"
tooltip={t('nodes.loadWorkflow')}
aria-label={t('nodes.loadWorkflow')}
{...props}
/>
)}

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { FaSyncAlt } from 'react-icons/fa';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
const ReloadSchemaButton = () => {
const ReloadNodeTemplatesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@ -16,13 +16,13 @@ const ReloadSchemaButton = () => {
return (
<IAIButton
leftIcon={<FaSyncAlt />}
tooltip={t('nodes.reloadSchema')}
aria-label={t('nodes.reloadSchema')}
tooltip={t('nodes.reloadNodeTemplates')}
aria-label={t('nodes.reloadNodeTemplates')}
onClick={handleReloadSchema}
>
{t('nodes.reloadSchema')}
{t('nodes.reloadNodeTemplates')}
</IAIButton>
);
};
export default memo(ReloadSchemaButton);
export default memo(ReloadNodeTemplatesButton);

View File

@ -6,7 +6,10 @@ import {
AlertDialogHeader,
AlertDialogOverlay,
Button,
Divider,
Flex,
Text,
VStack,
useDisclosure,
} from '@chakra-ui/react';
import { RootState } from 'app/store/store';
@ -19,7 +22,7 @@ import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
const ClearGraphButton = () => {
const ResetWorkflowButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
@ -48,8 +51,8 @@ const ClearGraphButton = () => {
<>
<IAIIconButton
icon={<FaTrash />}
tooltip={t('nodes.clearGraph')}
aria-label={t('nodes.clearGraph')}
tooltip={t('nodes.resetWorkflow')}
aria-label={t('nodes.resetWorkflow')}
onClick={onOpen}
isDisabled={!nodesCount}
/>
@ -64,18 +67,21 @@ const ClearGraphButton = () => {
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('nodes.clearGraph')}
{t('nodes.resetWorkflow')}
</AlertDialogHeader>
<AlertDialogBody>
<Text>{t('nodes.clearGraphDesc')}</Text>
<AlertDialogBody py={4}>
<Flex flexDir="column" gap={2}>
<Text>{t('nodes.resetWorkflowDesc')}</Text>
<Text variant="subtext">{t('nodes.resetWorkflowDesc2')}</Text>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml={3} onClick={handleConfirmClear}>
<Button colorScheme="error" ml={3} onClick={handleConfirmClear}>
{t('common.accept')}
</Button>
</AlertDialogFooter>
@ -85,4 +91,4 @@ const ClearGraphButton = () => {
);
};
export default memo(ClearGraphButton);
export default memo(ResetWorkflowButton);

View File

@ -0,0 +1,29 @@
import IAIIconButton from 'common/components/IAIIconButton';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSave } from 'react-icons/fa';
const SaveWorkflowButton = () => {
const { t } = useTranslation();
const workflow = useWorkflow();
const handleSave = 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 (
<IAIIconButton
icon={<FaSave />}
tooltip={t('nodes.saveWorkflow')}
aria-label={t('nodes.saveWorkflow')}
onClick={handleSave}
/>
);
};
export default memo(SaveWorkflowButton);

View File

@ -1,16 +1,8 @@
import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { Panel } from 'reactflow';
import WorkflowEditorControls from './WorkflowEditorControls';
const TopCenterPanel = () => {
return (
<Panel position="top-center">
<Flex gap={2}>
<WorkflowEditorControls />
</Flex>
</Panel>
);
return <Panel position="top-center">{null}</Panel>;
};
export default memo(TopCenterPanel);

View File

@ -1,15 +1,17 @@
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import InvokeButton from 'features/parameters/components/ProcessButtons/InvokeButton';
import { memo } from 'react';
import ClearGraphButton from './ClearGraphButton';
import ResetWorkflowButton from './ResetWorkflowButton';
import LoadWorkflowButton from './LoadWorkflowButton';
import SaveWorkflowButton from './SaveWorkflowButton';
const WorkflowEditorControls = () => {
return (
<>
<InvokeButton />
<CancelButton />
<ClearGraphButton />
<ResetWorkflowButton />
<SaveWorkflowButton />
<LoadWorkflowButton />
</>
);

View File

@ -1,8 +1,8 @@
import { Tooltip } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { Panel } from 'reactflow';
const TopLeftPanel = () => {
@ -14,15 +14,12 @@ const TopLeftPanel = () => {
return (
<Panel position="top-left">
<Tooltip label="Add New Node (Shift+A, Space)">
<IAIButton
size="sm"
aria-label="Add Node"
onClick={handleOpenAddNodePopover}
>
Add Node
</IAIButton>
</Tooltip>
<IAIIconButton
tooltip="Add Node (Shift+A, Space)"
aria-label="Add Node"
icon={<FaPlus />}
onClick={handleOpenAddNodePopover}
/>
</Panel>
);
};

View File

@ -27,7 +27,7 @@ import {
import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa';
import { SelectionMode } from 'reactflow';
import ReloadSchemaButton from '../TopCenterPanel/ReloadSchemaButton';
import ReloadNodeTemplatesButton from '../TopCenterPanel/ReloadSchemaButton';
const formLabelProps: FormLabelProps = {
fontWeight: 600,
@ -163,7 +163,7 @@ const WorkflowEditorSettings = () => {
label="Validate Connections and Graph"
helperText="Prevent invalid connections from being made, and invalid graphs from being invoked"
/>
<ReloadSchemaButton />
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>

View File

@ -1,26 +1,10 @@
import { Flex } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
import { memo, useMemo } from 'react';
import { useDebounce } from 'use-debounce';
const useWatchWorkflow = () => {
const nodes = useAppSelector((state: RootState) => state.nodes);
const [debouncedNodes] = useDebounce(nodes, 300);
const workflow = useMemo(
() => buildWorkflow(debouncedNodes),
[debouncedNodes]
);
return {
workflow,
};
};
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { memo } from 'react';
const WorkflowJSONTab = () => {
const { workflow } = useWatchWorkflow();
const workflow = useWorkflow();
return (
<Flex

View File

@ -10,6 +10,7 @@ import { memo } from 'react';
import WorkflowGeneralTab from './WorkflowGeneralTab';
import WorkflowLinearTab from './WorkflowLinearTab';
import WorkflowJSONTab from './WorkflowJSONTab';
import WorkflowEditorControls from '../../flow/panels/TopCenterPanel/WorkflowEditorControls';
const WorkflowPanel = () => {
return (
@ -21,8 +22,12 @@ const WorkflowPanel = () => {
h: 'full',
borderRadius: 'base',
p: 4,
gap: 2,
}}
>
<Flex gap={2}>
<WorkflowEditorControls />
</Flex>
<Tabs
variant="line"
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}

View File

@ -0,0 +1,16 @@
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
import { useMemo } from 'react';
import { useDebounce } from 'use-debounce';
export const useWorkflow = () => {
const nodes = useAppSelector((state: RootState) => state.nodes);
const [debouncedNodes] = useDebounce(nodes, 300);
const workflow = useMemo(
() => buildWorkflow(debouncedNodes),
[debouncedNodes]
);
return workflow;
};

View File

@ -600,9 +600,9 @@ const nodesSlice = createSlice({
state.workflow.contact = action.payload;
},
workflowLoaded: (state, action: PayloadAction<Workflow>) => {
// TODO: validation
const { nodes, edges, ...workflow } = action.payload;
state.workflow = workflow;
state.nodes = applyNodeChanges(
nodes.map((node) => ({
item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` },
@ -614,6 +614,16 @@ const nodesSlice = createSlice({
edges.map((edge) => ({ item: edge, type: 'add' })),
[]
);
state.nodeExecutionStates = nodes.reduce<
Record<string, NodeExecutionState>
>((acc, node) => {
acc[node.id] = {
nodeId: node.id,
...initialNodeExecutionState,
};
return acc;
}, {});
},
workflowReset: (state) => {
state.workflow = cloneDeep(initialWorkflow);

View File

@ -807,7 +807,7 @@ export const zSemVer = z.string().refine((val) => {
export type SemVer = z.infer<typeof zSemVer>;
export const zWorkflow = z.object({
name: z.string().trim().min(1),
name: z.string(),
author: z.string(),
description: z.string(),
version: z.string(),

View File

@ -27,6 +27,7 @@ import { MdCancel, MdCancelScheduleSend } from 'react-icons/md';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { sessionCanceled } from 'services/api/thunks/session';
import IAIButton from 'common/components/IAIButton';
const cancelButtonSelector = createSelector(
systemSelector,
@ -49,15 +50,14 @@ const cancelButtonSelector = createSelector(
}
);
interface CancelButtonProps {
type Props = Omit<ButtonProps, 'aria-label'> & {
btnGroupWidth?: string | number;
}
asIconButton?: boolean;
};
const CancelButton = (
props: CancelButtonProps & Omit<ButtonProps, 'aria-label'>
) => {
const CancelButton = (props: Props) => {
const dispatch = useAppDispatch();
const { btnGroupWidth = 'auto', ...rest } = props;
const { btnGroupWidth = 'auto', asIconButton = false, ...rest } = props;
const {
isProcessing,
isConnected,
@ -124,16 +124,31 @@ const CancelButton = (
return (
<ButtonGroup isAttached width={btnGroupWidth}>
<IAIIconButton
icon={cancelIcon}
tooltip={cancelLabel}
aria-label={cancelLabel}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
colorScheme="error"
id="cancel-button"
{...rest}
/>
{asIconButton ? (
<IAIIconButton
icon={cancelIcon}
tooltip={cancelLabel}
aria-label={cancelLabel}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
colorScheme="error"
id="cancel-button"
{...rest}
/>
) : (
<IAIButton
leftIcon={cancelIcon}
tooltip={cancelLabel}
aria-label={cancelLabel}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
colorScheme="error"
id="cancel-button"
{...rest}
>
Cancel
</IAIButton>
)}
<Menu closeOnSelect={false}>
<MenuButton
as={IAIIconButton}

View File

@ -56,7 +56,7 @@ const FloatingSidePanelButtons = ({
icon={<FaSlidersH />}
/>
<InvokeButton asIconButton sx={floatingButtonStyles} />
<CancelButton sx={floatingButtonStyles} />
<CancelButton sx={floatingButtonStyles} asIconButton />
</Flex>
</Portal>
);

View File

@ -107,10 +107,15 @@ export const theme: ThemeOverride = {
'0px 0px 0px 1px var(--invokeai-colors-base-150), 0px 0px 0px 3px var(--invokeai-colors-accent-500)',
dark: '0px 0px 0px 1px var(--invokeai-colors-base-900), 0px 0px 0px 3px var(--invokeai-colors-accent-400)',
},
nodeSelectedOutline: {
nodeSelected: {
light: `0 0 0 2px var(--invokeai-colors-accent-400)`,
dark: `0 0 0 2px var(--invokeai-colors-accent-500)`,
},
nodeInProgress: {
light:
'0 0 4px 2px var(--invokeai-colors-accent-500), 0 0 15px 4px var(--invokeai-colors-accent-600)',
dark: '0 0 4px 2px var(--invokeai-colors-accent-400), 0 0 15px 4px var(--invokeai-colors-accent-400)',
},
},
colors: InvokeAIColors,
components: {