Merge branch 'main' into refactor/rename-performance-options

This commit is contained in:
Lincoln Stein
2023-08-21 22:29:34 -04:00
committed by GitHub
267 changed files with 3650 additions and 2675 deletions

View File

@ -23,6 +23,11 @@ module.exports = {
plugins: ['react', '@typescript-eslint', 'eslint-plugin-react-hooks'],
root: true,
rules: {
curly: 'error',
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react-hooks/exhaustive-deps': 'error',
'no-var': 'error',
'brace-style': 'error',

View File

@ -73,7 +73,6 @@
"@nanostores/react": "^0.7.1",
"@reduxjs/toolkit": "^1.9.5",
"@roarr/browser-log-writer": "^1.1.5",
"chakra-ui-contextmenu": "^1.0.5",
"dateformat": "^5.0.3",
"downshift": "^7.6.0",
"formik": "^2.4.2",
@ -85,6 +84,7 @@
"konva": "^9.2.0",
"lodash-es": "^4.17.21",
"nanostores": "^0.9.2",
"new-github-issue-url": "^1.0.0",
"openapi-fetch": "^0.6.1",
"overlayscrollbars": "^2.2.0",
"overlayscrollbars-react": "^0.5.0",
@ -95,6 +95,7 @@
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.11",
"react-hotkeys-hook": "4.4.0",
"react-i18next": "^13.0.1",
"react-icons": "^4.10.1",

View File

@ -52,7 +52,7 @@
"img2img": "Image To Image",
"unifiedCanvas": "Unified Canvas",
"linear": "Linear",
"nodes": "Node Editor",
"nodes": "Workflow Editor",
"batch": "Batch Manager",
"modelManager": "Model Manager",
"postprocessing": "Post Processing",
@ -133,6 +133,7 @@
"generalHotkeys": "General Hotkeys",
"galleryHotkeys": "Gallery Hotkeys",
"unifiedCanvasHotkeys": "Unified Canvas Hotkeys",
"nodesHotkeys": "Nodes Hotkeys",
"invoke": {
"title": "Invoke",
"desc": "Generate an image"
@ -332,6 +333,10 @@
"acceptStagingImage": {
"title": "Accept Staging Image",
"desc": "Accept Current Staging Area Image"
},
"addNodes": {
"title": "Add Nodes",
"desc": "Opens the add node menu"
}
},
"modelManager": {

View File

@ -42,7 +42,6 @@ async function main() {
// We only want to make fields optional if they are required
if (!Array.isArray(schemaObject?.required)) {
schemaObject.required = ['id', 'type'];
return;
}
schemaObject.required.forEach((prop) => {
@ -68,12 +67,26 @@ async function main() {
return;
}
// if (
// 'input' in schemaObject &&
// (schemaObject.input === 'any' || schemaObject.input === 'connection')
// ) {
// schemaObject.required = false;
// }
// Check if we are generating types for an invocation output
const isInvocationOutputPath = metadata.path.match(
/^#\/components\/schemas\/\w*Output$/
);
const hasOutputProperties =
schemaObject.properties && 'type' in schemaObject.properties;
if (isInvocationOutputPath && hasOutputProperties) {
if (!Array.isArray(schemaObject?.required)) {
schemaObject.required = ['type'];
}
schemaObject.required = [
...new Set(schemaObject.required.concat(['type'])),
];
console.log(
`Making output's "type" required: ${COLORS.fg.yellow}${schemaObject.title}${COLORS.reset}`
);
}
},
});
fs.writeFileSync(OUTPUT_FILE, types);

View File

@ -16,9 +16,11 @@ import InvokeTabs from 'features/ui/components/InvokeTabs';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import i18n from 'i18n';
import { size } from 'lodash-es';
import { ReactNode, memo, useEffect } from 'react';
import { ReactNode, memo, useCallback, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
const DEFAULT_CONFIG = {};
@ -32,6 +34,11 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
const logger = useLogger();
const dispatch = useAppDispatch();
const handleReset = useCallback(() => {
localStorage.clear();
location.reload();
return false;
}, []);
useEffect(() => {
i18n.changeLanguage(language);
@ -49,7 +56,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
}, [dispatch]);
return (
<>
<ErrorBoundary
onReset={handleReset}
FallbackComponent={AppErrorBoundaryFallback}
>
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
<ImageUploader>
<Grid
@ -87,7 +97,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
<ChangeBoardModal />
<Toaster />
<GlobalHotkeys />
</>
</ErrorBoundary>
);
};

View File

@ -0,0 +1,97 @@
import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
import newGithubIssueUrl from 'new-github-issue-url';
import { memo, useCallback, useMemo } from 'react';
import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa';
import { FaArrowRotateLeft } from 'react-icons/fa6';
import { serializeError } from 'serialize-error';
type Props = {
error: Error;
resetErrorBoundary: () => void;
};
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
const toast = useToast();
const handleCopy = useCallback(() => {
const text = JSON.stringify(serializeError(error), null, 2);
navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``);
toast({
title: 'Error Copied',
});
}, [error, toast]);
const url = useMemo(
() =>
newGithubIssueUrl({
user: 'invoke-ai',
repo: 'InvokeAI',
template: 'BUG_REPORT.yml',
title: `[bug]: ${error.name}: ${error.message}`,
}),
[error.message, error.name]
);
return (
<Flex
layerStyle="body"
sx={{
w: '100vw',
h: '100vh',
alignItems: 'center',
justifyContent: 'center',
p: 4,
}}
>
<Flex
layerStyle="first"
sx={{
flexDir: 'column',
borderRadius: 'base',
justifyContent: 'center',
gap: 8,
p: 16,
}}
>
<Heading>Something went wrong</Heading>
<Flex
layerStyle="second"
sx={{
px: 8,
py: 4,
borderRadius: 'base',
gap: 4,
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Text
sx={{
fontWeight: 600,
color: 'error.500',
_dark: { color: 'error.400' },
}}
>
{error.name}: {error.message}
</Text>
</Flex>
<Flex sx={{ gap: 4 }}>
<IAIButton
leftIcon={<FaArrowRotateLeft />}
onClick={resetErrorBoundary}
>
Reset UI
</IAIButton>
<IAIButton leftIcon={<FaCopy />} onClick={handleCopy}>
Copy Error
</IAIButton>
<Link href={url} isExternal>
<IAIButton leftIcon={<FaExternalLinkAlt />}>Create Issue</IAIButton>
</Link>
</Flex>
</Flex>
</Flex>
);
};
export default memo(AppErrorBoundaryFallback);

View File

@ -3,7 +3,7 @@ import {
createLocalStorageManager,
extendTheme,
} from '@chakra-ui/react';
import { ReactNode, useEffect, useMemo } from 'react';
import { ReactNode, memo, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { theme as invokeAITheme } from 'theme/theme';
@ -46,4 +46,4 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
);
}
export default ThemeLocaleProvider;
export default memo(ThemeLocaleProvider);

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toastQueueSelector } from 'features/system/store/systemSelectors';
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
import { MakeToastArg, makeToast } from 'features/system/util/makeToast';
import { useCallback, useEffect } from 'react';
import { memo, useCallback, useEffect } from 'react';
/**
* Logical component. Watches the toast queue and makes toasts when the queue is not empty.
@ -44,4 +44,4 @@ export const useAppToaster = () => {
return toaster;
};
export default Toaster;
export default memo(Toaster);

View File

@ -1,13 +1,17 @@
import { logger } from 'app/logging/logger';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import {
controlNetImageChanged,
controlNetProcessedImageChanged,
} from 'features/controlNet/store/controlNetSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/types';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { clamp } from 'lodash-es';
import { clamp, forEach } from 'lodash-es';
import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
import { imagesAdapter } from 'services/api/util';
@ -73,22 +77,61 @@ export const addRequestedSingleImageDeletionListener = () => {
}
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
if (imageUsage.isCanvasImage) {
dispatch(resetCanvas());
}
if (imageUsage.isControlNetImage) {
dispatch(controlNetReset());
}
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (
getState().generation.initialImage?.imageName === imageDTO.image_name
) {
dispatch(clearInitialImage());
}
if (imageUsage.isInitialImage) {
dispatch(clearInitialImage());
}
// reset controlNets that use the deleted images
forEach(getState().controlNet.controlNets, (controlNet) => {
if (
controlNet.controlImage === imageDTO.image_name ||
controlNet.processedControlImage === imageDTO.image_name
) {
dispatch(
controlNetImageChanged({
controlNetId: controlNet.controlNetId,
controlImage: null,
})
);
dispatch(
controlNetProcessedImageChanged({
controlNetId: controlNet.controlNetId,
processedControlImage: null,
})
);
}
});
if (imageUsage.isNodesImage) {
dispatch(nodeEditorReset());
}
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (
input.type === 'ImageField' &&
input.value?.image_name === imageDTO.image_name
) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
});
// Delete from server
const { requestId } = dispatch(
@ -154,17 +197,58 @@ export const addRequestedMultipleImageDeletionListener = () => {
dispatch(resetCanvas());
}
if (imagesUsage.some((i) => i.isControlNetImage)) {
dispatch(controlNetReset());
}
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (
getState().generation.initialImage?.imageName ===
imageDTO.image_name
) {
dispatch(clearInitialImage());
}
if (imagesUsage.some((i) => i.isInitialImage)) {
dispatch(clearInitialImage());
}
// reset controlNets that use the deleted images
forEach(getState().controlNet.controlNets, (controlNet) => {
if (
controlNet.controlImage === imageDTO.image_name ||
controlNet.processedControlImage === imageDTO.image_name
) {
dispatch(
controlNetImageChanged({
controlNetId: controlNet.controlNetId,
controlImage: null,
})
);
dispatch(
controlNetProcessedImageChanged({
controlNetId: controlNet.controlNetId,
processedControlImage: null,
})
);
}
});
if (imagesUsage.some((i) => i.isNodesImage)) {
dispatch(nodeEditorReset());
}
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (
input.type === 'ImageField' &&
input.value?.image_name === imageDTO.image_name
) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
});
} catch {
// no-op
}

View File

@ -8,8 +8,8 @@ import {
import { memo, ReactNode } from 'react';
export interface IAIButtonProps extends ButtonProps {
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
tooltip?: TooltipProps['label'];
tooltipProps?: Omit<TooltipProps, 'children' | 'label'>;
isChecked?: boolean;
children: ReactNode;
}

View File

@ -100,14 +100,18 @@ const IAIDndImage = (props: IAIDndImageProps) => {
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (onMouseOver) onMouseOver(e);
if (onMouseOver) {
onMouseOver(e);
}
setIsHovered(true);
},
[onMouseOver]
);
const handleMouseOut = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (onMouseOut) onMouseOut(e);
if (onMouseOut) {
onMouseOut(e);
}
setIsHovered(false);
},
[onMouseOut]

View File

@ -1,4 +1,5 @@
import { Box, Flex, Icon } from '@chakra-ui/react';
import { memo } from 'react';
import { FaExclamation } from 'react-icons/fa';
const IAIErrorLoadingImageFallback = () => {
@ -39,4 +40,4 @@ const IAIErrorLoadingImageFallback = () => {
);
};
export default IAIErrorLoadingImageFallback;
export default memo(IAIErrorLoadingImageFallback);

View File

@ -1,4 +1,5 @@
import { Box, Skeleton } from '@chakra-ui/react';
import { memo } from 'react';
const IAIFillSkeleton = () => {
return (
@ -27,4 +28,4 @@ const IAIFillSkeleton = () => {
);
};
export default IAIFillSkeleton;
export default memo(IAIFillSkeleton);

View File

@ -9,8 +9,8 @@ import { memo } from 'react';
export type IAIIconButtonProps = IconButtonProps & {
role?: string;
tooltip?: string;
tooltipProps?: Omit<TooltipProps, 'children'>;
tooltip?: TooltipProps['label'];
tooltipProps?: Omit<TooltipProps, 'children' | 'label'>;
isChecked?: boolean;
};

View File

@ -1,4 +1,5 @@
import { Badge, Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { ImageDTO } from 'services/api/types';
type ImageMetadataOverlayProps = {
@ -26,4 +27,4 @@ const ImageMetadataOverlay = ({ imageDTO }: ImageMetadataOverlayProps) => {
);
};
export default ImageMetadataOverlay;
export default memo(ImageMetadataOverlay);

View File

@ -1,4 +1,5 @@
import { Box, Flex, Heading } from '@chakra-ui/react';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
type ImageUploadOverlayProps = {
@ -87,4 +88,4 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
</Box>
);
};
export default ImageUploadOverlay;
export default memo(ImageUploadOverlay);

View File

@ -150,7 +150,9 @@ const ImageUploader = (props: ImageUploaderProps) => {
{...getRootProps({ style: {} })}
onKeyDown={(e: KeyboardEvent) => {
// Bail out if user hits spacebar - do not open the uploader
if (e.key === ' ') return;
if (e.key === ' ') {
return;
}
}}
>
<input {...getInputProps()} />

View File

@ -1,4 +1,5 @@
import { Flex, Icon } from '@chakra-ui/react';
import { memo } from 'react';
import { FaImage } from 'react-icons/fa';
const SelectImagePlaceholder = () => {
@ -19,4 +20,4 @@ const SelectImagePlaceholder = () => {
);
};
export default SelectImagePlaceholder;
export default memo(SelectImagePlaceholder);

View File

@ -1,4 +1,5 @@
import { Box } from '@chakra-ui/react';
import { memo } from 'react';
type Props = {
isSelected: boolean;
@ -18,6 +19,7 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
opacity: isSelected ? 1 : 0.7,
transitionProperty: 'common',
transitionDuration: '0.1s',
pointerEvents: 'none',
shadow: isSelected
? isHovered
? 'hoverSelected.light'
@ -39,4 +41,4 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
);
};
export default SelectionOverlay;
export default memo(SelectionOverlay);

View File

@ -2,71 +2,104 @@ 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 { validateSeedWeights } from 'common/util/seedWeightPairs';
import { isInvocationNode } from 'features/nodes/types/types';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { forEach } from 'lodash-es';
import { NON_REFINER_BASE_MODELS } from 'services/api/constants';
import { modelsApi } from '../../services/api/endpoints/models';
import { forEach, map } from 'lodash-es';
import { getConnectedEdges } from 'reactflow';
const readinessSelector = createSelector(
const selector = createSelector(
[stateSelector, activeTabNameSelector],
(state, activeTabName) => {
const { generation, system } = state;
const { initialImage } = generation;
const { generation, system, nodes } = state;
const { initialImage, model } = generation;
const { isProcessing, isConnected } = system;
let isReady = true;
const reasonsWhyNotReady: string[] = [];
const reasons: string[] = [];
if (activeTabName === 'img2img' && !initialImage) {
isReady = false;
reasonsWhyNotReady.push('No initial image selected');
}
const { isSuccess: mainModelsSuccessfullyLoaded } =
modelsApi.endpoints.getMainModels.select(NON_REFINER_BASE_MODELS)(state);
if (!mainModelsSuccessfullyLoaded) {
isReady = false;
reasonsWhyNotReady.push('Models are not loaded');
}
// TODO: job queue
// Cannot generate if already processing an image
if (isProcessing) {
isReady = false;
reasonsWhyNotReady.push('System Busy');
reasons.push('System busy');
}
// Cannot generate if not connected
if (!isConnected) {
isReady = false;
reasonsWhyNotReady.push('System Disconnected');
reasons.push('System disconnected');
}
// // Cannot generate variations without valid seed weights
// if (
// shouldGenerateVariations &&
// (!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
// ) {
// isReady = false;
// reasonsWhyNotReady.push('Seed-Weights badly formatted.');
// }
if (activeTabName === 'img2img' && !initialImage) {
reasons.push('No initial image selected');
}
forEach(state.controlNet.controlNets, (controlNet, id) => {
if (!controlNet.model) {
isReady = false;
reasonsWhyNotReady.push(`ControlNet ${id} has no model selected.`);
if (activeTabName === 'nodes' && nodes.shouldValidateGraph) {
nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = nodes.nodeTemplates[node.data.type];
if (!nodeTemplate) {
// Node type not found
reasons.push('Missing node template');
return;
}
const connectedEdges = getConnectedEdges([node], nodes.edges);
forEach(node.data.inputs, (field) => {
const fieldTemplate = nodeTemplate.inputs[field.name];
const hasConnection = connectedEdges.some(
(edge) =>
edge.target === node.id && edge.targetHandle === field.name
);
if (!fieldTemplate) {
reasons.push('Missing field template');
return;
}
if (fieldTemplate.required && !field.value && !hasConnection) {
reasons.push(
`${node.data.label || nodeTemplate.title} -> ${
field.label || fieldTemplate.title
} missing input`
);
return;
}
});
});
} else {
if (!model) {
reasons.push('No model selected');
}
});
// All good
return { isReady, reasonsWhyNotReady };
if (state.controlNet.isEnabled) {
map(state.controlNet.controlNets).forEach((controlNet, i) => {
if (!controlNet.isEnabled) {
return;
}
if (!controlNet.model) {
reasons.push(`ControlNet ${i + 1} has no model selected.`);
}
if (
!controlNet.controlImage ||
(!controlNet.processedControlImage &&
controlNet.processorType !== 'none')
) {
reasons.push(`ControlNet ${i + 1} has no control image`);
}
});
}
}
return { isReady: !reasons.length, isProcessing, reasons };
},
defaultSelectorOptions
);
export const useIsReadyToInvoke = () => {
const { isReady } = useAppSelector(readinessSelector);
return isReady;
const { isReady, isProcessing, reasons } = useAppSelector(selector);
return { isReady, isProcessing, reasons };
};

View File

@ -11,8 +11,14 @@ export default function useResolution():
const tabletResolutions = ['md', 'lg'];
const desktopResolutions = ['xl', '2xl'];
if (mobileResolutions.includes(breakpointValue)) return 'mobile';
if (tabletResolutions.includes(breakpointValue)) return 'tablet';
if (desktopResolutions.includes(breakpointValue)) return 'desktop';
if (mobileResolutions.includes(breakpointValue)) {
return 'mobile';
}
if (tabletResolutions.includes(breakpointValue)) {
return 'tablet';
}
if (desktopResolutions.includes(breakpointValue)) {
return 'desktop';
}
return 'unknown';
}

View File

@ -6,7 +6,11 @@ export const dateComparator = (a: string, b: string) => {
const dateB = new Date(b);
// sort in ascending order
if (dateA > dateB) return 1;
if (dateA < dateB) return -1;
if (dateA > dateB) {
return 1;
}
if (dateA < dateB) {
return -1;
}
return 0;
};

View File

@ -5,7 +5,9 @@ type Base64AndCaption = {
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
const w = window.open('');
if (!w) return;
if (!w) {
return;
}
images.forEach((i) => {
const image = new Image();

View File

@ -5,6 +5,7 @@ import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { isStagingSelector } from '../store/canvasSelectors';
import { memo } from 'react';
const ClearCanvasHistoryButtonModal = () => {
const isStaging = useAppSelector(isStagingSelector);
@ -28,4 +29,4 @@ const ClearCanvasHistoryButtonModal = () => {
</IAIAlertDialog>
);
};
export default ClearCanvasHistoryButtonModal;
export default memo(ClearCanvasHistoryButtonModal);

View File

@ -9,7 +9,7 @@ import {
import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node';
import { Vector2d } from 'konva/lib/types';
import { useCallback, useRef } from 'react';
import { memo, useCallback, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
import useCanvasDragMove from '../hooks/useCanvasDragMove';
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
@ -220,4 +220,4 @@ const IAICanvas = () => {
);
};
export default IAICanvas;
export default memo(IAICanvas);

View File

@ -4,6 +4,7 @@ import { isEqual } from 'lodash-es';
import { Group, Rect } from 'react-konva';
import { canvasSelector } from '../store/canvasSelectors';
import { memo } from 'react';
const selector = createSelector(
canvasSelector,
@ -67,4 +68,4 @@ const IAICanvasBoundingBoxOverlay = () => {
);
};
export default IAICanvasBoundingBoxOverlay;
export default memo(IAICanvasBoundingBoxOverlay);

View File

@ -6,7 +6,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { isEqual, range } from 'lodash-es';
import { ReactNode, useCallback, useLayoutEffect, useState } from 'react';
import { ReactNode, memo, useCallback, useLayoutEffect, useState } from 'react';
import { Group, Line as KonvaLine } from 'react-konva';
const selector = createSelector(
@ -117,4 +117,4 @@ const IAICanvasGrid = () => {
return <Group>{gridLines}</Group>;
};
export default IAICanvasGrid;
export default memo(IAICanvasGrid);

View File

@ -4,6 +4,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import useImage from 'use-image';
import { CanvasImage } from '../store/canvasTypes';
import { $authToken } from 'services/api/client';
import { memo } from 'react';
type IAICanvasImageProps = {
canvasImage: CanvasImage;
@ -25,4 +26,4 @@ const IAICanvasImage = (props: IAICanvasImageProps) => {
return <Image x={x} y={y} image={image} listening={false} />;
};
export default IAICanvasImage;
export default memo(IAICanvasImage);

View File

@ -4,7 +4,7 @@ import { systemSelector } from 'features/system/store/systemSelectors';
import { ImageConfig } from 'konva/lib/shapes/Image';
import { isEqual } from 'lodash-es';
import { useEffect, useState } from 'react';
import { memo, useEffect, useState } from 'react';
import { Image as KonvaImage } from 'react-konva';
import { canvasSelector } from '../store/canvasSelectors';
@ -66,4 +66,4 @@ const IAICanvasIntermediateImage = (props: Props) => {
) : null;
};
export default IAICanvasIntermediateImage;
export default memo(IAICanvasIntermediateImage);

View File

@ -7,7 +7,7 @@ import { Rect } from 'react-konva';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import Konva from 'konva';
import { isNumber } from 'lodash-es';
import { useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
export const canvasMaskCompositerSelector = createSelector(
canvasSelector,
@ -125,7 +125,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
}, [offset]);
useEffect(() => {
if (fillPatternImage) return;
if (fillPatternImage) {
return;
}
const image = new Image();
image.onload = () => {
@ -135,7 +137,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
}, [fillPatternImage, maskColorString]);
useEffect(() => {
if (!fillPatternImage) return;
if (!fillPatternImage) {
return;
}
fillPatternImage.src = getColoredSVG(maskColorString);
}, [fillPatternImage, maskColorString]);
@ -151,8 +155,9 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
!isNumber(stageScale) ||
!isNumber(stageDimensions.width) ||
!isNumber(stageDimensions.height)
)
) {
return null;
}
return (
<Rect
@ -172,4 +177,4 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
);
};
export default IAICanvasMaskCompositer;
export default memo(IAICanvasMaskCompositer);

View File

@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es';
import { Group, Line } from 'react-konva';
import { isCanvasMaskLine } from '../store/canvasTypes';
import { memo } from 'react';
export const canvasLinesSelector = createSelector(
[canvasSelector],
@ -52,4 +53,4 @@ const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
);
};
export default IAICanvasLines;
export default memo(IAICanvasLines);

View File

@ -12,6 +12,7 @@ import {
isCanvasFillRect,
} from '../store/canvasTypes';
import IAICanvasImage from './IAICanvasImage';
import { memo } from 'react';
const selector = createSelector(
[canvasSelector],
@ -33,7 +34,9 @@ const selector = createSelector(
const IAICanvasObjectRenderer = () => {
const { objects } = useAppSelector(selector);
if (!objects) return null;
if (!objects) {
return null;
}
return (
<Group name="outpainting-objects" listening={false}>
@ -101,4 +104,4 @@ const IAICanvasObjectRenderer = () => {
);
};
export default IAICanvasObjectRenderer;
export default memo(IAICanvasObjectRenderer);

View File

@ -12,7 +12,7 @@ import {
setDoesCanvasNeedScaling,
} from 'features/canvas/store/canvasSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useLayoutEffect, useRef } from 'react';
import { memo, useLayoutEffect, useRef } from 'react';
const canvasResizerSelector = createSelector(
canvasSelector,
@ -42,7 +42,9 @@ const IAICanvasResizer = () => {
useLayoutEffect(() => {
window.setTimeout(() => {
if (!ref.current) return;
if (!ref.current) {
return;
}
const { clientWidth, clientHeight } = ref.current;
@ -86,4 +88,4 @@ const IAICanvasResizer = () => {
);
};
export default IAICanvasResizer;
export default memo(IAICanvasResizer);

View File

@ -6,6 +6,7 @@ import { isEqual } from 'lodash-es';
import { Group, Rect } from 'react-konva';
import IAICanvasImage from './IAICanvasImage';
import { memo } from 'react';
const selector = createSelector(
[canvasSelector],
@ -88,4 +89,4 @@ const IAICanvasStagingArea = (props: Props) => {
);
};
export default IAICanvasStagingArea;
export default memo(IAICanvasStagingArea);

View File

@ -13,7 +13,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { isEqual } from 'lodash-es';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import {
@ -129,7 +129,9 @@ const IAICanvasStagingAreaToolbar = () => {
currentStagingAreaImage?.imageName ?? skipToken
);
if (!currentStagingAreaImage) return null;
if (!currentStagingAreaImage) {
return null;
}
return (
<Flex
@ -207,4 +209,4 @@ const IAICanvasStagingAreaToolbar = () => {
);
};
export default IAICanvasStagingAreaToolbar;
export default memo(IAICanvasStagingAreaToolbar);

View File

@ -7,6 +7,7 @@ import { isEqual } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import roundToHundreth from '../util/roundToHundreth';
import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos';
import { memo } from 'react';
const warningColor = 'var(--invokeai-colors-warning-500)';
@ -162,4 +163,4 @@ const IAICanvasStatusText = () => {
);
};
export default IAICanvasStatusText;
export default memo(IAICanvasStatusText);

View File

@ -10,6 +10,7 @@ import {
COLOR_PICKER_SIZE,
COLOR_PICKER_STROKE_RADIUS,
} from '../util/constants';
import { memo } from 'react';
const canvasBrushPreviewSelector = createSelector(
canvasSelector,
@ -134,7 +135,9 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
clip,
} = useAppSelector(canvasBrushPreviewSelector);
if (!shouldDrawBrushPreview) return null;
if (!shouldDrawBrushPreview) {
return null;
}
return (
<Group listening={false} {...clip} {...rest}>
@ -206,4 +209,4 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
);
};
export default IAICanvasToolPreview;
export default memo(IAICanvasToolPreview);

View File

@ -19,7 +19,7 @@ import { KonvaEventObject } from 'konva/lib/Node';
import { Vector2d } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Group, Rect, Transformer } from 'react-konva';
@ -85,7 +85,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
useState(false);
useEffect(() => {
if (!transformerRef.current || !shapeRef.current) return;
if (!transformerRef.current || !shapeRef.current) {
return;
}
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}, []);
@ -133,7 +135,9 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
* not its width and height. We need to un-scale the width and height before
* setting the values.
*/
if (!shapeRef.current) return;
if (!shapeRef.current) {
return;
}
const rect = shapeRef.current;
@ -313,4 +317,4 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
);
};
export default IAICanvasBoundingBox;
export default memo(IAICanvasBoundingBox);

