shuffle button on workflows

This commit is contained in:
Attila Cseh
2025-08-20 11:22:27 +02:00
committed by psychedelicious
parent 28a77ab06c
commit 008e421ad4
13 changed files with 253 additions and 96 deletions

View File

@@ -1948,6 +1948,8 @@
"addToForm": "Add to Form",
"label": "Label",
"showDescription": "Show Description",
"showShuffle": "Show Shuffle",
"shuffle": "Shuffle",
"component": "Component",
"numberInput": "Number Input",
"singleLine": "Single Line",

View File

@@ -0,0 +1,5 @@
const randomFloat = (min: number, max: number): number => {
return Math.random() * (max - min + Number.EPSILON) + min;
};
export default randomFloat;

View File

@@ -1,35 +1,49 @@
import { CompositeNumberInput } from '@invoke-ai/ui-library';
import { Button, CompositeNumberInput } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const FloatFieldInput = memo(
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, handleClickRandomizeValue } =
useFloatField(nodeId, field.name, fieldTemplate, settings);
const { t } = useTranslation();
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
/>
<>
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}
);

View File

@@ -1,22 +1,22 @@
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const FloatFieldInputAndSlider = memo(
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, handleClickRandomizeValue } =
useFloatField(nodeId, field.name, fieldTemplate, settings);
const { t } = useTranslation();
return (
<>
@@ -43,7 +43,19 @@ export const FloatFieldInputAndSlider = memo(
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}

View File

@@ -1,37 +1,54 @@
import { CompositeSlider } from '@invoke-ai/ui-library';
import { Button, CompositeSlider } from '@invoke-ai/ui-library';
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const FloatFieldSlider = memo(
(
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
const { defaultValue, onChange, min, max, step, fineStep, showShuffle, handleClickRandomizeValue } = useFloatField(
nodeId,
field.name,
fieldTemplate,
settings
);
const { t } = useTranslation();
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}
);

View File

@@ -1,5 +1,6 @@
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import randomFloat from 'common/util/randomFloat';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { isNil } from 'es-toolkit/compat';
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
@@ -11,9 +12,9 @@ export const useFloatField = (
nodeId: string,
fieldName: string,
fieldTemplate: FloatFieldInputTemplate,
overrides: { min?: number; max?: number; step?: number } = {}
overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {}
) => {
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const dispatch = useAppDispatch();
const step = useMemo(() => {
@@ -77,6 +78,11 @@ export const useFloatField = (
[dispatch, fieldName, nodeId]
);
const handleClickRandomizeValue = useCallback(() => {
const value = Number((Math.round(randomFloat(min, max) / step) * step).toFixed(10));
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value }));
}, [dispatch, fieldName, nodeId, min, max, step]);
return {
defaultValue: fieldTemplate.default,
onChange,
@@ -85,5 +91,7 @@ export const useFloatField = (
step,
fineStep,
constrainValue,
showShuffle,
handleClickRandomizeValue,
};
};

View File

@@ -1,10 +1,12 @@
import { CompositeNumberInput } from '@invoke-ai/ui-library';
import { Button, CompositeNumberInput } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const IntegerFieldInput = memo(
(
@@ -15,26 +17,46 @@ export const IntegerFieldInput = memo(
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
const {
defaultValue,
onValueChange,
min,
max,
step,
fineStep,
constrainValue,
showShuffle,
handleClickRandomizeValue,
} = useIntegerField(nodeId, field.name, fieldTemplate, settings);
const { t } = useTranslation();
return (
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
<>
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onValueChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}
);

View File

@@ -1,10 +1,12 @@
import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const IntegerFieldInputAndSlider = memo(
(
@@ -15,18 +17,25 @@ export const IntegerFieldInputAndSlider = memo(
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
const {
defaultValue,
onValueChange,
min,
max,
step,
fineStep,
constrainValue,
showShuffle,
handleClickRandomizeValue,
} = useIntegerField(nodeId, field.name, fieldTemplate, settings);
const { t } = useTranslation();
return (
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
onChange={onValueChange}
value={field.value}
min={min}
max={max}
@@ -39,7 +48,7 @@ export const IntegerFieldInputAndSlider = memo(
/>
<CompositeNumberInput
defaultValue={defaultValue}
onChange={onChange}
onChange={onValueChange}
value={field.value}
min={min}
max={max}
@@ -47,7 +56,19 @@ export const IntegerFieldInputAndSlider = memo(
fineStep={fineStep}
className={NO_DRAG_CLASS}
flex="1 1 0"
constrainValue={constrainValue}
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}

View File

@@ -1,10 +1,12 @@
import { CompositeSlider } from '@invoke-ai/ui-library';
import { Button, CompositeSlider } from '@invoke-ai/ui-library';
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShuffleBold } from 'react-icons/pi';
export const IntegerFieldSlider = memo(
(
@@ -15,27 +17,38 @@ export const IntegerFieldSlider = memo(
>
) => {
const { nodeId, field, fieldTemplate, settings } = props;
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
nodeId,
field.name,
fieldTemplate,
settings
);
const { defaultValue, onValueChange, min, max, step, fineStep, showShuffle, handleClickRandomizeValue } =
useIntegerField(nodeId, field.name, fieldTemplate, settings);
const { t } = useTranslation();
return (
<CompositeSlider
defaultValue={defaultValue}
onChange={onChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
<>
<CompositeSlider
defaultValue={defaultValue}
onChange={onValueChange}
value={field.value}
min={min}
max={max}
step={step}
fineStep={fineStep}
className={NO_DRAG_CLASS}
marks
withThumbTooltip
flex="1 1 0"
/>
{showShuffle && (
<Button
size="sm"
isDisabled={false}
onClick={handleClickRandomizeValue}
leftIcon={<PiShuffleBold />}
flexShrink={0}
>
{t('workflows.builder.shuffle')}
</Button>
)}
</>
);
}
);

View File

@@ -1,5 +1,6 @@
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppDispatch } from 'app/store/storeHooks';
import randomInt from 'common/util/randomInt';
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
import { isNil } from 'es-toolkit/compat';
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
@@ -11,9 +12,9 @@ export const useIntegerField = (
nodeId: string,
fieldName: string,
fieldTemplate: IntegerFieldInputTemplate,
overrides: { min?: number; max?: number; step?: number } = {}
overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {}
) => {
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
const dispatch = useAppDispatch();
const step = useMemo(() => {
@@ -65,25 +66,31 @@ export const useIntegerField = (
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
const constrainValue = useCallback(
(v: number) =>
constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
(v: number) => constrainNumber(v, { min, max, step }, { min: overrideMin, max: overrideMax, step: overrideStep }),
[max, min, overrideMax, overrideMin, overrideStep, step]
);
const onChange = useCallback(
const onValueChange = useCallback(
(value: number) => {
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value }));
},
[dispatch, fieldName, nodeId]
);
const handleClickRandomizeValue = useCallback(() => {
const value = Math.round(randomInt(min, max) / step) * step;
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value }));
}, [dispatch, fieldName, nodeId, min, max, step]);
return {
defaultValue: fieldTemplate.default,
onChange,
onValueChange,
min,
max,
step,
fineStep,
constrainValue,
showShuffle,
handleClickRandomizeValue,
};
};

View File

@@ -20,8 +20,25 @@ type Props = {
};
export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { showShuffle } = settings;
const { t } = useTranslation();
const dispatch = useAppDispatch();
const toggleShowShuffle = useCallback(() => {
const newSettings: NodeFieldFloatSettings = {
...settings,
showShuffle: !showShuffle,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [dispatch, id, settings, showShuffle]);
return (
<>
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showShuffle')}</FormLabel>
<Switch size="sm" isChecked={showShuffle} onChange={toggleShowShuffle} />
</FormControl>
<SettingComponent
id={id}
settings={settings}

View File

@@ -21,8 +21,25 @@ type Props = {
};
export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
const { showShuffle } = settings;
const { t } = useTranslation();
const dispatch = useAppDispatch();
const toggleShowShuffle = useCallback(() => {
const newSettings: NodeFieldIntegerSettings = {
...settings,
showShuffle: !showShuffle,
};
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } }));
}, [dispatch, id, settings, showShuffle]);
return (
<>
<FormControl>
<FormLabel flex={1}>{t('workflows.builder.showShuffle')}</FormLabel>
<Switch size="sm" isChecked={showShuffle} onChange={toggleShowShuffle} />
</FormControl>
<SettingComponent
id={id}
settings={settings}

View File

@@ -74,6 +74,7 @@ export const NODE_FIELD_CLASS_NAME = `form-builder-${NODE_FIELD_TYPE}`;
const FLOAT_FIELD_SETTINGS_TYPE = 'float-field-config';
const zNodeFieldFloatSettings = z.object({
type: z.literal(FLOAT_FIELD_SETTINGS_TYPE).default(FLOAT_FIELD_SETTINGS_TYPE),
showShuffle: z.boolean().default(false),
component: zNumberComponent.default('number-input'),
min: z.number().optional(),
max: z.number().optional(),
@@ -84,6 +85,7 @@ export type NodeFieldFloatSettings = z.infer<typeof zNodeFieldFloatSettings>;
const INTEGER_FIELD_CONFIG_TYPE = 'integer-field-config';
const zNodeFieldIntegerSettings = z.object({
type: z.literal(INTEGER_FIELD_CONFIG_TYPE).default(INTEGER_FIELD_CONFIG_TYPE),
showShuffle: z.boolean().default(false),
component: zNumberComponent.default('number-input'),
min: z.number().optional(),
max: z.number().optional(),