Skip to content

Commit 36066c5

Browse files
fix(ui): ensure dynamic prompts updates on any change to any dependent state
When users generate on the canvas or upscaling tabs, we parse prompts through dynamic prompts before invoking. Whenever the prompt or other settings change, we run dynamic prompts. Previously, we used a redux listener to react to changes to dynamic prompts' dependent state, keeping the processed dynamic prompts synced. For example, when the user changed the prompt field, we re-processed the dynamic prompts. This requires that all redux actions that change the dependent state be added to the listener matcher. It's easy to forget actions, though, which can result in the dynamic prompts state being stale. For example, when resetting canvas state, we dispatch an action that resets the whole params slice, but this wasn't in the matcher. As a result, when resetting canvas, the dynamic prompts aren't updated. If the user then clicks Invoke (with an empty prompt), the last dynamic prompts state will be used. For example: - Generate w/ prompt "frog", get frog - Click new canvas session - Generate without any prompt, still get frog To resolve this, the logic that keeps the dynamic prompts synced is moved from the listener to a hook. The way the logic is triggered is improved - it's now triggered in a useEffect, which is run when the dependent state changes. This way, it doesn't matter _how_ the dependent state changes - the changes will always be "seen", and the dynamic prompts will update.
1 parent 361c6ee commit 36066c5

File tree

14 files changed

+155
-153
lines changed

14 files changed

+155
-153
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
99
import type { PartialAppConfig } from 'app/types/invokeai';
1010
import { useFocusRegionWatcher } from 'common/hooks/focus';
1111
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
12+
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
1213
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
1314
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
1415
import { useReadinessWatcher } from 'features/queue/store/readiness';
@@ -58,6 +59,7 @@ export const GlobalHookIsolator = memo(
5859
useSyncQueueStatus();
5960
useFocusRegionWatcher();
6061
useWorkflowBuilderWatcher();
62+
useDynamicPromptsWatcher();
6163

6264
return null;
6365
}

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerM
2222
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
2323
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
2424
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
25-
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
2625
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
2726
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
2827
import type { AppDispatch, RootState } from 'app/store/store';
@@ -95,7 +94,4 @@ addAppConfigReceivedListener(startAppListening);
9594
// Ad-hoc upscale workflwo
9695
addAdHocPostProcessingRequestedListener(startAppListening);
9796

98-
// Prompts
99-
addDynamicPromptsListener(startAppListening);
100-
10197
addSetDefaultSettingsListener(startAppListening);

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