View File

@ -20,6 +20,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { isEqual } from 'lodash-es';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@ -150,4 +151,4 @@ const IAICanvasMaskOptions = () => {
);
};
export default IAICanvasMaskOptions;
export default memo(IAICanvasMaskOptions);

View File

@ -18,7 +18,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { isEqual } from 'lodash-es';
import { ChangeEvent } from 'react';
import { ChangeEvent, memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa';
@ -163,4 +163,4 @@ const IAICanvasSettingsButtonPopover = () => {
);
};
export default IAICanvasSettingsButtonPopover;
export default memo(IAICanvasSettingsButtonPopover);

View File

@ -18,6 +18,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import { systemSelector } from 'features/system/store/systemSelectors';
import { clamp, isEqual } from 'lodash-es';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@ -252,4 +253,4 @@ const IAICanvasToolChooserOptions = () => {
);
};
export default IAICanvasToolChooserOptions;
export default memo(IAICanvasToolChooserOptions);

View File

@ -48,6 +48,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
import IAICanvasUndoButton from './IAICanvasUndoButton';
import { memo } from 'react';
export const selector = createSelector(
[systemSelector, canvasSelector, isStagingSelector],
@ -166,7 +167,9 @@ const IAICanvasToolbar = () => {
const handleResetCanvasView = (shouldScaleTo1 = false) => {
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) return;
if (!canvasBaseLayer) {
return;
}
const clientRect = canvasBaseLayer.getClientRect({
skipTransform: true,
});
@ -309,4 +312,4 @@ const IAICanvasToolbar = () => {
);
};
export default IAICanvasToolbar;
export default memo(IAICanvasToolbar);

View File

@ -32,13 +32,17 @@ const useCanvasDrag = () => {
return {
handleDragStart: useCallback(() => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
return;
}
dispatch(setIsMovingStage(true));
}, [dispatch, isMovingBoundingBox, isStaging, tool]),
handleDragMove: useCallback(
(e: KonvaEventObject<MouseEvent>) => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
return;
}
const newCoordinates = { x: e.target.x(), y: e.target.y() };
@ -48,7 +52,9 @@ const useCanvasDrag = () => {
),
handleDragEnd: useCallback(() => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) return;
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
return;
}
dispatch(setIsMovingStage(false));
}, [dispatch, isMovingBoundingBox, isStaging, tool]),
};

