mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Prompt history shortcuts
This commit is contained in:
committed by
psychedelicious
parent
4528bcafaf
commit
d6442d9a34
@@ -235,6 +235,7 @@
|
||||
"compatibleEmbeddings": "Compatible Embeddings",
|
||||
"noMatchingTriggers": "No matching triggers",
|
||||
"generateFromImage": "Generate prompt from image",
|
||||
"historyHotkeyHint": "Tip: Use Alt+Up/Down to browse history from the prompt.",
|
||||
"expandCurrentPrompt": "Expand Current Prompt",
|
||||
"uploadImageForPromptGeneration": "Upload Image for Prompt Generation",
|
||||
"expandingPrompt": "Expanding prompt...",
|
||||
@@ -480,6 +481,14 @@
|
||||
"title": "Focus Prompt",
|
||||
"desc": "Move cursor focus to the positive prompt."
|
||||
},
|
||||
"promptHistoryPrev": {
|
||||
"title": "Previous Prompt in History",
|
||||
"desc": "When the prompt is focused, move to the previous (older) prompt in your history."
|
||||
},
|
||||
"promptHistoryNext": {
|
||||
"title": "Next Prompt in History",
|
||||
"desc": "When the prompt is focused, move to the next (newer) prompt in your history."
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Toggle Left Panel",
|
||||
"desc": "Show or hide the left panel."
|
||||
|
||||
@@ -199,11 +199,11 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.positivePromptHistory.includes(prompt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.positivePromptHistory.unshift(prompt);
|
||||
state.positivePromptHistory = [
|
||||
prompt,
|
||||
...state.positivePromptHistory.filter((p) => p !== prompt),
|
||||
];
|
||||
|
||||
if (state.positivePromptHistory.length > MAX_POSITIVE_PROMPT_HISTORY) {
|
||||
state.positivePromptHistory = state.positivePromptHistory.slice(0, MAX_POSITIVE_PROMPT_HISTORY);
|
||||
|
||||
@@ -3,9 +3,11 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize';
|
||||
import {
|
||||
positivePromptAddedToHistory,
|
||||
positivePromptChanged,
|
||||
selectModelSupportsNegativePrompt,
|
||||
selectPositivePrompt,
|
||||
selectPositivePromptHistory,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { promptGenerationFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
@@ -27,7 +29,7 @@ import {
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectAllowPromptExpansion } from 'features/system/store/configSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
|
||||
@@ -43,6 +45,7 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
|
||||
export const ParamPositivePrompt = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prompt = useAppSelector(selectPositivePrompt);
|
||||
const history = useAppSelector(selectPositivePromptHistory);
|
||||
const viewMode = useAppSelector(selectStylePresetViewMode);
|
||||
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
|
||||
const modelSupportsNegativePrompt = useAppSelector(selectModelSupportsNegativePrompt);
|
||||
@@ -77,6 +80,22 @@ export const ParamPositivePrompt = memo(() => {
|
||||
isDisabled: isPromptExpansionPending,
|
||||
});
|
||||
|
||||
// Browsing state for boundary Up/Down traversal
|
||||
const browsingIndexRef = useRef<number | null>(null); // null => not browsing; 0..n => index in history
|
||||
const preBrowsePromptRef = useRef<string>(''); // original prompt when browsing started
|
||||
const lastHistoryFirstRef = useRef<string | undefined>(undefined);
|
||||
|
||||
// Reset browsing when history updates due to a new generation (first item changes or history mutates)
|
||||
useEffect(() => {
|
||||
if (lastHistoryFirstRef.current !== history[0]) {
|
||||
browsingIndexRef.current = null;
|
||||
preBrowsePromptRef.current = '';
|
||||
lastHistoryFirstRef.current = history[0];
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
// Boundary navigation via Up/Down keys was replaced by explicit hotkeys below.
|
||||
|
||||
const focus: HotkeyCallback = useCallback(
|
||||
(e) => {
|
||||
onFocus();
|
||||
@@ -93,6 +112,112 @@ export const ParamPositivePrompt = memo(() => {
|
||||
dependencies: [focus],
|
||||
});
|
||||
|
||||
// Helper: check if prompt textarea is focused
|
||||
const isPromptFocused = useCallback(() => document.activeElement === textareaRef.current, []);
|
||||
|
||||
// Compute a starting working history and ensure current prompt is bumped into history
|
||||
const startBrowsing = useCallback(() => {
|
||||
if (browsingIndexRef.current !== null) return;
|
||||
preBrowsePromptRef.current = prompt ?? '';
|
||||
const trimmedCurrent = (prompt ?? '').trim();
|
||||
if (trimmedCurrent) {
|
||||
dispatch(positivePromptAddedToHistory(trimmedCurrent));
|
||||
}
|
||||
browsingIndexRef.current = 0;
|
||||
}, [dispatch, prompt]);
|
||||
|
||||
const applyHistoryAtIndex = useCallback(
|
||||
(idx: number, placeCaretAt: 'start' | 'end') => {
|
||||
const list = history;
|
||||
if (list.length === 0) return;
|
||||
const clamped = Math.max(0, Math.min(idx, list.length - 1));
|
||||
browsingIndexRef.current = clamped;
|
||||
dispatch(positivePromptChanged(list[clamped]));
|
||||
requestAnimationFrame(() => {
|
||||
const el = textareaRef.current;
|
||||
if (!el) return;
|
||||
if (placeCaretAt === 'start') {
|
||||
el.selectionStart = 0;
|
||||
el.selectionEnd = 0;
|
||||
} else {
|
||||
const end = el.value.length;
|
||||
el.selectionStart = end;
|
||||
el.selectionEnd = end;
|
||||
}
|
||||
});
|
||||
},
|
||||
[dispatch, history]
|
||||
);
|
||||
|
||||
const browsePrev = useCallback(() => {
|
||||
if (!isPromptFocused()) return;
|
||||
if (history.length === 0) return;
|
||||
if (browsingIndexRef.current === null) {
|
||||
startBrowsing();
|
||||
// Move to older entry on first activation
|
||||
if (history.length > 1) {
|
||||
applyHistoryAtIndex(1, 'start');
|
||||
} else {
|
||||
applyHistoryAtIndex(0, 'start');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Already browsing, go older if possible
|
||||
const next = Math.min((browsingIndexRef.current ?? 0) + 1, history.length - 1);
|
||||
applyHistoryAtIndex(next, 'start');
|
||||
}, [applyHistoryAtIndex, history.length, isPromptFocused, startBrowsing]);
|
||||
|
||||
const browseNext = useCallback(() => {
|
||||
if (!isPromptFocused()) return;
|
||||
if (history.length === 0) return;
|
||||
if (browsingIndexRef.current === null) {
|
||||
// Not browsing; Down does nothing (matches shell semantics)
|
||||
return;
|
||||
}
|
||||
if ((browsingIndexRef.current ?? 0) > 0) {
|
||||
const next = (browsingIndexRef.current ?? 0) - 1;
|
||||
applyHistoryAtIndex(next, 'end');
|
||||
} else {
|
||||
// Exit browsing and restore pre-browse prompt
|
||||
browsingIndexRef.current = null;
|
||||
dispatch(positivePromptChanged(preBrowsePromptRef.current));
|
||||
requestAnimationFrame(() => {
|
||||
const el = textareaRef.current;
|
||||
if (el) {
|
||||
const end = el.value.length;
|
||||
el.selectionStart = end;
|
||||
el.selectionEnd = end;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [applyHistoryAtIndex, dispatch, history.length, isPromptFocused]);
|
||||
|
||||
// Register hotkeys for browsing
|
||||
useRegisteredHotkeys({
|
||||
id: 'promptHistoryPrev',
|
||||
category: 'app',
|
||||
callback: (e) => {
|
||||
if (isPromptFocused()) {
|
||||
e.preventDefault();
|
||||
browsePrev();
|
||||
}
|
||||
},
|
||||
options: { preventDefault: true, enableOnFormTags: ['INPUT', 'SELECT', 'TEXTAREA'] },
|
||||
dependencies: [browsePrev, isPromptFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'promptHistoryNext',
|
||||
category: 'app',
|
||||
callback: (e) => {
|
||||
if (isPromptFocused()) {
|
||||
e.preventDefault();
|
||||
browseNext();
|
||||
}
|
||||
},
|
||||
options: { preventDefault: true, enableOnFormTags: ['INPUT', 'SELECT', 'TEXTAREA'] },
|
||||
dependencies: [browseNext, isPromptFocused],
|
||||
});
|
||||
|
||||
const dndTargetData = useMemo(() => promptGenerationFromImageDndTarget.getData(), []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -96,25 +96,32 @@ const PromptHistoryContent = memo(() => {
|
||||
</Button>
|
||||
</Flex>
|
||||
<Divider />
|
||||
{positivePromptHistory.length === 0 && (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text color="base.300">No prompt history recorded.</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{positivePromptHistory.length !== 0 && filteredPrompts.length === 0 && (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text color="base.300">No matching prompts in history.</Text>{' '}
|
||||
</Flex>
|
||||
)}
|
||||
{filteredPrompts.length > 0 && (
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column">
|
||||
{filteredPrompts.map((prompt, index) => (
|
||||
<PromptItem key={`${prompt}-${index}`} prompt={prompt} />
|
||||
))}
|
||||
<Flex flexDir="column" flexGrow={1} minH={0}>
|
||||
{positivePromptHistory.length === 0 && (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text color="base.300">No prompt history recorded.</Text>
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
)}
|
||||
)}
|
||||
{positivePromptHistory.length !== 0 && filteredPrompts.length === 0 && (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text color="base.300">No matching prompts in history.</Text>{' '}
|
||||
</Flex>
|
||||
)}
|
||||
{filteredPrompts.length > 0 && (
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column">
|
||||
{filteredPrompts.map((prompt, index) => (
|
||||
<PromptItem key={`${prompt}-${index}`} prompt={prompt} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="center" pt={1}>
|
||||
<Text fontSize="xs" color="base.400" textAlign="center">
|
||||
<Text as="span" fontWeight="semibold">Alt + Up/Down</Text> to switch between prompts.
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -81,6 +81,10 @@ export const useHotkeyData = (): HotkeysData => {
|
||||
addHotkey('app', 'selectGenerateTab', ['1']);
|
||||
addHotkey('app', 'selectCanvasTab', ['2']);
|
||||
addHotkey('app', 'selectUpscalingTab', ['3']);
|
||||
// Prompt/history navigation (when prompt textarea is focused)
|
||||
addHotkey('app', 'promptHistoryPrev', ['alt+up']);
|
||||
addHotkey('app', 'promptHistoryNext', ['alt+down']);
|
||||
|
||||
if (isVideoEnabled) {
|
||||
addHotkey('app', 'selectVideoTab', ['4']);
|
||||
addHotkey('app', 'selectWorkflowsTab', ['5']);
|
||||
|
||||
Reference in New Issue
Block a user