Lines changed: 0 additions & 89 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useAppStore } from 'app/store/nanostores/store';
2+
import { useAppSelector } from 'app/store/storeHooks';
3+
import {
4+
isErrorChanged,
5+
isLoadingChanged,
6+
parsingErrorChanged,
7+
promptsChanged,
8+
selectDynamicPromptsMaxPrompts,
9+
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
10+
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
11+
import { selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
12+
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
13+
import { debounce } from 'lodash-es';
14+
import { useEffect, useMemo } from 'react';
15+
import { utilitiesApi } from 'services/api/endpoints/utilities';
16+
17+
const DYNAMIC_PROMPTS_DEBOUNCE_MS = 1000;
18+
19+
/**
20+
* This hook watches for changes to state that should trigger dynamic prompts to be updated.
21+
*/
22+
export const useDynamicPromptsWatcher = () => {
23+
const { getState, dispatch } = useAppStore();
24+
// The prompt to process is derived from the preset-modified prompts
25+
const presetModifiedPrompts = useAppSelector(selectPresetModifiedPrompts);
26+
const maxPrompts = useAppSelector(selectDynamicPromptsMaxPrompts);
27+
28+
const dynamicPrompting = useFeatureStatus('dynamicPrompting');
29+
30+
const debouncedUpdateDynamicPrompts = useMemo(
31+
() =>
32+
debounce(async (positivePrompt: string, maxPrompts: number) => {
33+
// Try to fetch the dynamic prompts and store in state
34+
try {
35+
const req = dispatch(
36+
utilitiesApi.endpoints.dynamicPrompts.initiate({
37+
prompt: positivePrompt,
38+
max_prompts: maxPrompts,
39+
})
40+
);
41+
42+
const res = await req.unwrap();
43+
req.unsubscribe();
44+
45+
dispatch(promptsChanged(res.prompts));
46+
dispatch(parsingErrorChanged(res.error));
47+
dispatch(isErrorChanged(false));
48+
} catch {
49+
dispatch(isErrorChanged(true));
50+
dispatch(isLoadingChanged(false));
51+
}
52+
}, DYNAMIC_PROMPTS_DEBOUNCE_MS),
53+
[dispatch]
54+
);
55+
56+
useEffect(() => {
57+
if (!dynamicPrompting) {
58+
return;
59+
}
60+
61+
const { positivePrompt } = presetModifiedPrompts;
62+
63+
// Before we execute, imperatively check the dynamic prompts query cache to see if we have already fetched this prompt
64+
const state = getState();
65+
66+
const cachedPrompts = utilitiesApi.endpoints.dynamicPrompts.select({
67+
prompt: positivePrompt,
68+
max_prompts: maxPrompts,
69+
})(state).data;
70+
71+
if (cachedPrompts) {
72+
// Yep we already did this prompt, use the cached result
73+
dispatch(promptsChanged(cachedPrompts.prompts));
74+
dispatch(parsingErrorChanged(cachedPrompts.error));
75+
return;
76+
}
77+
78+
// If the prompt is not in the cache, check if we should process it - this is just looking for dynamic prompts syntax
79+
if (!getShouldProcessPrompt(positivePrompt)) {
80+
dispatch(promptsChanged([positivePrompt]));
81+
dispatch(parsingErrorChanged(undefined));
82+
dispatch(isErrorChanged(false));
83+
return;
84+
}
85+
86+
// If we are here, we need to process the prompt
87+
if (!state.dynamicPrompts.isLoading) {
88+
dispatch(isLoadingChanged(true));
89+
}
90+
91+
debouncedUpdateDynamicPrompts(positivePrompt, maxPrompts);
92+
}, [debouncedUpdateDynamicPrompts, dispatch, dynamicPrompting, getState, maxPrompts, presetModifiedPrompts]);
93+
};

invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ export const dynamicPromptsSlice = createSlice({
3636
maxPromptsChanged: (state, action: PayloadAction<number>) => {
3737
state.maxPrompts = action.payload;
3838
},
39-
maxPromptsReset: (state) => {
40-
state.maxPrompts = initialDynamicPromptsState.maxPrompts;
41-
},
42-
combinatorialToggled: (state) => {
43-
state.combinatorial = !state.combinatorial;
44-
},
4539
promptsChanged: (state, action: PayloadAction<string[]>) => {
4640
state.prompts = action.payload;
4741
state.isLoading = false;
@@ -63,8 +57,6 @@ export const dynamicPromptsSlice = createSlice({
6357

6458
export const {
6559
maxPromptsChanged,
66-
maxPromptsReset,
67-
combinatorialToggled,
6860
promptsChanged,
6961
parsingErrorChanged,
7062
isErrorChanged,

invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { isNonRefinerMainModelConfig, isSpandrelImageToImageModelConfig } from '
88
import { assert } from 'tsafe';
99

1010
import { addLoRAs } from './generation/addLoRAs';
11-
import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils';
11+
import { getBoardField, selectPresetModifiedPrompts } from './graphBuilderUtils';
1212

1313
export const buildMultidiffusionUpscaleGraph = async (
1414
state: RootState
@@ -97,7 +97,7 @@ export const buildMultidiffusionUpscaleGraph = async (
9797

9898
if (model.base === 'sdxl') {
9999
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } =
100-
getPresetModifiedPrompts(state);
100+
selectPresetModifiedPrompts(state);
101101

102102
posCond = g.addNode({
103103
type: 'sdxl_compel_prompt',
@@ -130,7 +130,7 @@ export const buildMultidiffusionUpscaleGraph = async (
130130
negative_style_prompt: negativeStylePrompt,
131131
});
132132
} else {
133-
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
133+
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
134134

135135
posCond = g.addNode({
136136
type: 'compel',

invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import { Graph } from 'features/nodes/util/graph/generation/Graph';
1616
import {
1717
CANVAS_OUTPUT_PREFIX,
1818
getBoardField,
19-
getPresetModifiedPrompts,
2019
getSizes,
20+
selectPresetModifiedPrompts,
2121
} from 'features/nodes/util/graph/graphBuilderUtils';
2222
import type { ImageOutputNodes } from 'features/nodes/util/graph/types';
2323
import type { Invocation } from 'services/api/types';
@@ -45,7 +45,7 @@ export const buildCogView4Graph = async (
4545
assert(model, 'No model found in state');
4646

4747
const { originalSize, scaledSize } = getSizes(bbox);
48-
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
48+
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
4949

5050
const g = new Graph(getPrefixedId('cogview4_graph'));
5151
const modelLoader = g.addNode({

invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { Graph } from 'features/nodes/util/graph/generation/Graph';
1919
import {
2020
CANVAS_OUTPUT_PREFIX,
2121
getBoardField,
22-
getPresetModifiedPrompts,
2322
getSizes,
23+
selectPresetModifiedPrompts,
2424
} from 'features/nodes/util/graph/graphBuilderUtils';
2525
import type { ImageOutputNodes } from 'features/nodes/util/graph/types';
2626
import { t } from 'i18next';
@@ -91,7 +91,7 @@ export const buildFLUXGraph = async (
9191
guidance = 30;
9292
}
9393

94-
const { positivePrompt } = getPresetModifiedPrompts(state);
94+
const { positivePrompt } = selectPresetModifiedPrompts(state);
9595

9696
const g = new Graph(getPrefixedId('flux_graph'));
9797
const modelLoader = g.addNode({

invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import { Graph } from 'features/nodes/util/graph/generation/Graph';
2020
import {
2121
CANVAS_OUTPUT_PREFIX,
2222
getBoardField,
23-
getPresetModifiedPrompts,
2423
getSizes,
24+
selectPresetModifiedPrompts,
2525
} from 'features/nodes/util/graph/graphBuilderUtils';
2626
import type { ImageOutputNodes } from 'features/nodes/util/graph/types';
2727
import { selectMainModelConfig } from 'services/api/endpoints/models';
@@ -62,7 +62,7 @@ export const buildSD1Graph = async (
6262
assert(model, 'No model found in state');
6363

6464
const fp32 = vaePrecision === 'fp32';
65-
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
65+
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
6666
const { originalSize, scaledSize } = getSizes(bbox);
6767

6868
const g = new Graph(getPrefixedId('sd1_graph'));

invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { Graph } from 'features/nodes/util/graph/generation/Graph';
1515
import {
1616
CANVAS_OUTPUT_PREFIX,
1717
getBoardField,
18-
getPresetModifiedPrompts,
1918
getSizes,
19+
selectPresetModifiedPrompts,
2020
} from 'features/nodes/util/graph/graphBuilderUtils';
2121
import type { ImageOutputNodes } from 'features/nodes/util/graph/types';
2222
import { selectMainModelConfig } from 'services/api/endpoints/models';
@@ -56,7 +56,7 @@ export const buildSD3Graph = async (
5656
} = params;
5757

5858
const { originalSize, scaledSize } = getSizes(bbox);
59-
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
59+
const { positivePrompt, negativePrompt } = selectPresetModifiedPrompts(state);
6060

6161
const g = new Graph(getPrefixedId('sd3_graph'));
6262
const modelLoader = g.addNode({

0 commit comments

Comments
 (0)