View File

@ -134,7 +134,9 @@ const useInpaintingCanvasHotkeys = () => {
useHotkeys(
['space'],
(e: KeyboardEvent) => {
if (e.repeat) return;
if (e.repeat) {
return;
}
canvasStage?.container().focus();

View File

@ -38,7 +38,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
return useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
if (!stageRef.current) return;
if (!stageRef.current) {
return;
}
stageRef.current.container().focus();
@ -54,7 +56,9 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition) return;
if (!scaledCursorPosition) {
return;
}
e.evt.preventDefault();

View File

@ -41,11 +41,15 @@ const useCanvasMouseMove = (
const { updateColorUnderCursor } = useColorPicker();
return useCallback(() => {
if (!stageRef.current) return;
if (!stageRef.current) {
return;
}
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition) return;
if (!scaledCursorPosition) {
return;
}
dispatch(setCursorPosition(scaledCursorPosition));
@ -56,7 +60,9 @@ const useCanvasMouseMove = (
return;
}
if (!isDrawing || tool === 'move' || isStaging) return;
if (!isDrawing || tool === 'move' || isStaging) {
return;
}
didMouseMoveRef.current = true;
dispatch(

View File

@ -47,7 +47,9 @@ const useCanvasMouseUp = (
if (!didMouseMoveRef.current && isDrawing && stageRef.current) {
const scaledCursorPosition = getScaledCursorPosition(stageRef.current);
if (!scaledCursorPosition) return;
if (!scaledCursorPosition) {
return;
}
/**
* Extend the current line.

View File

@ -35,13 +35,17 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
return useCallback(
(e: KonvaEventObject<WheelEvent>) => {
// stop default scrolling
if (!stageRef.current || isMoveStageKeyHeld) return;
if (!stageRef.current || isMoveStageKeyHeld) {
return;
}
e.evt.preventDefault();
const cursorPos = stageRef.current.getPointerPosition();
if (!cursorPos) return;
if (!cursorPos) {
return;
}
const mousePointTo = {
x: (cursorPos.x - stageRef.current.x()) / stageScale,

View File

@ -16,11 +16,15 @@ const useColorPicker = () => {
return {
updateColorUnderCursor: () => {
if (!stage || !canvasBaseLayer) return;
if (!stage || !canvasBaseLayer) {
return;
}
const position = stage.getPointerPosition();
if (!position) return;
if (!position) {
return;
}
const pixelRatio = Konva.pixelRatio;

View File

@ -397,7 +397,9 @@ export const canvasSlice = createSlice({
const { tool, layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
state;
if (tool === 'move' || tool === 'colorPicker') return;
if (tool === 'move' || tool === 'colorPicker') {
return;
}
const newStrokeWidth = brushSize / 2;
@ -434,14 +436,18 @@ export const canvasSlice = createSlice({
addPointToCurrentLine: (state, action: PayloadAction<number[]>) => {
const lastLine = state.layerState.objects.findLast(isCanvasAnyLine);
if (!lastLine) return;
if (!lastLine) {
return;
}
lastLine.points.push(...action.payload);
},
undo: (state) => {
const targetState = state.pastLayerStates.pop();
if (!targetState) return;
if (!targetState) {
return;
}
state.futureLayerStates.unshift(cloneDeep(state.layerState));
@ -454,7 +460,9 @@ export const canvasSlice = createSlice({
redo: (state) => {
const targetState = state.futureLayerStates.shift();
if (!targetState) return;
if (!targetState) {
return;
}
state.pastLayerStates.push(cloneDeep(state.layerState));

View File

@ -5,7 +5,9 @@ const getScaledCursorPosition = (stage: Stage) => {
const stageTransform = stage.getAbsoluteTransform().copy();
if (!pointerPosition || !stageTransform) return;
if (!pointerPosition || !stageTransform) {
return;
}
const scaledCursorPosition = stageTransform.invert().point(pointerPosition);

View File

@ -91,8 +91,8 @@ const ControlNet = (props: ControlNetProps) => {
>
<Flex sx={{ gap: 2, alignItems: 'center' }}>
<IAISwitch
tooltip={'Toggle this ControlNet'}
aria-label={'Toggle this ControlNet'}
tooltip="Toggle this ControlNet"
aria-label="Toggle this ControlNet"
isChecked={isEnabled}
onChange={handleToggleIsEnabled}
/>

View File

@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch';
import { isControlNetEnabledToggled } from 'features/controlNet/store/controlNetSlice';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
const selector = createSelector(
stateSelector,
@ -36,4 +36,4 @@ const ParamControlNetFeatureToggle = () => {
);
};
export default ParamControlNetFeatureToggle;
export default memo(ParamControlNetFeatureToggle);

View File

@ -23,7 +23,7 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
return (
<IAISlider
isDisabled={!isEnabled}
label={'Weight'}
label="Weight"
value={weight}
onChange={handleWeightChanged}
min={0}

View File

@ -8,6 +8,7 @@ import ParamDynamicPromptsCombinatorial from './ParamDynamicPromptsCombinatorial
import ParamDynamicPromptsToggle from './ParamDynamicPromptsEnabled';
import ParamDynamicPromptsMaxPrompts from './ParamDynamicPromptsMaxPrompts';
import { useFeatureStatus } from '../../system/hooks/useFeatureStatus';
import { memo } from 'react';
const selector = createSelector(
stateSelector,
@ -40,4 +41,4 @@ const ParamDynamicPromptsCollapse = () => {
);
};
export default ParamDynamicPromptsCollapse;
export default memo(ParamDynamicPromptsCollapse);

View File

@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { combinatorialToggled } from '../store/dynamicPromptsSlice';
const selector = createSelector(
@ -34,4 +34,4 @@ const ParamDynamicPromptsCombinatorial = () => {
);
};
export default ParamDynamicPromptsCombinatorial;
export default memo(ParamDynamicPromptsCombinatorial);

View File

@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { isEnabledToggled } from '../store/dynamicPromptsSlice';
const selector = createSelector(
@ -33,4 +33,4 @@ const ParamDynamicPromptsToggle = () => {
);
};
export default ParamDynamicPromptsToggle;
export default memo(ParamDynamicPromptsToggle);

View File

@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import {
maxPromptsChanged,
maxPromptsReset,
@ -60,4 +60,4 @@ const ParamDynamicPromptsMaxPrompts = () => {
);
};
export default ParamDynamicPromptsMaxPrompts;
export default memo(ParamDynamicPromptsMaxPrompts);

View File

@ -13,7 +13,7 @@ import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSe
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { forEach } from 'lodash-es';
import { PropsWithChildren, useCallback, useMemo, useRef } from 'react';
import { PropsWithChildren, memo, useCallback, useMemo, useRef } from 'react';
import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models';
import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants';
@ -118,7 +118,7 @@ const ParamEmbeddingPopover = (props: Props) => {
<IAIMantineSearchableSelect
inputRef={inputRef}
autoFocus
placeholder={'Add Embedding'}
placeholder="Add Embedding"
value={null}
data={data}
nothingFound="No matching Embeddings"
@ -140,4 +140,4 @@ const ParamEmbeddingPopover = (props: Props) => {
);
};
export default ParamEmbeddingPopover;
export default memo(ParamEmbeddingPopover);

View File

@ -1,4 +1,5 @@
import { Badge, Flex } from '@chakra-ui/react';
import { memo } from 'react';
const AutoAddIcon = () => {
return (
@ -20,4 +21,4 @@ const AutoAddIcon = () => {
);
};
export default AutoAddIcon;
export default memo(AutoAddIcon);

View File

@ -6,7 +6,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useRef } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
const selector = createSelector(
@ -66,7 +66,7 @@ const BoardAutoAddSelect = () => {
label="Auto-Add Board"
inputRef={inputRef}
autoFocus
placeholder={'Select a Board'}
placeholder="Select a Board"
value={autoAddBoardId}
data={boards}
nothingFound="No matching Boards"
@ -81,4 +81,4 @@ const BoardAutoAddSelect = () => {
);
};
export default BoardAutoAddSelect;
export default memo(BoardAutoAddSelect);

View File

@ -2,8 +2,12 @@ import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import {
IAIContextMenu,
IAIContextMenuProps,
} from 'common/components/IAIContextMenu';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { BoardId } from 'features/gallery/store/types';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName';
@ -11,80 +15,80 @@ import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
import { BoardId } from 'features/gallery/store/types';
type Props = {
board?: BoardDTO;
board_id: BoardId;
children: ContextMenuProps<HTMLDivElement>['children'];
children: IAIContextMenuProps<HTMLDivElement>['children'];
setBoardToDelete?: (board?: BoardDTO) => void;
};
const BoardContextMenu = memo(
({ board, board_id, setBoardToDelete, children }: Props) => {
const dispatch = useAppDispatch();
const BoardContextMenu = ({
board,
board_id,
setBoardToDelete,
children,
}: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(stateSelector, ({ gallery, system }) => {
const isAutoAdd = gallery.autoAddBoardId === board_id;
const isProcessing = system.isProcessing;
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
}),
[board_id]
);
const selector = useMemo(
() =>
createSelector(stateSelector, ({ gallery, system }) => {
const isAutoAdd = gallery.autoAddBoardId === board_id;
const isProcessing = system.isProcessing;
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
return { isAutoAdd, isProcessing, autoAssignBoardOnClick };
}),
[board_id]
);
const { isAutoAdd, isProcessing, autoAssignBoardOnClick } =
useAppSelector(selector);
const boardName = useBoardName(board_id);
const { isAutoAdd, isProcessing, autoAssignBoardOnClick } =
useAppSelector(selector);
const boardName = useBoardName(board_id);
const handleSetAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(board_id));
}, [board_id, dispatch]);
const handleSetAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(board_id));
}, [board_id, dispatch]);
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);
return (
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup title={boardName}>
<MenuItem
icon={<FaPlus />}
isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick}
onClick={handleSetAutoAdd}
>
Auto-add to this Board
</MenuItem>
{!board && <NoBoardContextMenuItems />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuGroup>
</MenuList>
)}
>
{children}
</ContextMenu>
);
}
);
return (
<IAIContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuGroup title={boardName}>
<MenuItem
icon={<FaPlus />}
isDisabled={isAutoAdd || isProcessing || autoAssignBoardOnClick}
onClick={handleSetAutoAdd}
>
Auto-add to this Board
</MenuItem>
{!board && <NoBoardContextMenuItems />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuGroup>
</MenuList>
)}
>
{children}
</IAIContextMenu>
);
};
BoardContextMenu.displayName = 'HoverableBoard';
export default BoardContextMenu;
export default memo(BoardContextMenu);

View File

@ -1,5 +1,5 @@
import IAIIconButton from 'common/components/IAIIconButton';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useCreateBoardMutation } from 'services/api/endpoints/boards';
@ -24,4 +24,4 @@ const AddBoardButton = () => {
);
};
export default AddBoardButton;
export default memo(AddBoardButton);

View File

@ -41,7 +41,7 @@ const BoardsList = (props: Props) => {
<>
<Collapse in={isOpen} animateOpacity>
<Flex
layerStyle={'first'}
layerStyle="first"
sx={{
flexDir: 'column',
gap: 2,

View File

@ -39,187 +39,188 @@ interface GalleryBoardProps {
setBoardToDelete: (board?: BoardDTO) => void;
}
const GalleryBoard = memo(
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery, system }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
const isProcessing = system.isProcessing;
const GalleryBoard = ({
board,
isSelected,
setBoardToDelete,
}: GalleryBoardProps) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery, system }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
const autoAssignBoardOnClick = gallery.autoAssignBoardOnClick;
const isProcessing = system.isProcessing;
return {
isSelectedForAutoAdd,
autoAssignBoardOnClick,
isProcessing,
};
},
defaultSelectorOptions
),
[board.board_id]
);
return {
isSelectedForAutoAdd,
autoAssignBoardOnClick,
isProcessing,
};
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } =
useAppSelector(selector);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { isSelectedForAutoAdd, autoAssignBoardOnClick, isProcessing } =
useAppSelector(selector);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
const tooltip = useMemo(() => {
if (!imagesTotal || !assetsTotal) {
return undefined;
const { data: imagesTotal } = useGetBoardImagesTotalQuery(board.board_id);
const { data: assetsTotal } = useGetBoardAssetsTotalQuery(board.board_id);
const tooltip = useMemo(() => {
if (!imagesTotal || !assetsTotal) {
return undefined;
}
return `${imagesTotal} image${
imagesTotal > 1 ? 's' : ''
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`;
}, [assetsTotal, imagesTotal]);
const { currentData: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken
);
const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name);
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id));
if (autoAssignBoardOnClick && !isProcessing) {
dispatch(autoAddBoardIdChanged(board_id));
}
}, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
useUpdateBoardMutation();
const droppableData: AddToBoardDropData = useMemo(
() => ({
id: board_id,
actionType: 'ADD_TO_BOARD',
context: { boardId: board_id },
}),
[board_id]
);
const handleSubmit = useCallback(
async (newBoardName: string) => {
// empty strings are not allowed
if (!newBoardName.trim()) {
setLocalBoardName(board_name);
return;
}
return `${imagesTotal} image${
imagesTotal > 1 ? 's' : ''
}, ${assetsTotal} asset${assetsTotal > 1 ? 's' : ''}`;
}, [assetsTotal, imagesTotal]);
const { currentData: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken
);
const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name);
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id));
if (autoAssignBoardOnClick && !isProcessing) {
dispatch(autoAddBoardIdChanged(board_id));
// don't updated the board name if it hasn't changed
if (newBoardName === board_name) {
return;
}
}, [board_id, autoAssignBoardOnClick, isProcessing, dispatch]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
useUpdateBoardMutation();
try {
const { board_name } = await updateBoard({
board_id,
changes: { board_name: newBoardName },
}).unwrap();
const droppableData: AddToBoardDropData = useMemo(
() => ({
id: board_id,
actionType: 'ADD_TO_BOARD',
context: { boardId: board_id },
}),
[board_id]
);
// update local state
setLocalBoardName(board_name);
} catch {
// revert on error
setLocalBoardName(board_name);
}
},
[board_id, board_name, updateBoard]
);
const handleSubmit = useCallback(
async (newBoardName: string) => {
// empty strings are not allowed
if (!newBoardName.trim()) {
setLocalBoardName(board_name);
return;
}
const handleChange = useCallback((newBoardName: string) => {
setLocalBoardName(newBoardName);
}, []);
// don't updated the board name if it hasn't changed
if (newBoardName === board_name) {
return;
}
try {
const { board_name } = await updateBoard({
board_id,
changes: { board_name: newBoardName },
}).unwrap();
// update local state
setLocalBoardName(board_name);
} catch {
// revert on error
setLocalBoardName(board_name);
}
},
[board_id, board_name, updateBoard]
);
const handleChange = useCallback((newBoardName: string) => {
setLocalBoardName(newBoardName);
}, []);
return (
<Box
sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}
return (
<Box sx={{ w: 'full', h: 'full', touchAction: 'none', userSelect: 'none' }}>
<Flex
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
aspectRatio: '1/1',
w: 'full',
h: 'full',
}}
>
<Flex
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
aspectRatio: '1/1',
w: 'full',
h: 'full',
}}
<BoardContextMenu
board={board}
board_id={board_id}
setBoardToDelete={setBoardToDelete}
>
<BoardContextMenu
board={board}
board_id={board_id}
setBoardToDelete={setBoardToDelete}
>
{(ref) => (
<Tooltip label={tooltip} openDelay={1000} hasArrow>
<Flex
ref={ref}
onClick={handleSelectBoard}
sx={{
w: 'full',
h: 'full',
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}}
>
{coverImage?.thumbnail_url ? (
<Image
src={coverImage?.thumbnail_url}
draggable={false}
{(ref) => (
<Tooltip label={tooltip} openDelay={1000} hasArrow>
<Flex
ref={ref}
onClick={handleSelectBoard}
sx={{
w: 'full',
h: 'full',
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
cursor: 'pointer',
bg: 'base.200',
_dark: {
bg: 'base.800',
},
}}
>
{coverImage?.thumbnail_url ? (
<Image
src={coverImage?.thumbnail_url}
draggable={false}
sx={{
objectFit: 'cover',
w: 'full',
h: 'full',
maxH: 'full',
borderRadius: 'base',
borderBottomRadius: 'lg',
}}
/>
) : (
<Flex
sx={{
w: 'full',
h: 'full',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Icon
boxSize={12}
as={FaUser}
sx={{
objectFit: 'cover',
w: 'full',
h: 'full',
maxH: 'full',
borderRadius: 'base',
borderBottomRadius: 'lg',
mt: -6,
opacity: 0.7,
color: 'base.500',
_dark: {
color: 'base.500',
},
}}
/>
) : (
<Flex
sx={{
w: 'full',
h: 'full',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Icon
boxSize={12}
as={FaUser}
sx={{
mt: -6,
opacity: 0.7,
color: 'base.500',
_dark: {
color: 'base.500',
},
}}
/>
</Flex>
)}
{/* <Flex
</Flex>
)}
{/* <Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
@ -231,80 +232,77 @@ const GalleryBoard = memo(
{totalImages}/{totalAssets}
</Badge>
</Flex> */}
{isSelectedForAutoAdd && <AutoAddIcon />}
<SelectionOverlay
isSelected={isSelected}
isHovered={isHovered}
/>
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 1,
justifyContent: 'center',
alignItems: 'center',
w: 'full',
maxW: 'full',
borderBottomRadius: 'base',
bg: isSelected ? 'accent.400' : 'base.500',
{isSelectedForAutoAdd && <AutoAddIcon />}
<SelectionOverlay
isSelected={isSelected}
isHovered={isHovered}
/>
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 1,
justifyContent: 'center',
alignItems: 'center',
w: 'full',
maxW: 'full',
borderBottomRadius: 'base',
bg: isSelected ? 'accent.400' : 'base.500',
color: isSelected ? 'base.50' : 'base.100',
_dark: {
bg: isSelected ? 'accent.500' : 'base.600',
color: isSelected ? 'base.50' : 'base.100',
_dark: {
bg: isSelected ? 'accent.500' : 'base.600',
color: isSelected ? 'base.50' : 'base.100',
},
lineHeight: 'short',
fontSize: 'xs',
},
lineHeight: 'short',
fontSize: 'xs',
}}
>
<Editable
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
sx={{
w: 'full',
}}
>
<Editable
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
<EditablePreview
sx={{
w: 'full',
p: 0,
fontWeight: isSelected ? 700 : 500,
textAlign: 'center',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
<EditablePreview
sx={{
noOfLines={1}
/>
<EditableInput
sx={{
p: 0,
_focusVisible: {
p: 0,
fontWeight: isSelected ? 700 : 500,
textAlign: 'center',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
noOfLines={1}
/>
<EditableInput
sx={{
p: 0,
_focusVisible: {
p: 0,
textAlign: 'center',
// get rid of the edit border
boxShadow: 'none',
},
}}
/>
</Editable>
</Flex>
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
/>
// get rid of the edit border
boxShadow: 'none',
},
}}
/>
</Editable>
</Flex>
</Tooltip>
)}
</BoardContextMenu>
</Flex>
</Box>
);
}
);
GalleryBoard.displayName = 'HoverableBoard';
<IAIDroppable
data={droppableData}
dropLabel={<Text fontSize="md">Move</Text>}
/>
</Flex>
</Tooltip>
)}
</BoardContextMenu>
</Flex>
</Box>
);
};
export default GalleryBoard;
export default memo(GalleryBoard);

