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

View File

@ -24,34 +24,40 @@ type NodeWrapperProps = PropsWithChildren & {
const NodeWrapper = (props: NodeWrapperProps) => { const NodeWrapper = (props: NodeWrapperProps) => {
const { nodeId, width, children, selected } = props; const { nodeId, width, children, selected } = props;
const selectNodeExecutionState = useMemo( const selectIsInProgress = useMemo(
() => () =>
createSelector( createSelector(
stateSelector, stateSelector,
({ nodes }) => nodes.nodeExecutionStates[nodeId] ({ nodes }) =>
nodes.nodeExecutionStates[nodeId]?.status === NodeStatus.IN_PROGRESS
), ),
[nodeId] [nodeId]
); );
const nodeExecutionState = useAppSelector(selectNodeExecutionState); const isInProgress = useAppSelector(selectIsInProgress);
const [ const [
nodeSelectedOutlineLight, nodeSelectedLight,
nodeSelectedOutlineDark, nodeSelectedDark,
nodeInProgressLight,
nodeInProgressDark,
shadowsXl, shadowsXl,
shadowsBase, shadowsBase,
] = useToken('shadows', [ ] = useToken('shadows', [
'nodeSelectedOutline.light', 'nodeSelected.light',
'nodeSelectedOutline.dark', 'nodeSelected.dark',
'nodeInProgress.light',
'nodeInProgress.dark',
'shadows.xl', 'shadows.xl',
'shadows.base', 'shadows.base',
]); ]);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const shadow = useColorModeValue( const selectedShadow = useColorModeValue(nodeSelectedLight, nodeSelectedDark);
nodeSelectedOutlineLight, const inProgressShadow = useColorModeValue(
nodeSelectedOutlineDark nodeInProgressLight,
nodeInProgressDark
); );
const opacity = useAppSelector((state) => state.nodes.nodeOpacity); const opacity = useAppSelector((state) => state.nodes.nodeOpacity);
@ -71,24 +77,9 @@ const NodeWrapper = (props: NodeWrapperProps) => {
w: width ?? NODE_WIDTH, w: width ?? NODE_WIDTH,
transitionProperty: 'common', transitionProperty: 'common',
transitionDuration: '0.1s', transitionDuration: '0.1s',
shadow: selected shadow: selected ? selectedShadow : undefined,
? nodeExecutionState?.status === NodeStatus.IN_PROGRESS
? undefined
: shadow
: undefined,
cursor: 'grab', cursor: 'grab',
opacity, 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 <Box
@ -104,6 +95,22 @@ const NodeWrapper = (props: NodeWrapperProps) => {
zIndex: -1, 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} {children}
</Box> </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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
FaExpand, FaExpand,
// FaInfo, // FaInfo,
FaMapMarkerAlt, FaMapMarkerAlt,
FaMinus,
FaPlus,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { FaMagnifyingGlassMinus, FaMagnifyingGlassPlus } from 'react-icons/fa6';
import { useReactFlow } from 'reactflow'; import { useReactFlow } from 'reactflow';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/nodesSlice';
const ViewportControls = () => { const ViewportControls = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -49,27 +48,24 @@ const ViewportControls = () => {
return ( return (
<ButtonGroup isAttached orientation="vertical"> <ButtonGroup isAttached orientation="vertical">
<Tooltip label={t('nodes.zoomInNodes')}> <IAIIconButton
<IAIIconButton tooltip={t('nodes.zoomInNodes')}
aria-label="Zoom in " aria-label={t('nodes.zoomInNodes')}
onClick={handleClickedZoomIn} onClick={handleClickedZoomIn}
icon={<FaPlus />} icon={<FaMagnifyingGlassPlus />}
/> />
</Tooltip> <IAIIconButton
<Tooltip label={t('nodes.zoomOutNodes')}> tooltip={t('nodes.zoomOutNodes')}
<IAIIconButton aria-label={t('nodes.zoomOutNodes')}
aria-label="Zoom out" onClick={handleClickedZoomOut}
onClick={handleClickedZoomOut} icon={<FaMagnifyingGlassMinus />}
icon={<FaMinus />} />
/> <IAIIconButton
</Tooltip> tooltip={t('nodes.fitViewportNodes')}
<Tooltip label={t('nodes.fitViewportNodes')}> aria-label={t('nodes.fitViewportNodes')}
<IAIIconButton onClick={handleClickedFitView}
aria-label="Fit viewport" icon={<FaExpand />}
onClick={handleClickedFitView} />
icon={<FaExpand />}
/>
</Tooltip>
{/* <Tooltip {/* <Tooltip
label={ label={
shouldShowFieldTypeLegend shouldShowFieldTypeLegend
@ -84,20 +80,21 @@ const ViewportControls = () => {
icon={<FaInfo />} icon={<FaInfo />}
/> />
</Tooltip> */} </Tooltip> */}
<Tooltip <IAIIconButton
label={ tooltip={
shouldShowMinimapPanel shouldShowMinimapPanel
? t('nodes.hideMinimapnodes') ? t('nodes.hideMinimapnodes')
: t('nodes.showMinimapnodes') : t('nodes.showMinimapnodes')
} }
> aria-label={
<IAIIconButton shouldShowMinimapPanel
aria-label="Toggle minimap" ? t('nodes.hideMinimapnodes')
isChecked={shouldShowMinimapPanel} : t('nodes.showMinimapnodes')
onClick={handleClickedToggleMiniMapPanel} }
icon={<FaMapMarkerAlt />} isChecked={shouldShowMinimapPanel}
/> onClick={handleClickedToggleMiniMapPanel}
</Tooltip> icon={<FaMapMarkerAlt />}
/>
</ButtonGroup> </ButtonGroup>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,10 @@
import { Flex } from '@chakra-ui/react'; 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 DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { buildWorkflow } from 'features/nodes/util/buildWorkflow'; import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { memo, useMemo } from 'react'; import { memo } 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,
};
};
const WorkflowJSONTab = () => { const WorkflowJSONTab = () => {
const { workflow } = useWatchWorkflow(); const workflow = useWorkflow();
return ( return (
<Flex <Flex

View File

@ -10,6 +10,7 @@ import { memo } from 'react';
import WorkflowGeneralTab from './WorkflowGeneralTab'; import WorkflowGeneralTab from './WorkflowGeneralTab';
import WorkflowLinearTab from './WorkflowLinearTab'; import WorkflowLinearTab from './WorkflowLinearTab';
import WorkflowJSONTab from './WorkflowJSONTab'; import WorkflowJSONTab from './WorkflowJSONTab';
import WorkflowEditorControls from '../../flow/panels/TopCenterPanel/WorkflowEditorControls';
const WorkflowPanel = () => { const WorkflowPanel = () => {
return ( return (
@ -21,8 +22,12 @@ const WorkflowPanel = () => {
h: 'full', h: 'full',
borderRadius: 'base', borderRadius: 'base',
p: 4, p: 4,
gap: 2,
}} }}
> >
<Flex gap={2}>
<WorkflowEditorControls />
</Flex>
<Tabs <Tabs
variant="line" variant="line"
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }} 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; state.workflow.contact = action.payload;
}, },
workflowLoaded: (state, action: PayloadAction<Workflow>) => { workflowLoaded: (state, action: PayloadAction<Workflow>) => {
// TODO: validation
const { nodes, edges, ...workflow } = action.payload; const { nodes, edges, ...workflow } = action.payload;
state.workflow = workflow; state.workflow = workflow;
state.nodes = applyNodeChanges( state.nodes = applyNodeChanges(
nodes.map((node) => ({ nodes.map((node) => ({
item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` }, item: { ...node, dragHandle: `.${DRAG_HANDLE_CLASSNAME}` },
@ -614,6 +614,16 @@ const nodesSlice = createSlice({
edges.map((edge) => ({ item: edge, type: 'add' })), 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) => { workflowReset: (state) => {
state.workflow = cloneDeep(initialWorkflow); 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 type SemVer = z.infer<typeof zSemVer>;
export const zWorkflow = z.object({ export const zWorkflow = z.object({
name: z.string().trim().min(1), name: z.string(),
author: z.string(), author: z.string(),
description: z.string(), description: z.string(),
version: 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 { ChevronDownIcon } from '@chakra-ui/icons';
import { sessionCanceled } from 'services/api/thunks/session'; import { sessionCanceled } from 'services/api/thunks/session';
import IAIButton from 'common/components/IAIButton';
const cancelButtonSelector = createSelector( const cancelButtonSelector = createSelector(
systemSelector, systemSelector,
@ -49,15 +50,14 @@ const cancelButtonSelector = createSelector(
} }
); );
interface CancelButtonProps { type Props = Omit<ButtonProps, 'aria-label'> & {
btnGroupWidth?: string | number; btnGroupWidth?: string | number;
} asIconButton?: boolean;
};
const CancelButton = ( const CancelButton = (props: Props) => {
props: CancelButtonProps & Omit<ButtonProps, 'aria-label'>
) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { btnGroupWidth = 'auto', ...rest } = props; const { btnGroupWidth = 'auto', asIconButton = false, ...rest } = props;
const { const {
isProcessing, isProcessing,
isConnected, isConnected,
@ -124,16 +124,31 @@ const CancelButton = (
return ( return (
<ButtonGroup isAttached width={btnGroupWidth}> <ButtonGroup isAttached width={btnGroupWidth}>
<IAIIconButton {asIconButton ? (
icon={cancelIcon} <IAIIconButton
tooltip={cancelLabel} icon={cancelIcon}
aria-label={cancelLabel} tooltip={cancelLabel}
isDisabled={!isConnected || !isProcessing || !isCancelable} aria-label={cancelLabel}
onClick={handleClickCancel} isDisabled={!isConnected || !isProcessing || !isCancelable}
colorScheme="error" onClick={handleClickCancel}
id="cancel-button" colorScheme="error"
{...rest} 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}> <Menu closeOnSelect={false}>
<MenuButton <MenuButton
as={IAIIconButton} as={IAIIconButton}

View File

@ -56,7 +56,7 @@ const FloatingSidePanelButtons = ({
icon={<FaSlidersH />} icon={<FaSlidersH />}
/> />
<InvokeButton asIconButton sx={floatingButtonStyles} /> <InvokeButton asIconButton sx={floatingButtonStyles} />
<CancelButton sx={floatingButtonStyles} /> <CancelButton sx={floatingButtonStyles} asIconButton />
</Flex> </Flex>
</Portal> </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)', '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)', 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)`, light: `0 0 0 2px var(--invokeai-colors-accent-400)`,
dark: `0 0 0 2px var(--invokeai-colors-accent-500)`, 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, colors: InvokeAIColors,
components: { components: {