Skip to content

Commit cd8d456

Browse files
authored
Merge branch 'main' into model-manager-visual-updates
2 parents c7d5726 + b0aa48d commit cd8d456

File tree

8 files changed

+222
-4
lines changed

8 files changed

+222
-4
lines changed

invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { RootState } from 'app/store/store';
44
import type { SliceConfig } from 'app/store/types';
55
import { deepClone } from 'common/util/deepClone';
66
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
7-
import { isPlainObject } from 'es-toolkit';
7+
import { isPlainObject, uniq } from 'es-toolkit';
88
import { clamp } from 'es-toolkit/compat';
99
import type { AspectRatioID, ParamsState, RgbaColor } from 'features/controlLayers/store/types';
1010
import {
@@ -19,6 +19,7 @@ import {
1919
isFluxKontextAspectRatioID,
2020
isGemini2_5AspectRatioID,
2121
isImagenAspectRatioID,
22+
MAX_POSITIVE_PROMPT_HISTORY,
2223
zParamsState,
2324
} from 'features/controlLayers/store/types';
2425
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
@@ -192,6 +193,27 @@ const slice = createSlice({
192193
positivePromptChanged: (state, action: PayloadAction<ParameterPositivePrompt>) => {
193194
state.positivePrompt = action.payload;
194195
},
196+
positivePromptAddedToHistory: (state, action: PayloadAction<ParameterPositivePrompt>) => {
197+
const prompt = action.payload.trim();
198+
if (prompt.length === 0) {
199+
return;
200+
}
201+
// Remove if already exists
202+
state.positivePromptHistory = uniq(state.positivePromptHistory);
203+
204+
// Add to front
205+
state.positivePromptHistory.unshift(prompt);
206+
207+
if (state.positivePromptHistory.length > MAX_POSITIVE_PROMPT_HISTORY) {
208+
state.positivePromptHistory = state.positivePromptHistory.slice(0, MAX_POSITIVE_PROMPT_HISTORY);
209+
}
210+
},
211+
promptRemovedFromHistory: (state, action: PayloadAction<string>) => {
212+
state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload);
213+
},
214+
promptHistoryCleared: (state) => {
215+
state.positivePromptHistory = [];
216+
},
195217
negativePromptChanged: (state, action: PayloadAction<ParameterNegativePrompt>) => {
196218
state.negativePrompt = action.payload;
197219
},
@@ -462,6 +484,9 @@ export const {
462484
setClipSkip,
463485
shouldUseCpuNoiseChanged,
464486
positivePromptChanged,
487+
positivePromptAddedToHistory,
488+
promptRemovedFromHistory,
489+
promptHistoryCleared,
465490
negativePromptChanged,
466491
refinerModelChanged,
467492
setRefinerSteps,
@@ -500,6 +525,12 @@ export const paramsSliceConfig: SliceConfig<typeof slice> = {
500525
state.dimensions.height = state.dimensions.rect.height;
501526
}
502527

528+
if (state._version === 1) {
529+
// v1 -> v2, add positive prompt history
530+
state._version = 2;
531+
state.positivePromptHistory = [];
532+
}
533+
503534
return zParamsState.parse(state);
504535
},
505536
},
@@ -600,6 +631,7 @@ export const selectShouldUseCPUNoise = createParamsSelector((params) => params.s
600631
export const selectUpscaleScheduler = createParamsSelector((params) => params.upscaleScheduler);
601632
export const selectUpscaleCfgScale = createParamsSelector((params) => params.upscaleCfgScale);
602633

634+
export const selectPositivePromptHistory = createParamsSelector((params) => params.positivePromptHistory);
603635
export const selectRefinerCFGScale = createParamsSelector((params) => params.refinerCFGScale);
604636
export const selectRefinerModel = createParamsSelector((params) => params.refinerModel);
605637
export const selectIsRefinerModelSelected = createParamsSelector((params) => Boolean(params.refinerModel));

invokeai/frontend/web/src/features/controlLayers/store/types.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ export const zLoRA = z.object({
470470
id: z.string(),
471471
isEnabled: z.boolean(),
472472
model: zModelIdentifierField,
473-
weight: z.number().gte(-1).lte(2),
473+
weight: z.number().gte(-10).lte(10),
474474
});
475475
export type LoRA = z.infer<typeof zLoRA>;
476476

@@ -612,8 +612,13 @@ const zDimensionsState = z.object({
612612
aspectRatio: zAspectRatioConfig,
613613
});
614614

615+
export const MAX_POSITIVE_PROMPT_HISTORY = 100;
616+
const zPositivePromptHistory = z
617+
.array(zParameterPositivePrompt)
618+
.transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY));
619+
615620
export const zParamsState = z.object({
616-
_version: z.literal(1),
621+
_version: z.literal(2),
617622
maskBlur: z.number(),
618623
maskBlurMethod: zParameterMaskBlurMethod,
619624
canvasCoherenceMode: zParameterCanvasCoherenceMode,
@@ -644,6 +649,7 @@ export const zParamsState = z.object({
644649
clipSkip: z.number(),
645650
shouldUseCpuNoise: z.boolean(),
646651
positivePrompt: zParameterPositivePrompt,
652+
positivePromptHistory: zPositivePromptHistory,
647653
negativePrompt: zParameterNegativePrompt,
648654
refinerModel: zParameterSDXLRefinerModel.nullable(),
649655
refinerSteps: z.number(),
@@ -661,7 +667,7 @@ export const zParamsState = z.object({
661667
});
662668
export type ParamsState = z.infer<typeof zParamsState>;
663669
export const getInitialParamsState = (): ParamsState => ({
664-
_version: 1,
670+
_version: 2,
665671
maskBlur: 16,
666672
maskBlurMethod: 'box',
667673
canvasCoherenceMode: 'Gaussian Blur',
@@ -692,6 +698,7 @@ export const getInitialParamsState = (): ParamsState => ({
692698
clipSkip: 0,
693699
shouldUseCpuNoise: true,
694700
positivePrompt: '',
701+
positivePromptHistory: [],
695702
negativePrompt: null,
696703
refinerModel: null,
697704
refinerSteps: 20,

invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import type { HotkeyCallback } from 'react-hotkeys-hook';
3232
import { useTranslation } from 'react-i18next';
3333
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
3434

35+
import { PositivePromptHistoryIconButton } from './PositivePromptHistory';
36+
3537
const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
3638
trackWidth: false,
3739
trackHeight: true,
@@ -118,6 +120,7 @@ export const ParamPositivePrompt = memo(() => {
118120
<Flex flexDir="column" gap={2} justifyContent="flex-start" alignItems="center">
119121
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
120122
<ShowDynamicPromptsPreviewButton />
123+
<PositivePromptHistoryIconButton />
121124
{activeTab !== 'video' && modelSupportsNegativePrompt && <NegativePromptToggleButton />}
122125
</Flex>
123126
{isPromptExpansionEnabled && <PromptExpansionMenu />}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import {
2+
Button,
3+
Divider,
4+
Flex,
5+
IconButton,
6+
Input,
7+
Popover,
8+
PopoverBody,
9+
PopoverContent,
10+
PopoverTrigger,
11+
Portal,
12+
Text,
13+
useShiftModifier,
14+
} from '@invoke-ai/ui-library';
15+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
16+
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
17+
import {
18+
positivePromptChanged,
19+
promptHistoryCleared,
20+
promptRemovedFromHistory,
21+
selectPositivePromptHistory,
22+
} from 'features/controlLayers/store/paramsSlice';
23+
import type { ChangeEvent } from 'react';
24+
import { memo, useCallback, useMemo, useState } from 'react';
25+
import { PiArrowArcLeftBold, PiClockCounterClockwise, PiTrashBold, PiTrashSimpleBold } from 'react-icons/pi';
26+
27+
export const PositivePromptHistoryIconButton = memo(() => {
28+
return (
29+
<Popover isLazy>
30+
<PopoverTrigger>
31+
<IconButton
32+
size="sm"
33+
variant="promptOverlay"
34+
aria-label="Positive Prompt History"
35+
icon={<PiClockCounterClockwise />}
36+
tooltip="Prompt History"
37+
/>
38+
</PopoverTrigger>
39+
<Portal>
40+
<PopoverContent>
41+
<PopoverBody maxH={300} maxW={400} h={300} w={400}>
42+
<PromptHistoryContent />
43+
</PopoverBody>
44+
</PopoverContent>
45+
</Portal>
46+
</Popover>
47+
);
48+
});
49+
50+
PositivePromptHistoryIconButton.displayName = 'PositivePromptHistoryIconButton';
51+
52+
const PromptHistoryContent = memo(() => {
53+
const dispatch = useAppDispatch();
54+
const positivePromptHistory = useAppSelector(selectPositivePromptHistory);
55+
const [searchTerm, setSearchTerm] = useState('');
56+
57+
const onClickClearHistory = useCallback(() => {
58+
dispatch(promptHistoryCleared());
59+
}, [dispatch]);
60+
61+
const filteredPrompts = useMemo(() => {
62+
const trimmedSearchTerm = searchTerm.trim();
63+
if (!trimmedSearchTerm) {
64+
return positivePromptHistory;
65+
}
66+
return positivePromptHistory.filter((prompt) => prompt.toLowerCase().includes(trimmedSearchTerm.toLowerCase()));
67+
}, [positivePromptHistory, searchTerm]);
68+
69+
const onChangeSearchTerm = useCallback((e: ChangeEvent<HTMLInputElement>) => {
70+
setSearchTerm(e.target.value);
71+
}, []);
72+
73+
return (
74+
<Flex flexDir="column" gap={2} w="full" h="full">
75+
<Flex alignItems="center" gap={2} justifyContent="space-between">
76+
<Text fontWeight="semibold" color="base.300">
77+
Prompt History
78+
</Text>
79+
<Input
80+
size="sm"
81+
variant="outline"
82+
placeholder="Search..."
83+
value={searchTerm}
84+
onChange={onChangeSearchTerm}
85+
width="max-content"
86+
isDisabled={positivePromptHistory.length === 0}
87+
/>
88+
<Button
89+
size="sm"
90+
variant="link"
91+
leftIcon={<PiTrashSimpleBold />}
92+
onClick={onClickClearHistory}
93+
isDisabled={positivePromptHistory.length === 0}
94+
>
95+
Clear History
96+
</Button>
97+
</Flex>
98+
<Divider />
99+
{positivePromptHistory.length === 0 && (
100+
<Flex w="full" h="full" alignItems="center" justifyContent="center">
101+
<Text color="base.300">No prompt history recorded.</Text>
102+
</Flex>
103+
)}
104+
{positivePromptHistory.length !== 0 && filteredPrompts.length === 0 && (
105+
<Flex w="full" h="full" alignItems="center" justifyContent="center">
106+
<Text color="base.300">No matching prompts in history.</Text>{' '}
107+
</Flex>
108+
)}
109+
{filteredPrompts.length > 0 && (
110+
<ScrollableContent>
111+
<Flex flexDir="column">
112+
{filteredPrompts.map((prompt, index) => (
113+
<PromptItem key={`${prompt}-${index}`} prompt={prompt} />
114+
))}
115+
</Flex>
116+
</ScrollableContent>
117+
)}
118+
</Flex>
119+
);
120+
});
121+
PromptHistoryContent.displayName = 'PromptHistoryContent';
122+
123+
const PromptItem = memo(({ prompt }: { prompt: string }) => {
124+
const dispatch = useAppDispatch();
125+
const shiftKey = useShiftModifier();
126+
127+
const onClickUse = useCallback(() => {
128+
dispatch(positivePromptChanged(prompt));
129+
}, [dispatch, prompt]);
130+
131+
const onClickDelete = useCallback(() => {
132+
dispatch(promptRemovedFromHistory(prompt));
133+
}, [dispatch, prompt]);
134+
135+
return (
136+
<Flex gap={2}>
137+
{!shiftKey && (
138+
<IconButton
139+
size="sm"
140+
variant="ghost"
141+
aria-label="Use prompt"
142+
icon={<PiArrowArcLeftBold />}
143+
onClick={onClickUse}
144+
/>
145+
)}
146+
{shiftKey && (
147+
<IconButton
148+
size="sm"
149+
variant="ghost"
150+
aria-label="Delete"
151+
icon={<PiTrashBold />}
152+
onClick={onClickDelete}
153+
colorScheme="error"
154+
/>
155+
)}
156+
<Text color="base.300">{prompt}</Text>
157+
</Flex>
158+
);
159+
});
160+
PromptItem.displayName = 'PromptItem';

invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom
77
import { withResult, withResultAsync } from 'common/util/result';
88
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
99
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
10+
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
1011
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
1112
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
1213
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
@@ -130,6 +131,9 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
130131

131132
const enqueueResult = await req.unwrap();
132133

134+
// Push to prompt history on successful enqueue
135+
dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));
136+
133137
return { batchConfig, enqueueResult };
134138
};
135139

invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AppStore } from 'app/store/store';
55
import { useAppStore } from 'app/store/storeHooks';
66
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
77
import { withResult, withResultAsync } from 'common/util/result';
8+
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
89
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
910
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
1011
import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph';
@@ -124,6 +125,9 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
124125

125126
const enqueueResult = await req.unwrap();
126127

128+
// Push to prompt history on successful enqueue
129+
dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));
130+
127131
return { batchConfig, enqueueResult };
128132
};
129133

invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
22
import { logger } from 'app/logging/logger';
33
import type { AppStore } from 'app/store/store';
44
import { useAppStore } from 'app/store/storeHooks';
5+
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
56
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
67
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
78
import { useCallback } from 'react';
@@ -43,6 +44,9 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => {
4344
);
4445
const enqueueResult = await req.unwrap();
4546

47+
// Push to prompt history on successful enqueue
48+
dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));
49+
4650
return { batchConfig, enqueueResult };
4751
};
4852

invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AppStore } from 'app/store/store';
55
import { useAppStore } from 'app/store/storeHooks';
66
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
77
import { withResult, withResultAsync } from 'common/util/result';
8+
import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice';
89
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
910
import { buildRunwayVideoGraph } from 'features/nodes/util/graph/generation/buildRunwayVideoGraph';
1011
import { buildVeo3VideoGraph } from 'features/nodes/util/graph/generation/buildVeo3VideoGraph';
@@ -108,6 +109,9 @@ const enqueueVideo = async (store: AppStore, prepend: boolean) => {
108109

109110
const enqueueResult = await req.unwrap();
110111

112+
// Push to prompt history on successful enqueue
113+
dispatch(positivePromptAddedToHistory(selectPositivePrompt(state)));
114+
111115
return { batchConfig, enqueueResult };
112116
};
113117

0 commit comments

Comments
 (0)