View File

@ -3,7 +3,7 @@ import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { TypesafeDroppableData } from 'features/dnd/types';
import { BoardId } from 'features/gallery/store/types';
import { ReactNode } from 'react';
import { ReactNode, memo } from 'react';
import BoardContextMenu from '../BoardContextMenu';
type GenericBoardProps = {
@ -105,4 +105,4 @@ const GenericBoard = (props: GenericBoardProps) => {
);
};
export default GenericBoard;
export default memo(GenericBoard);

View File

@ -156,4 +156,4 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
NoBoardBoard.displayName = 'HoverableBoard';
export default NoBoardBoard;
export default memo(NoBoardBoard);

View File

@ -26,7 +26,7 @@ import {
setShouldShowImageDetails,
setShouldShowProgressInViewer,
} from 'features/ui/store/uiSlice';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import {
@ -323,4 +323,4 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
);
};
export default CurrentImageButtons;
export default memo(CurrentImageButtons);

View File

@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
import { memo } from 'react';
const CurrentImageDisplay = () => {
return (
@ -22,4 +23,4 @@ const CurrentImageDisplay = () => {
);
};
export default CurrentImageDisplay;
export default memo(CurrentImageDisplay);

View File

@ -1,4 +1,5 @@
import { Flex } from '@chakra-ui/react';
import { memo } from 'react';
import { FaEyeSlash } from 'react-icons/fa';
const CurrentImageHidden = () => {
@ -18,4 +19,4 @@ const CurrentImageHidden = () => {
);
};
export default CurrentImageHidden;
export default memo(CurrentImageHidden);

View File

@ -5,6 +5,7 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
@ -41,4 +42,4 @@ const GalleryPinButton = () => {
);
};
export default GalleryPinButton;
export default memo(GalleryPinButton);

