Skip to content

Commit facfb84

Browse files
refactor(ui): persistent workflow field value generators
Previously, workflow generators existed on a layer above the workflow and were ephemeral. Generators could be run and the result saved to the workflow. There was no way to have the generator and its settings to be an inherent part of the workflow. When you refresh the page or load a workflow, the generator settings are reset. For example, a number collection field's value is a list of numbers. When you use a range generator for that field, the generated list of numbers is written to the workflow. When you refresh the page or load the workflow later, all you have is the list of numbers. This change makes generators a part of the workflow itself. In other words, the a field's generator settings are persisted to the workflow alongside the field, and eligible fields can be thought of as having a generator _as_ their state. For example, consider a number collection. If the field has a generator enabled, the generator settings are stored in the workflow directly, in that field's state. When we need to access the field's value, if it has a generator, we run the generator. If there is no generator, we get the directly-entered value. This enables an important use-case, where the workflow editor can set up a good baseline generator and save it to the workflow. Then the workflow user loads the workflow, and they just see the generator settings, importantly with the default values set by the editor. They never need to see a big list of values. - Add generator persistence to number collection field values. - Update all logic that references field values to use "resolved" field values if the field is a number collection field. This includes validation logic. - Rework the generator UI. Generators are now part of each field, not a separate modal. You can enable the generator, reset its values, commit them and then edit them. Or, disable the generator to manually edit the values. - Support locking the linear view mode. If the workflow editor locks the field, the linear view will be slimmed down, showing only the generator fields. - Rework how the "reset to default value" functionality works with exposed fields to also work with generators. **Unfortunately, this did require some changes to redux state that I cannot easily handle in a redux state migration. As a result, on the first run after updating Invoke, their workflow editor state will be erased.**
1 parent a0bc48a commit facfb84

File tree

25 files changed

+719
-348
lines changed

25 files changed

+719
-348
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,13 @@
855855
},
856856
"nodes": {
857857
"noBatchGroup": "no group",
858+
"generator": "Generator",
859+
"generatedValues": "Generated Values",
860+
"commitValues": "Commit Values",
861+
"addValue": "Add Value",
858862
"addNode": "Add Node",
863+
"lockLinearView": "Lock Linear View",
864+
"unlockLinearView": "Unlock Linear View",
859865
"addNodeToolTip": "Add Node (Shift+A, Space)",
860866
"addLinearView": "Add to Linear View",
861867
"animatedEdges": "Animated Edges",
@@ -994,11 +1000,7 @@
9941000
"imageAccessError": "Unable to find image {{image_name}}, resetting to default",
9951001
"boardAccessError": "Unable to find board {{board_id}}, resetting to default",
9961002
"modelAccessError": "Unable to find model {{key}}, resetting to default",
997-
"saveToGallery": "Save To Gallery",
998-
"addItem": "Add Item",
999-
"generateValues": "Generate Values",
1000-
"floatRangeGenerator": "Float Range Generator",
1001-
"integerRangeGenerator": "Integer Range Generator"
1003+
"saveToGallery": "Save To Gallery"
10021004
},
10031005
"parameters": {
10041006
"aspect": "Aspect",

invokeai/frontend/web/src/app/components/App.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
2222
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
2323
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
2424
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
25-
import { FloatRangeGeneratorModal } from 'features/nodes/components/FloatRangeGeneratorModal';
26-
import { IntegerRangeGeneratorModal } from 'features/nodes/components/IntegerRangeGeneratorModal';
2725
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal';
2826
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
2927
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
@@ -112,8 +110,6 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
112110
<ImageContextMenu />
113111
<FullscreenDropzone />
114112
<VideosModal />
115-
<FloatRangeGeneratorModal />
116-
<IntegerRangeGeneratorModal />
117113
</ErrorBoundary>
118114
);
119115
};

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isIntegerFieldCollectionInputInstance,
1010
isStringFieldCollectionInputInstance,
1111
} from 'features/nodes/types/field';
12+
import { resolveNumberFieldCollectionValue } from 'features/nodes/types/fieldValidators';
1213
import type { InvocationNodeEdge } from 'features/nodes/types/invocation';
1314
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
1415
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
@@ -140,10 +141,11 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
140141

141142
// Find outgoing edges from the batch node, we will remove these from the graph and create batch data collection items from them instead
142143
const edgesFromStringBatch = nodes.edges.filter((e) => e.source === node.id && e.sourceHandle === 'value');
144+
const resolvedValue = resolveNumberFieldCollectionValue(integers);
143145
if (batchGroupId !== 'None') {
144-
addZippedBatchDataCollectionItem(edgesFromStringBatch, integers.value);
146+
addZippedBatchDataCollectionItem(edgesFromStringBatch, resolvedValue);
145147
} else {
146-
addProductBatchDataCollectionItem(edgesFromStringBatch, integers.value);
148+
addProductBatchDataCollectionItem(edgesFromStringBatch, resolvedValue);
147149
}
148150
}
149151

