wip buttons

This commit is contained in:
psychedelicious 2023-08-23 16:24:09 +10:00
parent 8eca3bbbcd
commit 3651cf7ee2
14 changed files with 96 additions and 155 deletions

View File

@ -718,10 +718,11 @@
"swapSizes": "Swap Sizes" "swapSizes": "Swap Sizes"
}, },
"nodes": { "nodes": {
"reloadSchema": "Reload Schema", "reloadSchema": "Reload Node Templates",
"saveGraph": "Save Graph", "saveGraph": "Save Workflow",
"loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)", "loadGraph": "Load Workflow",
"clearGraph": "Clear Graph", "resetGraph": "Reset Workflow",
"clearGraph": "Reset Graph",
"clearGraphDesc": "Are you sure you want to clear all nodes?", "clearGraphDesc": "Are you sure you want to clear all nodes?",
"zoomInNodes": "Zoom In", "zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out", "zoomOutNodes": "Zoom Out",

View File

@ -32,6 +32,10 @@ const selector = createSelector(
} }
if (activeTabName === 'nodes' && nodes.shouldValidateGraph) { if (activeTabName === 'nodes' && nodes.shouldValidateGraph) {
if (!nodes.nodes.length) {
reasons.push('No nodes in graph');
}
nodes.nodes.forEach((node) => { nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) { if (!isInvocationNode(node)) {
return; return;

View File

@ -1,81 +0,0 @@
import { Box } from '@chakra-ui/react';
import { userInvoked } from 'app/store/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import { IAIIconButtonProps } from 'common/components/IAIIconButton';
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
import { InvokeButtonTooltipContent } from 'features/parameters/components/ProcessButtons/InvokeButton';
import ProgressBar from 'features/system/components/ProgressBar';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {}
const NodeInvokeButton = (props: InvokeButton) => {
const { ...rest } = props;
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const { isReady, isProcessing } = useIsReadyToInvoke();
const handleInvoke = useCallback(() => {
dispatch(userInvoked('nodes'));
}, [dispatch]);
const { t } = useTranslation();
useHotkeys(
['ctrl+enter', 'meta+enter'],
handleInvoke,
{
enabled: () => isReady,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
[isReady, activeTabName]
);
return (
<Box style={{ flexGrow: 4 }} position="relative">
<Box style={{ position: 'relative' }}>
{!isReady && (
<Box
borderRadius="base"
style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '100%',
overflow: 'clip',
}}
>
<ProgressBar />
</Box>
)}
<IAIButton
tooltip={<InvokeButtonTooltipContent />}
aria-label={t('parameters.invoke')}
type="submit"
isDisabled={!isReady}
onClick={handleInvoke}
flexGrow={1}
w="100%"
colorScheme="accent"
id="invoke-button"
leftIcon={isProcessing ? undefined : <FaPlay />}
fontWeight={700}
isLoading={isProcessing}
loadingText={t('parameters.invoke')}
{...rest}
>
Invoke
</IAIButton>
</Box>
</Box>
);
};
export default memo(NodeInvokeButton);

View File

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

View File

@ -1,24 +1,14 @@
import { HStack } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import { memo } from 'react'; import { memo } from 'react';
import { Panel } from 'reactflow'; import { Panel } from 'reactflow';
import NodeEditorSettings from './NodeEditorSettings'; import WorkflowEditorControls from './WorkflowEditorControls';
import ClearGraphButton from './ClearGraphButton';
import NodeInvokeButton from './NodeInvokeButton';
import ReloadSchemaButton from './ReloadSchemaButton';
import LoadWorkflowButton from './LoadWorkflowButton';
const TopCenterPanel = () => { const TopCenterPanel = () => {
return ( return (
<Panel position="top-center"> <Panel position="top-center">
<HStack> <Flex gap={2}>
<NodeInvokeButton /> <WorkflowEditorControls />
<CancelButton /> </Flex>
<ReloadSchemaButton />
<ClearGraphButton />
<NodeEditorSettings />
<LoadWorkflowButton />
</HStack>
</Panel> </Panel>
); );
}; };

View File

@ -0,0 +1,18 @@
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 LoadWorkflowButton from './LoadWorkflowButton';
const WorkflowEditorControls = () => {
return (
<>
<InvokeButton />
<CancelButton />
<ClearGraphButton />
<LoadWorkflowButton />
</>
);
};
export default memo(WorkflowEditorControls);

View File

@ -2,6 +2,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { memo } from 'react'; import { memo } from 'react';
import { Panel } from 'reactflow'; import { Panel } from 'reactflow';
import FieldTypeLegend from './FieldTypeLegend'; import FieldTypeLegend from './FieldTypeLegend';
import WorkflowEditorSettings from './WorkflowEditorSettings';
const TopRightPanel = () => { const TopRightPanel = () => {
const shouldShowFieldTypeLegend = useAppSelector( const shouldShowFieldTypeLegend = useAppSelector(
@ -10,6 +11,7 @@ const TopRightPanel = () => {
return ( return (
<Panel position="top-right"> <Panel position="top-right">
<WorkflowEditorSettings />
{shouldShowFieldTypeLegend && <FieldTypeLegend />} {shouldShowFieldTypeLegend && <FieldTypeLegend />}
</Panel> </Panel>
); );

View File

@ -27,6 +27,11 @@ 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';
const formLabelProps: FormLabelProps = {
fontWeight: 600,
};
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -49,7 +54,7 @@ const selector = createSelector(
defaultSelectorOptions defaultSelectorOptions
); );
const NodeEditorSettings = () => { const WorkflowEditorSettings = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const {
@ -98,7 +103,8 @@ const NodeEditorSettings = () => {
return ( return (
<> <>
<IAIIconButton <IAIIconButton
aria-label="Node Editor Settings" aria-label="Workflow Editor Settings"
tooltip="Workflow Editor Settings"
icon={<FaCog />} icon={<FaCog />}
onClick={onOpen} onClick={onOpen}
/> />
@ -106,7 +112,7 @@ const NodeEditorSettings = () => {
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered> <Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader>Node Editor Settings</ModalHeader> <ModalHeader>Workflow Editor Settings</ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Flex <Flex
@ -157,6 +163,7 @@ const NodeEditorSettings = () => {
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 />
</Flex> </Flex>
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
@ -165,8 +172,4 @@ const NodeEditorSettings = () => {
); );
}; };
export default memo(NodeEditorSettings); export default memo(WorkflowEditorSettings);
const formLabelProps: FormLabelProps = {
fontWeight: 600,
};

View File

@ -7,14 +7,11 @@ import { zWorkflow } from 'features/nodes/types/types';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { flushSync } from 'react-dom';
import { useReactFlow } from 'reactflow';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { fromZodError, fromZodIssue } from 'zod-validation-error'; import { fromZodError, fromZodIssue } from 'zod-validation-error';
export const useLoadWorkflowFromFile = () => { export const useLoadWorkflowFromFile = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { fitView } = useReactFlow();
const logger = useLogger('nodes'); const logger = useLogger('nodes');
const loadWorkflowFromFile = useCallback( const loadWorkflowFromFile = useCallback(
(file: File | null) => { (file: File | null) => {
@ -51,7 +48,6 @@ export const useLoadWorkflowFromFile = () => {
} }
dispatch(workflowLoaded(result.data)); dispatch(workflowLoaded(result.data));
flushSync(fitView);
dispatch( dispatch(
addToast( addToast(
@ -79,7 +75,7 @@ export const useLoadWorkflowFromFile = () => {
reader.readAsText(file); reader.readAsText(file);
}, },
[dispatch, fitView, logger] [dispatch, logger]
); );
return loadWorkflowFromFile; return loadWorkflowFromFile;

View File

@ -1,6 +1,5 @@
import { import {
Box, Box,
ChakraProps,
Divider, Divider,
Flex, Flex,
ListItem, ListItem,
@ -19,7 +18,6 @@ import IAIIconButton, {
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import { selectIsBusy } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -27,32 +25,6 @@ import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa'; import { FaPlay } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
_disabled: {
bg: 'none',
color: 'base.600',
cursor: 'not-allowed',
_hover: {
color: 'base.600',
bg: 'none',
},
},
};
const selector = createSelector(
[stateSelector, activeTabNameSelector, selectIsBusy],
({ gallery }, activeTabName, isBusy) => {
const { autoAddBoardId } = gallery;
return {
isBusy,
autoAddBoardId,
activeTabName,
};
},
defaultSelectorOptions
);
interface InvokeButton interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> { extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
asIconButton?: boolean; asIconButton?: boolean;
@ -62,7 +34,7 @@ export default function InvokeButton(props: InvokeButton) {
const { asIconButton = false, sx, ...rest } = props; const { asIconButton = false, sx, ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isReady, isProcessing } = useIsReadyToInvoke(); const { isReady, isProcessing } = useIsReadyToInvoke();
const { activeTabName } = useAppSelector(selector); const activeTabName = useAppSelector(activeTabNameSelector);
const handleInvoke = useCallback(() => { const handleInvoke = useCallback(() => {
dispatch(clampSymmetrySteps()); dispatch(clampSymmetrySteps());
@ -113,10 +85,10 @@ export default function InvokeButton(props: InvokeButton) {
colorScheme="accent" colorScheme="accent"
isLoading={isProcessing} isLoading={isProcessing}
id="invoke-button" id="invoke-button"
data-progress={isProcessing}
sx={{ sx={{
w: 'full', w: 'full',
flexGrow: 1, flexGrow: 1,
...(isProcessing ? IN_PROGRESS_STYLES : {}),
...sx, ...sx,
}} }}
{...rest} {...rest}
@ -126,6 +98,7 @@ export default function InvokeButton(props: InvokeButton) {
tooltip={<InvokeButtonTooltipContent />} tooltip={<InvokeButtonTooltipContent />}
aria-label={t('parameters.invoke')} aria-label={t('parameters.invoke')}
type="submit" type="submit"
data-progress={isProcessing}
isDisabled={!isReady} isDisabled={!isReady}
onClick={handleInvoke} onClick={handleInvoke}
colorScheme="accent" colorScheme="accent"
@ -137,7 +110,6 @@ export default function InvokeButton(props: InvokeButton) {
w: 'full', w: 'full',
flexGrow: 1, flexGrow: 1,
fontWeight: 700, fontWeight: 700,
...(isProcessing ? IN_PROGRESS_STYLES : {}),
...sx, ...sx,
}} }}
{...rest} {...rest}
@ -150,9 +122,21 @@ export default function InvokeButton(props: InvokeButton) {
); );
} }
const tooltipSelector = createSelector(
[stateSelector],
({ gallery }) => {
const { autoAddBoardId } = gallery;
return {
autoAddBoardId,
};
},
defaultSelectorOptions
);
export const InvokeButtonTooltipContent = memo(() => { export const InvokeButtonTooltipContent = memo(() => {
const { isReady, reasons } = useIsReadyToInvoke(); const { isReady, reasons } = useIsReadyToInvoke();
const { autoAddBoardId } = useAppSelector(selector); const { autoAddBoardId } = useAppSelector(tooltipSelector);
const autoAddBoardName = useBoardName(autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId);
return ( return (

View File

@ -15,7 +15,7 @@ const InvokeAILogoComponent = ({ showVersion = true }: Props) => {
const isHovered = useHoverDirty(ref); const isHovered = useHoverDirty(ref);
return ( return (
<Flex alignItems="center" gap={3} ps={1} ref={ref}> <Flex alignItems="center" gap={5} ps={1} ref={ref}>
<Image <Image
src={InvokeAILogoImage} src={InvokeAILogoImage}
alt="invoke-ai-logo" alt="invoke-ai-logo"

View File

@ -3,7 +3,7 @@ import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { InvokeLogLevel } from 'app/logging/logger'; import { InvokeLogLevel } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { t } from 'i18next'; import { t } from 'i18next';
import { startCase, upperFirst } from 'lodash-es'; import { get, startCase, upperFirst } from 'lodash-es';
import { LogLevelName } from 'roarr'; import { LogLevelName } from 'roarr';
import { import {
isAnySessionRejected, isAnySessionRejected,
@ -368,14 +368,14 @@ export const systemSlice = createSlice({
return; return;
} }
} else if (action.payload?.error) { } else if (action.payload?.error) {
errorDescription = action.payload?.error as string; errorDescription = action.payload?.error;
} }
state.toastQueue.push( state.toastQueue.push(
makeToast({ makeToast({
title: t('toast.serverError'), title: t('toast.serverError'),
status: 'error', status: 'error',
description: errorDescription, description: get(errorDescription, 'detail', 'Unknown Error'),
duration, duration,
}) })
); );

View File

@ -8,7 +8,16 @@ const invokeAI = defineStyle((props) => {
if (c === 'base') { if (c === 'base') {
const _disabled = { const _disabled = {
bg: mode('base.150', 'base.700')(props), bg: mode('base.150', 'base.700')(props),
color: mode('base.500', 'base.500')(props), color: mode('base.300', 'base.500')(props),
svg: {
fill: mode('base.500', 'base.500')(props),
},
opacity: 1,
};
const data_progress = {
bg: 'none',
color: mode('base.300', 'base.500')(props),
svg: { svg: {
fill: mode('base.500', 'base.500')(props), fill: mode('base.500', 'base.500')(props),
}, },
@ -31,6 +40,7 @@ const invokeAI = defineStyle((props) => {
_disabled, _disabled,
}, },
_disabled, _disabled,
'&[data-progress="true"]': { ...data_progress, _hover: data_progress },
}; };
} }
@ -45,6 +55,17 @@ const invokeAI = defineStyle((props) => {
filter: mode(undefined, 'saturate(65%)')(props), filter: mode(undefined, 'saturate(65%)')(props),
}; };
const data_progress = {
// bg: 'none',
color: mode(`${c}.50`, `${c}.500`)(props),
svg: {
fill: mode(`${c}.50`, `${c}.500`)(props),
filter: 'unset',
},
opacity: 0.7,
filter: mode(undefined, 'saturate(65%)')(props),
};
return { return {
bg: mode(`${c}.400`, `${c}.600`)(props), bg: mode(`${c}.400`, `${c}.600`)(props),
color: mode(`base.50`, `base.100`)(props), color: mode(`base.50`, `base.100`)(props),
@ -61,6 +82,7 @@ const invokeAI = defineStyle((props) => {
}, },
_disabled, _disabled,
}, },
'&[data-progress="true"]': { ...data_progress, _hover: data_progress },
}; };
}); });

View File

@ -9,7 +9,7 @@ const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const invokeAIFilledTrack = defineStyle((_props) => ({ const invokeAIFilledTrack = defineStyle((_props) => ({
bg: 'accentAlpha.500', bg: 'accentAlpha.700',
})); }));
const invokeAITrack = defineStyle((_props) => { const invokeAITrack = defineStyle((_props) => {