View File

@ -12,7 +12,7 @@ import {
setGalleryImageMinimumWidth,
shouldAutoSwitchChanged,
} from 'features/gallery/store/gallerySlice';
import { ChangeEvent, useCallback } from 'react';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa';
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
@ -101,4 +101,4 @@ const GallerySettingsPopover = () => {
);
};
export default GallerySettingsPopover;
export default memo(GallerySettingsPopover);

View File

@ -5,7 +5,7 @@ import {
isModalOpenChanged,
} from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { FaFolder, FaTrash } from 'react-icons/fa';
import { MdStar, MdStarBorder } from 'react-icons/md';
import {
@ -74,4 +74,4 @@ const MultipleSelectionMenuItems = () => {
);
};
export default MultipleSelectionMenuItems;
export default memo(MultipleSelectionMenuItems);

View File

@ -136,11 +136,15 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
}, [copyImageToClipboard, imageDTO.image_url]);
const handleStarImage = useCallback(() => {
if (imageDTO) starImages({ imageDTOs: [imageDTO] });
if (imageDTO) {
starImages({ imageDTOs: [imageDTO] });
}
}, [starImages, imageDTO]);
const handleUnstarImage = useCallback(() => {
if (imageDTO) unstarImages({ imageDTOs: [imageDTO] });
if (imageDTO) {
unstarImages({ imageDTOs: [imageDTO] });
}
}, [unstarImages, imageDTO]);
return (

View File

@ -1,4 +1,5 @@
import { Flex, Spinner, SpinnerProps } from '@chakra-ui/react';
import { memo } from 'react';
type ImageFallbackSpinnerProps = SpinnerProps;
@ -23,4 +24,4 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
);
};
export default ImageFallbackSpinner;
export default memo(ImageFallbackSpinner);

