mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-01 06:45:00 -05:00
feat(ui): configurable form field constraints (WIP)
This commit is contained in:
@@ -1768,7 +1768,9 @@
|
||||
"containerPlaceholder": "Empty Container",
|
||||
"headingPlaceholder": "Empty Heading",
|
||||
"textPlaceholder": "Empty Text",
|
||||
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release."
|
||||
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.",
|
||||
"minimum": "Minimum",
|
||||
"maximum": "Maximum"
|
||||
}
|
||||
},
|
||||
"controlLayers": {
|
||||
|
||||
@@ -3,24 +3,35 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
|
||||
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';
|
||||
|
||||
export const FloatFieldInput = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
/>
|
||||
);
|
||||
});
|
||||
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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FloatFieldInput.displayName = 'FloatFieldInput ';
|
||||
|
||||
@@ -3,18 +3,27 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
|
||||
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';
|
||||
|
||||
export const FloatFieldInputAndSlider = memo(
|
||||
(props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
|
||||
(
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
@@ -27,7 +36,7 @@ export const FloatFieldInputAndSlider = memo(
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
|
||||
@@ -3,26 +3,37 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
|
||||
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';
|
||||
|
||||
export const FloatFieldSlider = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
|
||||
export const FloatFieldSlider = 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
|
||||
);
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
marks
|
||||
withThumbTooltip
|
||||
flex="1 1 0"
|
||||
/>
|
||||
);
|
||||
});
|
||||
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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FloatFieldSlider.displayName = 'FloatFieldSlider ';
|
||||
|
||||
@@ -1,65 +1,93 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { constrainNumber } from 'features/nodes/util/constrainNumber';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useFloatField = (props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
export const useFloatField = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
fieldTemplate: FloatFieldInputTemplate,
|
||||
overrides: { min?: number; max?: number; step?: number } = {}
|
||||
) => {
|
||||
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName: field.name, value }));
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 0.01;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 0.01;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (overrideStep !== undefined) {
|
||||
return overrideStep;
|
||||
}
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
}, [fieldTemplate.multipleOf, overrideStep]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (overrideStep !== undefined) {
|
||||
return overrideStep;
|
||||
}
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 0.01;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
}, [fieldTemplate.multipleOf, overrideStep]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
|
||||
if (overrideMin !== undefined) {
|
||||
min = overrideMin;
|
||||
} else if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
} else if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 0.01;
|
||||
}
|
||||
|
||||
return roundUpToMultiple(min, step);
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum, overrideMin, step]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
|
||||
if (overrideMax !== undefined) {
|
||||
max = overrideMax;
|
||||
} else if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
} else if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 0.01;
|
||||
}
|
||||
|
||||
return roundDownToMultiple(max, step);
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
|
||||
|
||||
const constrainValue = useCallback(
|
||||
(v: number) =>
|
||||
constrainNumber(
|
||||
v,
|
||||
{ min, max, multipleOf: step },
|
||||
{ min: overrideMin, max: overrideMax, multipleOf: overrideStep }
|
||||
),
|
||||
[max, min, overrideMax, overrideMin, overrideStep, step]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value }));
|
||||
},
|
||||
[dispatch, fieldName, nodeId]
|
||||
);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
value: field.value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
fineStep,
|
||||
constrainValue,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -95,6 +95,8 @@ import {
|
||||
} from 'features/nodes/types/field';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
|
||||
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
|
||||
@@ -157,6 +159,8 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
|
||||
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'textarea') {
|
||||
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else {
|
||||
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,32 +175,47 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
|
||||
if (!isIntegerFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
if (settings?.type !== 'integer-field-config') {
|
||||
if (!settings || settings.type !== 'integer-field-config') {
|
||||
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (settings.component === 'number-input') {
|
||||
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'slider') {
|
||||
}
|
||||
|
||||
if (settings.component === 'slider') {
|
||||
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'number-input-and-slider') {
|
||||
}
|
||||
|
||||
if (settings.component === 'number-input-and-slider') {
|
||||
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
|
||||
}
|
||||
|
||||
if (isFloatFieldInputTemplate(template)) {
|
||||
if (!isFloatFieldInputInstance(field)) {
|
||||
return null;
|
||||
}
|
||||
if (settings?.type !== 'float-field-config') {
|
||||
|
||||
if (!settings || settings.type !== 'float-field-config') {
|
||||
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
}
|
||||
|
||||
if (settings.component === 'number-input') {
|
||||
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'slider') {
|
||||
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
} else if (settings.component === 'number-input-and-slider') {
|
||||
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
|
||||
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
|
||||
}
|
||||
|
||||
if (settings.component === 'slider') {
|
||||
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
|
||||
}
|
||||
|
||||
if (settings.component === 'number-input-and-slider') {
|
||||
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
|
||||
}
|
||||
|
||||
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
|
||||
}
|
||||
|
||||
if (isIntegerFieldCollectionInputTemplate(template)) {
|
||||
|
||||
@@ -3,23 +3,37 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
|
||||
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';
|
||||
|
||||
export const IntegerFieldInput = memo(
|
||||
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
|
||||
(
|
||||
props: FieldComponentProps<
|
||||
IntegerFieldInputInstance,
|
||||
IntegerFieldInputTemplate,
|
||||
{ settings?: NodeFieldIntegerSettings }
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
|
||||
return (
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
fineStep={fineStep}
|
||||
className={NO_DRAG_CLASS}
|
||||
flex="1 1 0"
|
||||
constrainValue={constrainValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,31 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
|
||||
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';
|
||||
|
||||
export const IntegerFieldInputAndSlider = memo(
|
||||
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
|
||||
(
|
||||
props: FieldComponentProps<
|
||||
IntegerFieldInputInstance,
|
||||
IntegerFieldInputTemplate,
|
||||
{ settings?: NodeFieldIntegerSettings }
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
@@ -27,7 +40,7 @@ export const IntegerFieldInputAndSlider = memo(
|
||||
<CompositeNumberInput
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
|
||||
@@ -3,17 +3,30 @@ import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/I
|
||||
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';
|
||||
|
||||
export const IntegerFieldSlider = memo(
|
||||
(props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { defaultValue, onChange, value, min, max, step, fineStep } = useIntegerField(props);
|
||||
(
|
||||
props: FieldComponentProps<
|
||||
IntegerFieldInputInstance,
|
||||
IntegerFieldInputTemplate,
|
||||
{ settings?: NodeFieldIntegerSettings }
|
||||
>
|
||||
) => {
|
||||
const { nodeId, field, fieldTemplate, settings } = props;
|
||||
const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField(
|
||||
nodeId,
|
||||
field.name,
|
||||
fieldTemplate,
|
||||
settings
|
||||
);
|
||||
|
||||
return (
|
||||
<CompositeSlider
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
value={field.value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
|
||||
@@ -1,65 +1,93 @@
|
||||
import { NUMPY_RAND_MAX } from 'app/constants';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { constrainNumber } from 'features/nodes/util/constrainNumber';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useIntegerField = (props: FieldComponentProps<IntegerFieldInputInstance, IntegerFieldInputTemplate>) => {
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
export const useIntegerField = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
fieldTemplate: IntegerFieldInputTemplate,
|
||||
overrides: { min?: number; max?: number; step?: number } = {}
|
||||
) => {
|
||||
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName: field.name, value: Math.floor(Number(value)) }));
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
const step = useMemo(() => {
|
||||
if (overrideStep !== undefined) {
|
||||
return overrideStep;
|
||||
}
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, overrideStep]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (overrideStep !== undefined) {
|
||||
return overrideStep;
|
||||
}
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf, overrideStep]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
let min = -NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.minimum)) {
|
||||
|
||||
if (overrideMin !== undefined) {
|
||||
min = overrideMin;
|
||||
} else if (!isNil(fieldTemplate.minimum)) {
|
||||
min = fieldTemplate.minimum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
} else if (!isNil(fieldTemplate.exclusiveMinimum)) {
|
||||
min = fieldTemplate.exclusiveMinimum + 1;
|
||||
}
|
||||
return min;
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
|
||||
|
||||
return roundUpToMultiple(min, step);
|
||||
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum, overrideMin, step]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
let max = NUMPY_RAND_MAX;
|
||||
if (!isNil(fieldTemplate.maximum)) {
|
||||
|
||||
if (overrideMax !== undefined) {
|
||||
max = overrideMax;
|
||||
} else if (!isNil(fieldTemplate.maximum)) {
|
||||
max = fieldTemplate.maximum;
|
||||
}
|
||||
if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
} else if (!isNil(fieldTemplate.exclusiveMaximum)) {
|
||||
max = fieldTemplate.exclusiveMaximum - 1;
|
||||
}
|
||||
return max;
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
|
||||
|
||||
const step = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
return roundDownToMultiple(max, step);
|
||||
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
|
||||
|
||||
const fineStep = useMemo(() => {
|
||||
if (isNil(fieldTemplate.multipleOf)) {
|
||||
return 1;
|
||||
}
|
||||
return fieldTemplate.multipleOf;
|
||||
}, [fieldTemplate.multipleOf]);
|
||||
const constrainValue = useCallback(
|
||||
(v: number) =>
|
||||
constrainNumber(
|
||||
v,
|
||||
{ min, max, multipleOf: step },
|
||||
{ min: overrideMin, max: overrideMax, multipleOf: overrideStep }
|
||||
),
|
||||
[max, min, overrideMax, overrideMin, overrideStep, step]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value }));
|
||||
},
|
||||
[dispatch, fieldName, nodeId]
|
||||
);
|
||||
|
||||
return {
|
||||
defaultValue: fieldTemplate.default,
|
||||
onChange,
|
||||
value: field.value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
fineStep,
|
||||
constrainValue,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { FieldInputInstance, FieldInputTemplate } from 'features/nodes/types/field';
|
||||
|
||||
export type FieldComponentProps<V extends FieldInputInstance, T extends FieldInputTemplate, C = void> = {
|
||||
export type FieldComponentProps<
|
||||
TFieldInstance extends FieldInputInstance,
|
||||
TFieldTemplate extends FieldInputTemplate,
|
||||
FieldSettings = void,
|
||||
> = {
|
||||
nodeId: string;
|
||||
field: V;
|
||||
fieldTemplate: T;
|
||||
} & Omit<C, 'nodeId' | 'field' | 'fieldTemplate'>;
|
||||
field: TFieldInstance;
|
||||
fieldTemplate: TFieldTemplate;
|
||||
} & Omit<FieldSettings, 'nodeId' | 'field' | 'fieldTemplate'>;
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { CompositeNumberInput, Flex, FormControl, FormLabel, Select, Switch } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { type NodeFieldFloatSettings, zNumberComponent } from 'features/nodes/types/workflow';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string; config: NodeFieldFloatSettings }) => {
|
||||
type Props = {
|
||||
id: string;
|
||||
config: NodeFieldFloatSettings;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fieldTemplate: FloatFieldInputTemplate;
|
||||
};
|
||||
|
||||
export const NodeFieldElementFloatSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
|
||||
|
||||
const SettingComponent = memo(({ id, config }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -22,7 +45,7 @@ export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string;
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
@@ -32,4 +55,113 @@ export const NodeFieldElementFloatSettings = memo(({ id, config }: { id: string;
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
NodeFieldElementFloatSettings.displayName = 'NodeFieldElementFloatSettings';
|
||||
SettingComponent.displayName = 'SettingComponent';
|
||||
|
||||
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
min: config.min !== undefined ? undefined : floatField.min,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField.min, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
min: v,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
const constrained = constrain(v, floatField, newConfig);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: v }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
|
||||
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.min === undefined}
|
||||
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
|
||||
onChange={onChange}
|
||||
min={floatField.min}
|
||||
max={(config.max ?? floatField.max) - 0.1}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingMin.displayName = 'SettingMin';
|
||||
|
||||
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
max: config.max !== undefined ? undefined : floatField.max,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField.max, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
const newConfig: NodeFieldFloatSettings = {
|
||||
...config,
|
||||
max: v,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
const constrained = constrain(v, floatField, newConfig);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: v }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
|
||||
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.max === undefined}
|
||||
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
|
||||
onChange={onChange}
|
||||
min={(config.min ?? floatField.min) + 0.1}
|
||||
max={floatField.max}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingMax.displayName = 'SettingMax';
|
||||
|
||||
const constrain = (v: number, fieldSettings: ReturnType<typeof useFloatField>, overrides: NodeFieldFloatSettings) => {
|
||||
const min = overrides.min ?? fieldSettings.min;
|
||||
const max = overrides.max ?? fieldSettings.max;
|
||||
const step = overrides.step ?? fieldSettings.step;
|
||||
|
||||
const _v = Math.min(max, Math.max(min, v));
|
||||
const _diff = _v - min;
|
||||
const _steps = Math.round(_diff / step);
|
||||
return min + _steps * step;
|
||||
};
|
||||
|
||||
@@ -1,37 +1,173 @@
|
||||
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { CompositeNumberInput, Flex, FormControl, FormLabel, Select, Switch } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
|
||||
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
|
||||
import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { type NodeFieldIntegerSettings, zNumberComponent } from 'features/nodes/types/workflow';
|
||||
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
|
||||
import { zNumberComponent } from 'features/nodes/types/workflow';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
export const NodeFieldElementIntegerConfig = memo(
|
||||
({ id, config }: { id: string; config: NodeFieldIntegerSettings }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
type Props = {
|
||||
id: string;
|
||||
config: NodeFieldIntegerSettings;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fieldTemplate: IntegerFieldInputTemplate;
|
||||
};
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
component: zNumberComponent.parse(e.target.value),
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
);
|
||||
export const NodeFieldElementIntegerSettings = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SettingComponent id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMin id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
<SettingMax id={id} config={config} nodeId={nodeId} fieldName={fieldName} fieldTemplate={fieldTemplate} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
NodeFieldElementIntegerSettings.displayName = 'NodeFieldElementIntegerSettings';
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
<option value="slider">{t('workflows.builder.slider')}</option>
|
||||
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
NodeFieldElementIntegerConfig.displayName = 'NodeFieldElementIntegerConfig';
|
||||
const SettingComponent = memo(({ id, config }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
component: zNumberComponent.parse(e.target.value),
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
<option value="slider">{t('workflows.builder.slider')}</option>
|
||||
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingComponent.displayName = 'SettingComponent';
|
||||
|
||||
const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
min: config.min !== undefined ? undefined : floatField.min,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField.min, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
min: v,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
const constrained = constrain(v, floatField, newConfig);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: v }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.minimum')}</FormLabel>
|
||||
<Switch isChecked={config.min !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.min === undefined}
|
||||
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
|
||||
onChange={onChange}
|
||||
min={floatField.min}
|
||||
max={(config.max ?? floatField.max) - 0.1}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingMin.displayName = 'SettingMin';
|
||||
|
||||
const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
|
||||
const floatField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
const onToggleSetting = useCallback(() => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
max: config.max !== undefined ? undefined : floatField.max,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
}, [config, dispatch, floatField.max, id]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
max: v,
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
const constrained = constrain(v, floatField, newConfig);
|
||||
if (field.value !== constrained) {
|
||||
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: v }));
|
||||
}
|
||||
},
|
||||
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl orientation="vertical">
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<FormLabel m={0}>{t('workflows.builder.maximum')}</FormLabel>
|
||||
<Switch isChecked={config.max !== undefined} onChange={onToggleSetting} size="sm" />
|
||||
</Flex>
|
||||
<CompositeNumberInput
|
||||
w="full"
|
||||
isDisabled={config.max === undefined}
|
||||
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
|
||||
onChange={onChange}
|
||||
min={(config.min ?? floatField.min) + 0.1}
|
||||
max={floatField.max}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
SettingMax.displayName = 'SettingMax';
|
||||
|
||||
type NumberConstraints = { min: number; max: number; step: number };
|
||||
|
||||
const constrain = (v: number, constraints: NumberConstraints, overrides: PartialDeep<NumberConstraints>) => {
|
||||
const min = overrides.min ?? constraints.min;
|
||||
const max = overrides.max ?? constraints.max;
|
||||
const step = overrides.step ?? constraints.step;
|
||||
|
||||
console.log({ min, max, step });
|
||||
|
||||
const _v = Math.min(max, Math.max(min, v));
|
||||
const _diff = _v - min;
|
||||
const _steps = Math.round(_diff / step);
|
||||
return min + _steps * step;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { NodeFieldElementFloatSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings';
|
||||
import { NodeFieldElementIntegerConfig } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
|
||||
import { NodeFieldElementIntegerSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings';
|
||||
import { NodeFieldElementStringSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementStringSettings';
|
||||
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
@@ -70,13 +71,31 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverBody minW={48}>
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
|
||||
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
|
||||
</FormControl>
|
||||
{settings?.type === 'integer-field-config' && <NodeFieldElementIntegerConfig id={id} config={settings} />}
|
||||
{settings?.type === 'float-field-config' && <NodeFieldElementFloatSettings id={id} config={settings} />}
|
||||
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.showDescription')}</FormLabel>
|
||||
<Switch size="sm" isChecked={showDescription} onChange={toggleShowDescription} />
|
||||
</FormControl>
|
||||
{settings?.type === 'integer-field-config' && isIntegerFieldInputTemplate(fieldTemplate) && (
|
||||
<NodeFieldElementIntegerSettings
|
||||
id={id}
|
||||
config={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
)}
|
||||
{settings?.type === 'float-field-config' && isFloatFieldInputTemplate(fieldTemplate) && (
|
||||
<NodeFieldElementFloatSettings
|
||||
id={id}
|
||||
config={settings}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
fieldTemplate={fieldTemplate}
|
||||
/>
|
||||
)}
|
||||
{settings?.type === 'string-field-config' && <NodeFieldElementStringSettings id={id} config={settings} />}
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
|
||||
import { type NodeFieldIntegerSettings, zNumberComponent } from 'features/nodes/types/workflow';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NodeFieldElementIntegerConfig = memo(
|
||||
({ id, config }: { id: string; config: NodeFieldIntegerSettings }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeComponent = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const newConfig: NodeFieldIntegerSettings = {
|
||||
...config,
|
||||
component: zNumberComponent.parse(e.target.value),
|
||||
};
|
||||
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
|
||||
},
|
||||
[config, dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel flex={1}>{t('workflows.builder.component')}</FormLabel>
|
||||
<Select value={config.component} onChange={onChangeComponent} size="sm">
|
||||
<option value="number-input">{t('workflows.builder.numberInput')}</option>
|
||||
<option value="slider">{t('workflows.builder.slider')}</option>
|
||||
<option value="number-input-and-slider">{t('workflows.builder.both')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
NodeFieldElementIntegerConfig.displayName = 'NodeFieldElementIntegerConfig';
|
||||
@@ -5,7 +5,7 @@ import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useInputFieldInstance = (nodeId: string, fieldName: string): FieldInputInstance => {
|
||||
export const useInputFieldInstance = <T extends FieldInputInstance>(nodeId: string, fieldName: string): T => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
@@ -18,5 +18,5 @@ export const useInputFieldInstance = (nodeId: string, fieldName: string): FieldI
|
||||
|
||||
const instance = useAppSelector(selector);
|
||||
|
||||
return instance;
|
||||
return instance as T;
|
||||
};
|
||||
|
||||
@@ -4,20 +4,29 @@ import type {
|
||||
FieldInputTemplate,
|
||||
FloatFieldCollectionInputTemplate,
|
||||
FloatFieldCollectionValue,
|
||||
FloatFieldInputTemplate,
|
||||
FloatFieldValue,
|
||||
ImageFieldCollectionInputTemplate,
|
||||
ImageFieldCollectionValue,
|
||||
IntegerFieldCollectionInputTemplate,
|
||||
IntegerFieldCollectionValue,
|
||||
IntegerFieldInputTemplate,
|
||||
IntegerFieldValue,
|
||||
StatefulFieldValue,
|
||||
StringFieldCollectionInputTemplate,
|
||||
StringFieldCollectionValue,
|
||||
} from 'features/nodes/types/field';
|
||||
import {
|
||||
isFloatFieldCollectionInputInstance,
|
||||
isFloatFieldCollectionInputTemplate,
|
||||
isFloatFieldInputInstance,
|
||||
isFloatFieldInputTemplate,
|
||||
isImageFieldCollectionInputInstance,
|
||||
isImageFieldCollectionInputTemplate,
|
||||
isIntegerFieldCollectionInputInstance,
|
||||
isIntegerFieldCollectionInputTemplate,
|
||||
isIntegerFieldInputInstance,
|
||||
isIntegerFieldInputTemplate,
|
||||
isStringFieldCollectionInputInstance,
|
||||
isStringFieldCollectionInputTemplate,
|
||||
} from 'features/nodes/types/field';
|
||||
@@ -25,10 +34,15 @@ import { type InvocationNode, type InvocationTemplate, isInvocationNode } from '
|
||||
import { t } from 'i18next';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const validateImageFieldCollectionValue = (
|
||||
value: NonNullable<ImageFieldCollectionValue>,
|
||||
template: ImageFieldCollectionInputTemplate
|
||||
): string[] => {
|
||||
type FieldValidationFunc<TValue extends StatefulFieldValue, TTemplate extends FieldInputTemplate> = (
|
||||
value: TValue,
|
||||
template: TTemplate
|
||||
) => string[];
|
||||
|
||||
const validateImageFieldCollectionValue: FieldValidationFunc<
|
||||
NonNullable<ImageFieldCollectionValue>,
|
||||
ImageFieldCollectionInputTemplate
|
||||
> = (value, template) => {
|
||||
const reasons: string[] = [];
|
||||
const { minItems, maxItems } = template;
|
||||
const count = value.length;
|
||||
@@ -49,10 +63,10 @@ const validateImageFieldCollectionValue = (
|
||||
return reasons;
|
||||
};
|
||||
|
||||
const validateStringFieldCollectionValue = (
|
||||
value: NonNullable<StringFieldCollectionValue>,
|
||||
template: StringFieldCollectionInputTemplate
|
||||
): string[] => {
|
||||
const validateStringFieldCollectionValue: FieldValidationFunc<
|
||||
NonNullable<StringFieldCollectionValue>,
|
||||
StringFieldCollectionInputTemplate
|
||||
> = (value, template) => {
|
||||
const reasons: string[] = [];
|
||||
const { minItems, maxItems, minLength, maxLength } = template;
|
||||
const count = value.length;
|
||||
@@ -82,10 +96,10 @@ const validateStringFieldCollectionValue = (
|
||||
return reasons;
|
||||
};
|
||||
|
||||
const validateNumberFieldCollectionValue = (
|
||||
value: NonNullable<IntegerFieldCollectionValue> | NonNullable<FloatFieldCollectionValue>,
|
||||
template: IntegerFieldCollectionInputTemplate | FloatFieldCollectionInputTemplate
|
||||
): string[] => {
|
||||
const validateNumberFieldCollectionValue: FieldValidationFunc<
|
||||
NonNullable<IntegerFieldCollectionValue> | NonNullable<FloatFieldCollectionValue>,
|
||||
IntegerFieldCollectionInputTemplate | FloatFieldCollectionInputTemplate
|
||||
> = (value, template) => {
|
||||
const reasons: string[] = [];
|
||||
const { minItems, maxItems, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = template;
|
||||
const count = value.length;
|
||||
@@ -124,6 +138,32 @@ const validateNumberFieldCollectionValue = (
|
||||
return reasons;
|
||||
};
|
||||
|
||||
const validateNumberFieldValue: FieldValidationFunc<
|
||||
FloatFieldValue | IntegerFieldValue,
|
||||
FloatFieldInputTemplate | IntegerFieldInputTemplate
|
||||
> = (value, template) => {
|
||||
const reasons: string[] = [];
|
||||
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = template;
|
||||
|
||||
if (maximum !== undefined && value > maximum) {
|
||||
reasons.push(t('parameters.invoke.collectionNumberGTMax', { value, maximum }));
|
||||
}
|
||||
if (minimum !== undefined && value < minimum) {
|
||||
reasons.push(t('parameters.invoke.collectionNumberLTMin', { value, minimum }));
|
||||
}
|
||||
if (exclusiveMaximum !== undefined && value >= exclusiveMaximum) {
|
||||
reasons.push(t('parameters.invoke.collectionNumberGTExclusiveMax', { value, exclusiveMaximum }));
|
||||
}
|
||||
if (exclusiveMinimum !== undefined && value <= exclusiveMinimum) {
|
||||
reasons.push(t('parameters.invoke.collectionNumberLTExclusiveMin', { value, exclusiveMinimum }));
|
||||
}
|
||||
if (multipleOf !== undefined && value % multipleOf !== 0) {
|
||||
reasons.push(t('parameters.invoke.collectionNumberNotMultipleOf', { value, multipleOf }));
|
||||
}
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
type NodeError = {
|
||||
type: 'node-error';
|
||||
nodeId: string;
|
||||
@@ -147,6 +187,16 @@ const getFieldErrorPrefix = (
|
||||
return `${node.data.label || nodeTemplate.title} -> ${field.label || fieldTemplate.title}`;
|
||||
};
|
||||
|
||||
const getIssuesToFieldErrorsMapFunc =
|
||||
(nodeId: string, fieldName: string, prefix: string): ((issue: string) => FieldError) =>
|
||||
(issue: string) => ({
|
||||
type: 'field-error',
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue,
|
||||
});
|
||||
|
||||
export const getFieldErrors = (
|
||||
node: InvocationNode,
|
||||
nodeTemplate: InvocationTemplate,
|
||||
@@ -156,6 +206,7 @@ export const getFieldErrors = (
|
||||
): FieldError[] => {
|
||||
const errors: FieldError[] = [];
|
||||
const prefix = getFieldErrorPrefix(node, nodeTemplate, field, fieldTemplate);
|
||||
const issueToFieldError = getIssuesToFieldErrorsMapFunc(node.data.id, field.name, prefix);
|
||||
|
||||
const nodeId = node.data.id;
|
||||
const fieldName = field.name;
|
||||
@@ -176,68 +227,21 @@ export const getFieldErrors = (
|
||||
});
|
||||
} else if (isConnected) {
|
||||
// Connected fields have no value to validate - they are OK
|
||||
} else if (
|
||||
field.value &&
|
||||
isImageFieldCollectionInputTemplate(fieldTemplate) &&
|
||||
isImageFieldCollectionInputInstance(field)
|
||||
) {
|
||||
const issues = validateImageFieldCollectionValue(field.value, fieldTemplate);
|
||||
errors.push(
|
||||
...issues.map<FieldError>((issue) => ({
|
||||
type: 'field-error',
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue,
|
||||
}))
|
||||
);
|
||||
} else if (
|
||||
field.value &&
|
||||
isStringFieldCollectionInputTemplate(fieldTemplate) &&
|
||||
isStringFieldCollectionInputInstance(field)
|
||||
) {
|
||||
const issues = validateStringFieldCollectionValue(field.value, fieldTemplate);
|
||||
errors.push(
|
||||
...issues.map<FieldError>((issue) => ({
|
||||
type: 'field-error',
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue,
|
||||
}))
|
||||
);
|
||||
} else if (
|
||||
field.value &&
|
||||
isIntegerFieldCollectionInputTemplate(fieldTemplate) &&
|
||||
isIntegerFieldCollectionInputInstance(field)
|
||||
) {
|
||||
const issues = validateNumberFieldCollectionValue(field.value, fieldTemplate);
|
||||
errors.push(
|
||||
...issues.map<FieldError>((issue) => ({
|
||||
type: 'field-error',
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue,
|
||||
}))
|
||||
);
|
||||
} else if (
|
||||
field.value &&
|
||||
isFloatFieldCollectionInputTemplate(fieldTemplate) &&
|
||||
isFloatFieldCollectionInputInstance(field)
|
||||
) {
|
||||
const issues = validateNumberFieldCollectionValue(field.value, fieldTemplate);
|
||||
errors.push(
|
||||
...issues.map<FieldError>((issue) => ({
|
||||
type: 'field-error',
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue,
|
||||
}))
|
||||
);
|
||||
} else if (field.value !== undefined) {
|
||||
if (isImageFieldCollectionInputTemplate(fieldTemplate) && isImageFieldCollectionInputInstance(field)) {
|
||||
errors.push(...validateImageFieldCollectionValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
} else if (isStringFieldCollectionInputTemplate(fieldTemplate) && isStringFieldCollectionInputInstance(field)) {
|
||||
errors.push(...validateStringFieldCollectionValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
} else if (isIntegerFieldCollectionInputTemplate(fieldTemplate) && isIntegerFieldCollectionInputInstance(field)) {
|
||||
errors.push(...validateNumberFieldCollectionValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
} else if (isFloatFieldCollectionInputTemplate(fieldTemplate) && isFloatFieldCollectionInputInstance(field)) {
|
||||
errors.push(...validateNumberFieldCollectionValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
} else if (isFloatFieldInputTemplate(fieldTemplate) && isFloatFieldInputInstance(field)) {
|
||||
errors.push(...validateNumberFieldValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
} else if (isIntegerFieldInputTemplate(fieldTemplate) && isIntegerFieldInputInstance(field)) {
|
||||
errors.push(...validateNumberFieldValue(field.value, fieldTemplate).map(issueToFieldError));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ const FLOAT_FIELD_SETTINGS_TYPE = 'float-field-config';
|
||||
const zNodeFieldFloatSettings = z.object({
|
||||
type: z.literal(FLOAT_FIELD_SETTINGS_TYPE).default(FLOAT_FIELD_SETTINGS_TYPE),
|
||||
component: zNumberComponent.default('number-input'),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
step: z.number().optional(),
|
||||
});
|
||||
export const getFloatFieldSettingsDefaults = (): NodeFieldFloatSettings => zNodeFieldFloatSettings.parse({});
|
||||
export type NodeFieldFloatSettings = z.infer<typeof zNodeFieldFloatSettings>;
|
||||
@@ -83,6 +86,9 @@ const INTEGER_FIELD_CONFIG_TYPE = 'integer-field-config';
|
||||
const zNodeFieldIntegerSettings = z.object({
|
||||
type: z.literal(INTEGER_FIELD_CONFIG_TYPE).default(INTEGER_FIELD_CONFIG_TYPE),
|
||||
component: zNumberComponent.default('number-input'),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
step: z.number().optional(),
|
||||
});
|
||||
export type NodeFieldIntegerSettings = z.infer<typeof zNodeFieldIntegerSettings>;
|
||||
export const getIntegerFieldSettingsDefaults = (): NodeFieldIntegerSettings => zNodeFieldIntegerSettings.parse({});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { constrainNumber } from './constrainNumber';
|
||||
|
||||
describe('constrainNumber', () => {
|
||||
// Default constraints to be used in tests
|
||||
const defaultConstraints = { min: 0, max: 10, multipleOf: 1 };
|
||||
|
||||
it('should keep values within range', () => {
|
||||
expect(constrainNumber(5, defaultConstraints)).toBe(5);
|
||||
expect(constrainNumber(-5, defaultConstraints)).toBe(0);
|
||||
expect(constrainNumber(15, defaultConstraints)).toBe(10);
|
||||
});
|
||||
|
||||
it('should round to nearest multiple', () => {
|
||||
const constraints = { min: 0, max: 10, multipleOf: 2 };
|
||||
expect(constrainNumber(1, constraints)).toBe(2);
|
||||
expect(constrainNumber(2, constraints)).toBe(2);
|
||||
expect(constrainNumber(3, constraints)).toBe(4);
|
||||
expect(constrainNumber(9, constraints)).toBe(10);
|
||||
expect(constrainNumber(11, constraints)).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle negative multiples', () => {
|
||||
const constraints = { min: -10, max: 10, multipleOf: 3 };
|
||||
expect(constrainNumber(-9, constraints)).toBe(-9);
|
||||
expect(constrainNumber(-8, constraints)).toBe(-9);
|
||||
expect(constrainNumber(-7, constraints)).toBe(-6);
|
||||
expect(constrainNumber(-3, constraints)).toBe(-3);
|
||||
expect(constrainNumber(-2, constraints)).toBe(-3);
|
||||
// In JS, -0 !== +0... :)
|
||||
expect(constrainNumber(-1, constraints)).toBe(0);
|
||||
expect(constrainNumber(0, constraints)).toBe(0);
|
||||
expect(constrainNumber(1, constraints)).toBe(0);
|
||||
expect(constrainNumber(2, constraints)).toBe(3);
|
||||
expect(constrainNumber(3, constraints)).toBe(3);
|
||||
expect(constrainNumber(4, constraints)).toBe(3);
|
||||
expect(constrainNumber(7, constraints)).toBe(6);
|
||||
expect(constrainNumber(8, constraints)).toBe(9);
|
||||
expect(constrainNumber(9, constraints)).toBe(9);
|
||||
});
|
||||
|
||||
it('should respect boundaries when rounding', () => {
|
||||
const constraints = { min: 0, max: 10, multipleOf: 4 };
|
||||
// Value at 9 would normally round to 8
|
||||
expect(constrainNumber(9, constraints)).toBe(8);
|
||||
// Value at 11 would normally round to 12, but max is 10
|
||||
expect(constrainNumber(11, constraints)).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle decimal multiples', () => {
|
||||
const constraints = { min: 0, max: 1, multipleOf: 0.25 };
|
||||
expect(constrainNumber(0.3, constraints)).toBe(0.25);
|
||||
expect(constrainNumber(0.87, constraints)).toBe(0.75);
|
||||
expect(constrainNumber(0.88, constraints)).toBe(1.0);
|
||||
expect(constrainNumber(0.13, constraints)).toBe(0.25);
|
||||
});
|
||||
|
||||
it('should apply overrides correctly', () => {
|
||||
// Override min
|
||||
expect(constrainNumber(2, defaultConstraints, { min: 5 })).toBe(5);
|
||||
|
||||
// Override max
|
||||
expect(constrainNumber(8, defaultConstraints, { max: 7 })).toBe(7);
|
||||
|
||||
// Override multipleOf
|
||||
expect(constrainNumber(4.7, defaultConstraints, { multipleOf: 2 })).toBe(4);
|
||||
|
||||
// Override all
|
||||
expect(constrainNumber(15, defaultConstraints, { min: 5, max: 20, multipleOf: 5 })).toBe(15);
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
// Value exactly at min
|
||||
expect(constrainNumber(0, defaultConstraints)).toBe(0);
|
||||
|
||||
// Value exactly at max
|
||||
expect(constrainNumber(10, defaultConstraints)).toBe(10);
|
||||
|
||||
// multipleOf larger than range
|
||||
const narrowConstraints = { min: 5, max: 7, multipleOf: 5 };
|
||||
expect(constrainNumber(6, narrowConstraints)).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
type NumberConstraints = { min: number; max: number; multipleOf?: number };
|
||||
|
||||
/**
|
||||
* Constrain a number to a range and round to the nearest multiple of a given value.
|
||||
* @param v
|
||||
* @param constraints
|
||||
* @param overrides
|
||||
* @returns
|
||||
*/
|
||||
export const constrainNumber = (
|
||||
v: number,
|
||||
constraints: NumberConstraints,
|
||||
overrides?: PartialDeep<NumberConstraints>
|
||||
) => {
|
||||
const min = overrides?.min ?? constraints.min;
|
||||
const max = overrides?.max ?? constraints.max;
|
||||
const multipleOf = overrides?.multipleOf ?? constraints.multipleOf;
|
||||
|
||||
if (multipleOf === undefined) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
// First clamp to range
|
||||
v = Math.min(Math.max(v, min), max);
|
||||
|
||||
// Round to nearest multiple of multipleOf
|
||||
const roundedValue = Math.round(v / multipleOf) * multipleOf;
|
||||
|
||||
// Ensure the result is still within the range
|
||||
return Math.min(Math.max(roundedValue, min), max);
|
||||
};
|
||||
Reference in New Issue
Block a user