@@ -163,10 +165,11 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
163165

164166
// Find outgoing edges from the batch node, we will remove these from the graph and create batch data collection items from them instead
165167
const edgesFromStringBatch = nodes.edges.filter((e) => e.source === node.id && e.sourceHandle === 'value');
168+
const resolvedValue = resolveNumberFieldCollectionValue(floats);
166169
if (batchGroupId !== 'None') {
167-
addZippedBatchDataCollectionItem(edgesFromStringBatch, floats.value);
170+
addZippedBatchDataCollectionItem(edgesFromStringBatch, resolvedValue);
168171
} else {
169-
addProductBatchDataCollectionItem(edgesFromStringBatch, floats.value);
172+
addProductBatchDataCollectionItem(edgesFromStringBatch, resolvedValue);
170173
}
171174
}
172175

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
166166
reducer: rememberedRootReducer,
167167
middleware: (getDefaultMiddleware) =>
168168
getDefaultMiddleware({
169-
serializableCheck: import.meta.env.MODE === 'development',
170-
immutableCheck: import.meta.env.MODE === 'development',
169+
serializableCheck: false,
170+
immutableCheck: false,
171+
// serializableCheck: import.meta.env.MODE === 'development',
172+
// immutableCheck: import.meta.env.MODE === 'development',
171173
})
172174
.concat(api.middleware)
173175
.concat(dynamicMiddlewares)

invokeai/frontend/web/src/features/nodes/components/FloatRangeGeneratorModal.tsx

Lines changed: 0 additions & 105 deletions
This file was deleted.

invokeai/frontend/web/src/features/nodes/components/IntegerRangeGeneratorModal.tsx

Lines changed: 0 additions & 103 deletions
This file was deleted.

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
4343
{fieldNames.connectionFields.map((fieldName, i) => (
4444
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
4545
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
46-
<InputField nodeId={nodeId} fieldName={fieldName} />
46+
<InputField nodeId={nodeId} fieldName={fieldName} isLinearView={false} />
4747
</InvocationInputFieldCheck>
4848
</GridItem>
4949
))}
@@ -59,7 +59,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
5959
nodeId={nodeId}
6060
fieldName={fieldName}
6161
>
62-
<InputField nodeId={nodeId} fieldName={fieldName} />
62+
<InputField nodeId={nodeId} fieldName={fieldName} isLinearView={false} />
6363
</InvocationInputFieldCheck>
6464
))}
6565
{fieldNames.missingFields.map((fieldName) => (
@@ -68,7 +68,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
6868
nodeId={nodeId}
6969
fieldName={fieldName}
7070
>
71-
<InputField nodeId={nodeId} fieldName={fieldName} />
71+
<InputField nodeId={nodeId} fieldName={fieldName} isLinearView={false} />
7272
</InvocationInputFieldCheck>
7373
))}
7474
</Flex>

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IconButton } from '@invoke-ai/ui-library';
22
import { createSelector } from '@reduxjs/toolkit';
33
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
4-
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
4+
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
55
import {
66
selectWorkflowSlice,
77
workflowExposedFieldAdded,
@@ -19,7 +19,7 @@ type Props = {
1919
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
2020
const dispatch = useAppDispatch();
2121
const { t } = useTranslation();
22-
const value = useFieldValue(nodeId, fieldName);
22+
const field = useFieldInputInstance(nodeId, fieldName);
2323
const selectIsExposed = useMemo(
2424
() =>
2525
createSelector(selectWorkflowSlice, (workflow) => {
@@ -31,8 +31,11 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
3131
const isExposed = useAppSelector(selectIsExposed);
3232

3333
const handleExposeField = useCallback(() => {
34-
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
35-
}, [dispatch, fieldName, nodeId, value]);
34+
if (!field) {
35+
return;
36+
}
37+
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, field }));
38+
}, [dispatch, field, fieldName, nodeId]);
3639

3740
const handleUnexposeField = useCallback(() => {
3841
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { InputFieldWrapper } from './InputFieldWrapper';
1414
interface Props {
1515
nodeId: string;
1616
fieldName: string;
17+
isLinearView: boolean;
1718
}
1819

19-
const InputField = ({ nodeId, fieldName }: Props) => {
20+
const InputField = ({ nodeId, fieldName, isLinearView }: Props) => {
2021
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
2122
const [isHovered, setIsHovered] = useState(false);
2223
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
@@ -69,12 +70,12 @@ const InputField = ({ nodeId, fieldName }: Props) => {
6970
px={2}
7071
>
7172
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
72-
<Flex gap={1}>
73+
<Flex gap={1} alignItems="center">
7374
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
7475
{isHovered && <FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />}
7576
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
7677
</Flex>
77-
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
78+
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} isLinearView={isLinearView} />
7879
</Flex>
7980
</FormControl>
8081

0 commit comments

Comments
 (0)