View File

@ -88,8 +88,12 @@ const GalleryImage = (props: HoverableImageProps) => {
}, []);
const starIcon = useMemo(() => {
if (imageDTO?.starred) return <MdStar size="20" />;
if (!imageDTO?.starred && isHovered) return <MdStarBorder size="20" />;
if (imageDTO?.starred) {
return <MdStar size="20" />;
}
if (!imageDTO?.starred && isHovered) {
return <MdStarBorder size="20" />;
}
}, [imageDTO?.starred, isHovered]);
if (!imageDTO) {

View File

@ -1,5 +1,5 @@
import { Box, FlexProps, forwardRef } from '@chakra-ui/react';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, memo } from 'react';
type ItemContainerProps = PropsWithChildren & FlexProps;
const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
@ -8,4 +8,4 @@ const ItemContainer = forwardRef((props: ItemContainerProps, ref) => (
</Box>
));
export default ItemContainer;
export default memo(ItemContainer);

View File

@ -1,7 +1,7 @@
import { FlexProps, Grid, forwardRef } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, memo } from 'react';
type ListContainerProps = PropsWithChildren & FlexProps;
const ListContainer = forwardRef((props: ListContainerProps, ref) => {
@ -23,4 +23,4 @@ const ListContainer = forwardRef((props: ListContainerProps, ref) => {
);
});
export default ListContainer;
export default memo(ListContainer);

View File

@ -1,34 +1,35 @@
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
import { isString } from 'lodash-es';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { FaCopy, FaSave } from 'react-icons/fa';
type Props = {
label: string;
jsonObject: object;
data: object | string;
fileName?: string;
};
const ImageMetadataJSON = (props: Props) => {
const { label, jsonObject, fileName } = props;
const jsonString = useMemo(
() => JSON.stringify(jsonObject, null, 2),
[jsonObject]
const DataViewer = (props: Props) => {
const { label, data, fileName } = props;
const dataString = useMemo(
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
[data]
);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(jsonString);
}, [jsonString]);
navigator.clipboard.writeText(dataString);
}, [dataString]);
const handleSave = useCallback(() => {
const blob = new Blob([jsonString]);
const blob = new Blob([dataString]);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${fileName || label}.json`;
document.body.appendChild(a);
a.click();
a.remove();
}, [jsonString, label, fileName]);
}, [dataString, label, fileName]);
return (
<Flex
@ -65,7 +66,7 @@ const ImageMetadataJSON = (props: Props) => {
},
}}
>
<pre>{jsonString}</pre>
<pre>{dataString}</pre>
</OverlayScrollbarsComponent>
</Box>
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
@ -92,4 +93,4 @@ const ImageMetadataJSON = (props: Props) => {
);
};
export default ImageMetadataJSON;
export default memo(DataViewer);

View File

@ -1,5 +1,5 @@
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { UnsafeImageMetadata } from 'services/api/types';
import ImageMetadataItem from './ImageMetadataItem';
@ -206,4 +206,4 @@ const ImageMetadataActions = (props: Props) => {
);
};
export default ImageMetadataActions;
export default memo(ImageMetadataActions);

View File

@ -1,5 +1,6 @@
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
@ -74,4 +75,4 @@ const ImageMetadataItem = ({
);
};
export default ImageMetadataItem;
export default memo(ImageMetadataItem);

View File

@ -16,7 +16,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types';
import { useDebounce } from 'use-debounce';
import ImageMetadataActions from './ImageMetadataActions';
import ImageMetadataJSON from './ImageMetadataJSON';
import DataViewer from './DataViewer';
type ImageMetadataViewerProps = {
image: ImageDTO;
@ -79,21 +79,21 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
<TabPanels>
<TabPanel>
{metadata ? (
<ImageMetadataJSON jsonObject={metadata} label="Core Metadata" />
<DataViewer data={metadata} label="Core Metadata" />
) : (
<IAINoContentFallback label="No core metadata found" />
)}
</TabPanel>
<TabPanel>
{image ? (
<ImageMetadataJSON jsonObject={image} label="Image Details" />
<DataViewer data={image} label="Image Details" />
) : (
<IAINoContentFallback label="No image details found" />
)}
</TabPanel>
<TabPanel>
{graph ? (
<ImageMetadataJSON jsonObject={graph} label="Graph" />
<DataViewer data={graph} label="Graph" />
) : (
<IAINoContentFallback label="No graph found" />
)}

View File

@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { map } from 'lodash-es';
import ParamLora from './ParamLora';
import { memo } from 'react';
const selector = createSelector(
stateSelector,
@ -29,4 +30,4 @@ const ParamLoraList = () => {
);
};
export default ParamLoraList;
export default memo(ParamLoraList);

View File

@ -9,7 +9,7 @@ import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectI
import { loraAdded } from 'features/lora/store/loraSlice';
import { MODEL_TYPE_MAP } from 'features/parameters/types/constants';
import { forEach } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useGetLoRAModelsQuery } from 'services/api/endpoints/models';
const selector = createSelector(
@ -102,4 +102,4 @@ const ParamLoRASelect = () => {
);
};
export default ParamLoRASelect;
export default memo(ParamLoRASelect);

View File

@ -1,140 +0,0 @@
import { Flex, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppToaster } from 'app/components/Toaster';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { map } from 'lodash-es';
import { forwardRef, useCallback } from 'react';
import 'reactflow/dist/style.css';
import { AnyInvocationType } from 'services/events/types';
import { useBuildNodeData } from '../hooks/useBuildNodeData';
import { nodeAdded } from '../store/nodesSlice';
type NodeTemplate = {
label: string;
value: string;
description: string;
tags: string[];
};
const selector = createSelector(
[stateSelector],
({ nodes }) => {
const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => {
return {
label: template.title,
value: template.type,
description: template.description,
tags: template.tags,
};
});
data.push({
label: 'Progress Image',
value: 'current_image',
description: 'Displays the current image in the Node Editor',
tags: ['progress'],
});
data.push({
label: 'Notes',
value: 'notes',
description: 'Add notes about your workflow',
tags: ['notes'],
});
return { data };
},
defaultSelectorOptions
);
const AddNodeMenu = () => {
const dispatch = useAppDispatch();
const { data } = useAppSelector(selector);
const buildInvocation = useBuildNodeData();
const toaster = useAppToaster();
const addNode = useCallback(
(nodeType: AnyInvocationType) => {
const invocation = buildInvocation(nodeType);
if (!invocation) {
toaster({
status: 'error',
title: `Unknown Invocation type ${nodeType}`,
});
return;
}
dispatch(nodeAdded(invocation));
},
[dispatch, buildInvocation, toaster]
);
const handleChange = useCallback(
(v: string | null) => {
if (!v) {
return;
}
addNode(v as AnyInvocationType);
},
[addNode]
);
return (
<Flex sx={{ gap: 2, alignItems: 'center' }}>
<IAIMantineSearchableSelect
selectOnBlur={false}
placeholder="Add Node"
value={null}
data={data}
maxDropdownHeight={400}
nothingFound="No matching nodes"
itemComponent={SelectItem}
filter={(value, item: NodeTemplate) =>
item.label.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
item.description.toLowerCase().includes(value.toLowerCase().trim()) ||
item.tags.includes(value.toLowerCase().trim())
}
onChange={handleChange}
sx={{
width: '24rem',
}}
/>
</Flex>
);
};
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
value: string;
label: string;
description: string;
}
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
({ label, description, ...others }: ItemProps, ref) => {
return (
<div ref={ref} {...others}>
<div>
<Text fontWeight={600}>{label}</Text>
<Text
size="xs"
sx={{ color: 'base.600', _dark: { color: 'base.500' } }}
>
{description}
</Text>
</div>
</div>
);
}
);
SelectItem.displayName = 'SelectItem';
export default AddNodeMenu;

View File

@ -1,199 +0,0 @@
import { Badge, 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 { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo, useMemo } from 'react';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from 'reactflow';
import { FIELDS, colorTokenToCssVar } from '../types/constants';
import { isInvocationNode } from '../types/types';
const makeEdgeSelector = (
source: string,
sourceHandleId: string | null | undefined,
target: string,
targetHandleId: string | null | undefined,
selected?: boolean
) =>
createSelector(
stateSelector,
({ nodes }) => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
const isInvocationToInvocationEdge =
isInvocationNode(sourceNode) && isInvocationNode(targetNode);
const isSelected =
sourceNode?.selected || targetNode?.selected || selected;
const sourceType = isInvocationToInvocationEdge
? sourceNode?.data?.outputs[sourceHandleId || '']?.type
: undefined;
const stroke =
sourceType && nodes.shouldColorEdges
? colorTokenToCssVar(FIELDS[sourceType].color)
: colorTokenToCssVar('base.500');
return {
isSelected,
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
stroke,
};
},
defaultSelectorOptions
);
const CollapsedEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
data,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps<{ count: number }>) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[selected, source, sourceHandleId, target, targetHandleId]
);
const { isSelected, shouldAnimate } = useAppSelector(selector);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const { base500 } = useChakraThemeTokens();
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
{data?.count && data.count > 1 && (
<EdgeLabelRenderer>
<Flex
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
className="nodrag nopan"
>
<Badge
variant="solid"
sx={{
bg: 'base.500',
opacity: isSelected ? 0.8 : 0.5,
boxShadow: 'base',
}}
>
{data.count}
</Badge>
</Flex>
</EdgeLabelRenderer>
)}
</>
);
}
);
CollapsedEdge.displayName = 'CollapsedEdge';
const DefaultEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[source, sourceHandleId, target, targetHandleId, selected]
);
const { isSelected, shouldAnimate, stroke } = useAppSelector(selector);
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
);
}
);
DefaultEdge.displayName = 'DefaultEdge';
export const edgeTypes = {
collapsed: CollapsedEdge,
default: DefaultEdge,
};

View File

@ -1,9 +0,0 @@
import CurrentImageNode from './nodes/CurrentImageNode';
import InvocationNodeWrapper from './nodes/InvocationNodeWrapper';
import NotesNode from './nodes/NotesNode';
export const nodeTypes = {
invocation: InvocationNodeWrapper,
current_image: CurrentImageNode,
notes: NotesNode,
};

View File

@ -1,69 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import IAISwitch from 'common/components/IAISwitch';
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
import { InvocationNodeData } from 'features/nodes/types/types';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaBars } from 'react-icons/fa';
interface Props {
data: InvocationNodeData;
}
const NodeSettings = (props: Props) => {
const { data } = props;
const dispatch = useAppDispatch();
const handleChangeIsIntermediate = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(
fieldBooleanValueChanged({
nodeId: data.id,
fieldName: 'is_intermediate',
value: e.target.checked,
})
);
},
[data.id, dispatch]
);
return (
<IAIPopover
isLazy={false}
triggerComponent={
<IAIIconButton
className="nopan"
aria-label="Node Settings"
variant="link"
sx={{
minW: 8,
color: 'base.500',
_dark: {
color: 'base.500',
},
_hover: {
color: 'base.700',
_dark: {
color: 'base.300',
},
},
}}
icon={<FaBars />}
/>
}
>
<Flex sx={{ flexDir: 'column', gap: 4, w: 64 }}>
<IAISwitch
label="Intermediate"
isChecked={Boolean(data.inputs['is_intermediate']?.value)}
onChange={handleChangeIsIntermediate}
helperText="The outputs of intermediate nodes are considered temporary objects. Intermediate images are not added to the gallery."
/>
</Flex>
</IAIPopover>
);
};
export default memo(NodeSettings);

View File

@ -6,9 +6,10 @@ import { memo, useState } from 'react';
import { MdDeviceHub } from 'react-icons/md';
import { Panel, PanelGroup } from 'react-resizable-panels';
import 'reactflow/dist/style.css';
import NodeEditorPanelGroup from './panel/NodeEditorPanelGroup';
import { Flow } from './Flow';
import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup';
import { Flow } from './flow/Flow';
import { AnimatePresence, motion } from 'framer-motion';
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
const NodeEditor = () => {
const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
@ -33,7 +34,7 @@ const NodeEditor = () => {
/>
<Panel id="node-editor-content">
<Flex
layerStyle={'first'}
layerStyle="first"
sx={{
position: 'relative',
width: 'full',
@ -57,9 +58,10 @@ const NodeEditor = () => {
opacity: 0,
transition: { duration: 0.2 },
}}
style={{ width: '100%', height: '100%' }}
style={{ position: 'relative', width: '100%', height: '100%' }}
>
<Flow />
<AddNodePopover />
</motion.div>
)}
</AnimatePresence>
@ -80,7 +82,7 @@ const NodeEditor = () => {
style={{ position: 'absolute', width: '100%', height: '100%' }}
>
<Flex
layerStyle={'first'}
layerStyle="first"
sx={{
position: 'relative',
width: 'full',

View File

@ -1,26 +0,0 @@
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
import { omit } from 'lodash-es';
import { useMemo } from 'react';
import { useDebounce } from 'use-debounce';
import { buildNodesGraph } from '../util/graphBuilders/buildNodesGraph';
const useNodesGraph = () => {
const nodes = useAppSelector((state: RootState) => state.nodes);
const [debouncedNodes] = useDebounce(nodes, 300);
const graph = useMemo(
() => omit(buildNodesGraph(debouncedNodes), 'id'),
[debouncedNodes]
);
return graph;
};
const NodeGraph = () => {
const graph = useNodesGraph();
return <ImageMetadataJSON jsonObject={graph} label="Graph" />;
};
export default NodeGraph;

View File

@ -1,11 +0,0 @@
import { memo } from 'react';
import { Panel } from 'reactflow';
import AddNodeMenu from '../AddNodeMenu';
const TopLeftPanel = () => (
<Panel position="top-left">
<AddNodeMenu />
</Panel>
);
export default memo(TopLeftPanel);

View File

@ -1,47 +0,0 @@
import { MenuItem, MenuList } from '@chakra-ui/react';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import {
InputFieldTemplate,
InputFieldValue,
} from 'features/nodes/types/types';
import { MouseEvent, useCallback } from 'react';
import { menuListMotionProps } from 'theme/components/menu';
type Props = {
nodeId: string;
field: InputFieldValue;
fieldTemplate: InputFieldTemplate;
children: ContextMenuProps<HTMLDivElement>['children'];
};
const FieldContextMenu = (props: Props) => {
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);
return (
<ContextMenu<HTMLDivElement>
menuProps={{
size: 'sm',
isLazy: true,
}}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
onContextMenu={skipEvent}
>
<MenuItem>Test</MenuItem>
</MenuList>
)}
>
{props.children}
</ContextMenu>
);
};
export default FieldContextMenu;

View File

@ -1,139 +0,0 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import {
useDoesInputHaveValue,
useFieldTemplate,
} from 'features/nodes/hooks/useNodeData';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { PropsWithChildren, memo, useMemo } from 'react';
import FieldHandle from './FieldHandle';
import FieldTitle from './FieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
interface Props {
nodeId: string;
fieldName: string;
}
const InputField = ({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const {
isConnected,
isConnectionInProgress,
isConnectionStartField,
connectionError,
shouldDim,
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
const isMissingInput = useMemo(() => {
if (fieldTemplate?.fieldKind !== 'input') {
return false;
}
if (!fieldTemplate.required) {
return false;
}
if (!isConnected && fieldTemplate.input === 'connection') {
return true;
}
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input === 'any') {
return true;
}
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
if (fieldTemplate?.fieldKind !== 'input') {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
>
Unknown input: {fieldName}
</FormControl>
</InputFieldWrapper>
);
}
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
as={Flex}
isInvalid={isMissingInput}
isDisabled={isConnected}
sx={{
alignItems: 'center',
justifyContent: 'space-between',
ps: 2,
gap: 2,
}}
>
<Tooltip
label={
<FieldTooltipContent
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
hasArrow
>
<FormLabel sx={{ mb: 0 }}>
<FieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="input"
isDraggable
/>
</FormLabel>
</Tooltip>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</FormControl>
{fieldTemplate.input !== 'direct' && (
<FieldHandle
fieldTemplate={fieldTemplate}
handleType="target"
isConnectionInProgress={isConnectionInProgress}
isConnectionStartField={isConnectionStartField}
connectionError={connectionError}
/>
)}
</InputFieldWrapper>
);
};
export default InputField;
type InputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const InputFieldWrapper = memo(
({ shouldDim, children }: InputFieldWrapperProps) => (
<Flex
className="nopan"
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
w: 'full',
h: 'full',
}}
>
{children}
</Flex>
)
);
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@ -1,54 +0,0 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo } from 'react';
import FieldTitle from './FieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
type Props = {
nodeId: string;
fieldName: string;
};
const LinearViewField = ({ nodeId, fieldName }: Props) => {
return (
<Flex
layerStyle="second"
sx={{
position: 'relative',
borderRadius: 'base',
w: 'full',
p: 2,
}}
>
<FormControl as={Flex} sx={{ flexDir: 'column', gap: 1, flexShrink: 1 }}>
<Tooltip
label={
<FieldTooltipContent
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
hasArrow
>
<FormLabel
sx={{
display: 'flex',
justifyContent: 'space-between',
mb: 0,
}}
>
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
</FormLabel>
</Tooltip>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</FormControl>
</Flex>
);
};
export default memo(LinearViewField);

View File

@ -1,13 +0,0 @@
import {
InputFieldTemplate,
InputFieldValue,
} from 'features/nodes/types/types';
export type FieldComponentProps<
V extends InputFieldValue,
T extends InputFieldTemplate
> = {
nodeId: string;
field: V;
fieldTemplate: T;
};

View File

@ -0,0 +1,205 @@
import {
Flex,
Popover,
PopoverAnchor,
PopoverBody,
PopoverContent,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppToaster } from 'app/components/Toaster';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData';
import {
addNodePopoverClosed,
addNodePopoverOpened,
nodeAdded,
} from 'features/nodes/store/nodesSlice';
import { map } from 'lodash-es';
import { memo, useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import 'reactflow/dist/style.css';
import { AnyInvocationType } from 'services/events/types';
import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem';
type NodeTemplate = {
label: string;
value: string;
description: string;
tags: string[];
};
const filter = (value: string, item: NodeTemplate) => {
const regex = new RegExp(
value
.trim()
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
.split(' ')
.join('.*'),
'gi'
);
return (
regex.test(item.label) ||
regex.test(item.description) ||
item.tags.some((tag) => regex.test(tag))
);
};
const selector = createSelector(
[stateSelector],
({ nodes }) => {
const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => {
return {
label: template.title,
value: template.type,
description: template.description,
tags: template.tags,
};
});
data.push({
label: 'Progress Image',
value: 'current_image',
description: 'Displays the current image in the Node Editor',
tags: ['progress'],
});
data.push({
label: 'Notes',
value: 'notes',
description: 'Add notes about your workflow',
tags: ['notes'],
});
data.sort((a, b) => a.label.localeCompare(b.label));
return { data };
},
defaultSelectorOptions
);
const AddNodePopover = () => {
const dispatch = useAppDispatch();
const buildInvocation = useBuildNodeData();
const toaster = useAppToaster();
const { data } = useAppSelector(selector);
const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen);
const inputRef = useRef<HTMLInputElement>(null);
const addNode = useCallback(
(nodeType: AnyInvocationType) => {
const invocation = buildInvocation(nodeType);
if (!invocation) {
toaster({
status: 'error',
title: `Unknown Invocation type ${nodeType}`,
});
return;
}
dispatch(nodeAdded(invocation));
},
[dispatch, buildInvocation, toaster]
);
const handleChange = useCallback(
(v: string | null) => {
if (!v) {
return;
}
addNode(v as AnyInvocationType);
},
[addNode]
);
const onClose = useCallback(() => {
dispatch(addNodePopoverClosed());
}, [dispatch]);
const onOpen = useCallback(() => {
dispatch(addNodePopoverOpened());
}, [dispatch]);
const handleHotkeyOpen: HotkeyCallback = useCallback(
(e) => {
e.preventDefault();
onOpen();
setTimeout(() => {
inputRef.current?.focus();
}, 0);
},
[onOpen]
);
const handleHotkeyClose: HotkeyCallback = useCallback(() => {
onClose();
}, [onClose]);
useHotkeys(['space', '/'], handleHotkeyOpen);
useHotkeys(['escape'], handleHotkeyClose);
return (
<Popover
initialFocusRef={inputRef}
isOpen={isOpen}
onClose={onClose}
placement="bottom"
openDelay={0}
closeDelay={0}
closeOnBlur={true}
returnFocusOnClose={true}
>
<PopoverAnchor>
<Flex
sx={{
position: 'absolute',
top: '15%',
insetInlineStart: '50%',
pointerEvents: 'none',
}}
/>
</PopoverAnchor>
<PopoverContent
sx={{
p: 0,
top: -1,
shadow: 'dark-lg',
borderColor: 'accent.300',
borderWidth: '2px',
borderStyle: 'solid',
_dark: { borderColor: 'accent.400' },
}}
>
<PopoverBody sx={{ p: 0 }}>
<IAIMantineSearchableSelect
inputRef={inputRef}
selectOnBlur={false}
placeholder="Search for nodes"
value={null}
data={data}
maxDropdownHeight={400}
nothingFound="No matching nodes"
itemComponent={AddNodePopoverSelectItem}
filter={filter}
onChange={handleChange}
hoverOnSearchChange={true}
onDropdownClose={onClose}
sx={{
width: '32rem',
input: {
padding: '0.5rem',
},
}}
/>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(AddNodePopover);

View File

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

View File

@ -2,6 +2,7 @@ import { useToken } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { contextMenusClosed } from 'features/ui/store/uiSlice';
import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
Background,
OnConnect,
@ -16,7 +17,7 @@ import {
ProOptions,
ReactFlow,
} from 'reactflow';
import { useIsValidConnection } from '../hooks/useIsValidConnection';
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
import {
connectionEnded,
connectionMade,
@ -25,18 +26,37 @@ import {
edgesDeleted,
nodesChanged,
nodesDeleted,
selectedAll,
selectedEdgesChanged,
selectedNodesChanged,
selectionCopied,
selectionPasted,
viewportChanged,
} from '../store/nodesSlice';
import { CustomConnectionLine } from './CustomConnectionLine';
import { edgeTypes } from './CustomEdges';
import { nodeTypes } from './CustomNodes';
import BottomLeftPanel from './editorPanels/BottomLeftPanel';
import MinimapPanel from './editorPanels/MinimapPanel';
import TopCenterPanel from './editorPanels/TopCenterPanel';
import TopLeftPanel from './editorPanels/TopLeftPanel';
import TopRightPanel from './editorPanels/TopRightPanel';
} from '../../store/nodesSlice';
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode';
import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper';
import NotesNode from './nodes/Notes/NotesNode';
import BottomLeftPanel from './panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './panels/MinimapPanel/MinimapPanel';
import TopCenterPanel from './panels/TopCenterPanel/TopCenterPanel';
import TopLeftPanel from './panels/TopLeftPanel/TopLeftPanel';
import TopRightPanel from './panels/TopRightPanel/TopRightPanel';
const DELETE_KEYS = ['Delete', 'Backspace'];
const edgeTypes = {
collapsed: InvocationCollapsedEdge,
default: InvocationDefaultEdge,
};
const nodeTypes = {
invocation: InvocationNodeWrapper,
current_image: CurrentImageNode,
notes: NotesNode,
};
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app
const proOptions: ProOptions = { hideAttribution: true };
@ -119,8 +139,24 @@ export const Flow = () => {
dispatch(contextMenusClosed());
}, [dispatch]);
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
e.preventDefault();
dispatch(selectionCopied());
});
useHotkeys(['Ctrl+a', 'Meta+a'], (e) => {
e.preventDefault();
dispatch(selectedAll());
});
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
e.preventDefault();
dispatch(selectionPasted());
});
return (
<ReactFlow
id="workflow-editor"
defaultViewport={viewport}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
@ -144,6 +180,7 @@ export const Flow = () => {
proOptions={proOptions}
style={{ borderRadius }}
onPaneClick={handlePaneClick}
deleteKeyCode={DELETE_KEYS}
>
<TopLeftPanel />
<TopCenterPanel />

View File

@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { ConnectionLineComponentProps, getBezierPath } from 'reactflow';
import { FIELDS, colorTokenToCssVar } from '../types/constants';
import { FIELDS, colorTokenToCssVar } from '../../../types/constants';
import { memo } from 'react';
const selector = createSelector(stateSelector, ({ nodes }) => {
const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } =
@ -25,7 +26,7 @@ const selector = createSelector(stateSelector, ({ nodes }) => {
};
});
export const CustomConnectionLine = ({
const CustomConnectionLine = ({
fromX,
fromY,
fromPosition,
@ -59,3 +60,5 @@ export const CustomConnectionLine = ({
</g>
);
};
export default memo(CustomConnectionLine);

View File

@ -0,0 +1,94 @@
import { Badge, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import { memo, useMemo } from 'react';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from 'reactflow';
import { makeEdgeSelector } from './util/makeEdgeSelector';
const InvocationCollapsedEdge = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
markerEnd,
data,
selected,
source,
target,
sourceHandleId,
targetHandleId,
}: EdgeProps<{ count: number }>) => {
const selector = useMemo(
() =>
makeEdgeSelector(
source,
sourceHandleId,
target,
targetHandleId,
selected
),
[selected, source, sourceHandleId, target, targetHandleId]
);
const { isSelected, shouldAnimate } = useAppSelector(selector);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const { base500 } = useChakraThemeTokens();
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: isSelected ? 3 : 2,
stroke: base500,
opacity: isSelected ? 0.8 : 0.5,
animation: shouldAnimate
? 'dashdraw 0.5s linear infinite'
: undefined,
strokeDasharray: shouldAnimate ? 5 : 'none',
}}
/>
{data?.count && data.count > 1 && (
<EdgeLabelRenderer>
<Flex
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
className="nodrag nopan"
>
<Badge
variant="solid"
sx={{
bg: 'base.500',
opacity: isSelected ? 0.8 : 0.5,
boxShadow: 'base',
}}
>
{data.count}
</Badge>
</Flex>
</EdgeLabelRenderer>
)}
</>
);
};
export default memo(InvocationCollapsedEdge);

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