From e09103ddd037da2bdb88c8fa16f04705983d0eed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Dec 2025 01:53:55 +0000 Subject: [PATCH 1/3] feat(nodes): add Prompt Template node Add a new node that applies Style Preset templates to prompts in workflows. The node takes a style preset ID and positive/negative prompts as inputs, then replaces {prompt} placeholders in the template with the provided prompts. This makes Style Preset templates accessible in Workflow mode, enabling users to apply consistent styling across their workflow-based generations. --- invokeai/app/invocations/prompt_template.py | 57 +++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 invokeai/app/invocations/prompt_template.py diff --git a/invokeai/app/invocations/prompt_template.py b/invokeai/app/invocations/prompt_template.py new file mode 100644 index 00000000000..f8503bc5f21 --- /dev/null +++ b/invokeai/app/invocations/prompt_template.py @@ -0,0 +1,57 @@ +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output +from invokeai.app.invocations.fields import InputField, OutputField, UIComponent +from invokeai.app.services.shared.invocation_context import InvocationContext + + +@invocation_output("prompt_template_output") +class PromptTemplateOutput(BaseInvocationOutput): + """Output for the Prompt Template node""" + + positive_prompt: str = OutputField(description="The positive prompt with the template applied") + negative_prompt: str = OutputField(description="The negative prompt with the template applied") + + +@invocation( + "prompt_template", + title="Prompt Template", + tags=["prompt", "template", "style", "preset"], + category="prompt", + version="1.0.0", +) +class PromptTemplateInvocation(BaseInvocation): + """Applies a Style Preset template to positive and negative prompts. + + Select a Style Preset and provide positive/negative prompts. The node replaces + {prompt} placeholders in the template with your input prompts. + """ + + style_preset_id: str = InputField( + description="The ID of the Style Preset to use as a template", + ) + positive_prompt: str = InputField( + default="", + description="The positive prompt to insert into the template's {prompt} placeholder", + ui_component=UIComponent.Textarea, + ) + negative_prompt: str = InputField( + default="", + description="The negative prompt to insert into the template's {prompt} placeholder", + ui_component=UIComponent.Textarea, + ) + + def invoke(self, context: InvocationContext) -> PromptTemplateOutput: + # Fetch the style preset from the database + style_preset = context._services.style_preset_records.get(self.style_preset_id) + + # Get the template prompts + positive_template = style_preset.preset_data.positive_prompt + negative_template = style_preset.preset_data.negative_prompt + + # Replace {prompt} placeholder with the input prompts + rendered_positive = positive_template.replace("{prompt}", self.positive_prompt) + rendered_negative = negative_template.replace("{prompt}", self.negative_prompt) + + return PromptTemplateOutput( + positive_prompt=rendered_positive, + negative_prompt=rendered_negative, + ) From 9bb97e3438120bcd663692943173864727b8ab64 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 16 Dec 2025 08:52:31 +0100 Subject: [PATCH 2/3] feat(nodes): add StylePresetField for database-driven preset selection Adds a new StylePresetField type that enables dropdown selection of style presets from the database in the workflow editor. Changes: - Add StylePresetField to backend (fields.py) - Update Prompt Template node to use StylePresetField instead of string ID - Add frontend field type definitions (zod schemas, type guards) - Create StylePresetFieldInputComponent with Combobox - Register field in InputFieldRenderer and nodesSlice - Add translations for preset selection --- invokeai/app/invocations/fields.py | 6 ++ invokeai/app/invocations/prompt_template.py | 8 +- invokeai/frontend/web/public/locales/en.json | 4 +- .../Invocation/fields/InputFieldRenderer.tsx | 10 ++ .../inputs/StylePresetFieldInputComponent.tsx | 73 +++++++++++++ .../src/features/nodes/store/nodesSlice.ts | 6 ++ .../web/src/features/nodes/types/common.ts | 4 + .../web/src/features/nodes/types/constants.ts | 1 + .../web/src/features/nodes/types/field.ts | 32 ++++++ .../util/schema/buildFieldInputInstance.ts | 1 + .../util/schema/buildFieldInputTemplate.ts | 16 +++ .../frontend/web/src/services/api/schema.ts | 101 ++++++++++++++++-- 12 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 1bca7ec3f53..8f74bc964b0 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -241,6 +241,12 @@ class BoardField(BaseModel): board_id: str = Field(description="The id of the board") +class StylePresetField(BaseModel): + """A style preset primitive field""" + + style_preset_id: str = Field(description="The id of the style preset") + + class DenoiseMaskField(BaseModel): """An inpaint mask field""" diff --git a/invokeai/app/invocations/prompt_template.py b/invokeai/app/invocations/prompt_template.py index f8503bc5f21..d2ac86358e5 100644 --- a/invokeai/app/invocations/prompt_template.py +++ b/invokeai/app/invocations/prompt_template.py @@ -1,5 +1,5 @@ from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output -from invokeai.app.invocations.fields import InputField, OutputField, UIComponent +from invokeai.app.invocations.fields import InputField, OutputField, StylePresetField, UIComponent from invokeai.app.services.shared.invocation_context import InvocationContext @@ -25,8 +25,8 @@ class PromptTemplateInvocation(BaseInvocation): {prompt} placeholders in the template with your input prompts. """ - style_preset_id: str = InputField( - description="The ID of the Style Preset to use as a template", + style_preset: StylePresetField = InputField( + description="The Style Preset to use as a template", ) positive_prompt: str = InputField( default="", @@ -41,7 +41,7 @@ class PromptTemplateInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> PromptTemplateOutput: # Fetch the style preset from the database - style_preset = context._services.style_preset_records.get(self.style_preset_id) + style_preset = context._services.style_preset_records.get(self.style_preset.style_preset_id) # Get the template prompts positive_template = style_preset.preset_data.positive_prompt diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index be5ecd9a9cf..0c61015fa70 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2643,7 +2643,9 @@ "useForTemplate": "Use For Prompt Template", "viewList": "View Template List", "viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box.", - "togglePromptPreviews": "Toggle Prompt Previews" + "togglePromptPreviews": "Toggle Prompt Previews", + "selectPreset": "Select Style Preset", + "noMatchingPresets": "No matching presets" }, "ui": { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 7139d0e1f98..60a3f8e472a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -55,6 +55,8 @@ import { isStringFieldInputTemplate, isStringGeneratorFieldInputInstance, isStringGeneratorFieldInputTemplate, + isStylePresetFieldInputInstance, + isStylePresetFieldInputTemplate, } from 'features/nodes/types/field'; import type { NodeFieldElement } from 'features/nodes/types/workflow'; import { memo } from 'react'; @@ -67,6 +69,7 @@ import ColorFieldInputComponent from './inputs/ColorFieldInputComponent'; import EnumFieldInputComponent from './inputs/EnumFieldInputComponent'; import ImageFieldInputComponent from './inputs/ImageFieldInputComponent'; import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent'; +import StylePresetFieldInputComponent from './inputs/StylePresetFieldInputComponent'; type Props = { nodeId: string; @@ -206,6 +209,13 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) return ; } + if (isStylePresetFieldInputTemplate(template)) { + if (!isStylePresetFieldInputInstance(field)) { + return null; + } + return ; + } + if (isModelIdentifierFieldInputTemplate(template)) { if (!isModelIdentifierFieldInputInstance(field)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx new file mode 100644 index 00000000000..7791ed3a3c9 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StylePresetFieldInputComponent.tsx @@ -0,0 +1,73 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { fieldStylePresetValueChanged } from 'features/nodes/store/nodesSlice'; +import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants'; +import type { StylePresetFieldInputInstance, StylePresetFieldInputTemplate } from 'features/nodes/types/field'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; + +import type { FieldComponentProps } from './types'; + +const StylePresetFieldInputComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const { data: stylePresets, isLoading } = useListStylePresetsQuery(); + + const options = useMemo(() => { + const _options: ComboboxOption[] = []; + if (stylePresets) { + for (const preset of stylePresets) { + _options.push({ + label: preset.name, + value: preset.id, + }); + } + } + return _options; + }, [stylePresets]); + + const onChange = useCallback( + (v) => { + if (!v) { + return; + } + + dispatch( + fieldStylePresetValueChanged({ + nodeId, + fieldName: field.name, + value: { style_preset_id: v.value }, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + const value = useMemo(() => { + const _value = field.value; + if (!_value) { + return null; + } + return options.find((o) => o.value === _value.style_preset_id) ?? null; + }, [field.value, options]); + + const noOptionsMessage = useCallback(() => t('stylePresets.noMatchingPresets'), [t]); + + return ( + + ); +}; + +export default memo(StylePresetFieldInputComponent); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 98b41da3059..bdab6c1ae36 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -41,6 +41,7 @@ import type { StringFieldCollectionValue, StringFieldValue, StringGeneratorFieldValue, + StylePresetFieldValue, } from 'features/nodes/types/field'; import { zBoardFieldValue, @@ -62,6 +63,7 @@ import { zStringFieldCollectionValue, zStringFieldValue, zStringGeneratorFieldValue, + zStylePresetFieldValue, } from 'features/nodes/types/field'; import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; @@ -438,6 +440,9 @@ const slice = createSlice({ fieldBoardValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zBoardFieldValue); }, + fieldStylePresetValueChanged: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStylePresetFieldValue); + }, fieldImageValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zImageFieldValue); }, @@ -588,6 +593,7 @@ export const { fieldBoardValueChanged, fieldBooleanValueChanged, fieldColorValueChanged, + fieldStylePresetValueChanged, fieldEnumModelValueChanged, fieldImageValueChanged, fieldImageCollectionValueChanged, diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index 97c7fff795d..89e7cd8997c 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -16,6 +16,10 @@ export const zBoardField = z.object({ }); export type BoardField = z.infer; +export const zStylePresetField = z.object({ + style_preset_id: z.string().trim().min(1), +}); + export const zColorField = z.object({ r: z.number().int().min(0).max(255), g: z.number().int().min(0).max(255), diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index a8ab6d231e3..808bf6aecdf 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -35,6 +35,7 @@ export const NO_PAN_CLASS = 'nopan'; export const FIELD_COLORS: { [key: string]: string } = { BoardField: 'purple.500', BooleanField: 'green.500', + StylePresetField: 'purple.400', CLIPField: 'green.500', ColorField: 'pink.300', ConditioningField: 'cyan.500', diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 356b7656609..98b20912ab2 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -19,6 +19,7 @@ import { zModelIdentifierField, zModelType, zSchedulerField, + zStylePresetField, } from './common'; /** @@ -169,6 +170,11 @@ const zBoardFieldType = zFieldTypeBase.extend({ originalType: zStatelessFieldType.optional(), }); +const zStylePresetFieldType = zFieldTypeBase.extend({ + name: z.literal('StylePresetField'), + originalType: zStatelessFieldType.optional(), +}); + const zColorFieldType = zFieldTypeBase.extend({ name: z.literal('ColorField'), originalType: zStatelessFieldType.optional(), @@ -205,6 +211,7 @@ const zStatefulFieldType = z.union([ zEnumFieldType, zImageFieldType, zBoardFieldType, + zStylePresetFieldType, zModelIdentifierFieldType, zColorFieldType, zSchedulerFieldType, @@ -607,6 +614,27 @@ export const isBoardFieldInputInstance = buildInstanceTypeGuard(zBoardFieldInput export const isBoardFieldInputTemplate = buildTemplateTypeGuard('BoardField'); // #endregion +// #region StylePresetField +export const zStylePresetFieldValue = zStylePresetField.optional(); +const zStylePresetFieldInputInstance = zFieldInputInstanceBase.extend({ + value: zStylePresetFieldValue, +}); +const zStylePresetFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zStylePresetFieldType, + originalType: zFieldType.optional(), + default: zStylePresetFieldValue, +}); +const zStylePresetFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zStylePresetFieldType, +}); +export type StylePresetFieldValue = z.infer; +export type StylePresetFieldInputInstance = z.infer; +export type StylePresetFieldInputTemplate = z.infer; +export const isStylePresetFieldInputInstance = buildInstanceTypeGuard(zStylePresetFieldInputInstance); +export const isStylePresetFieldInputTemplate = + buildTemplateTypeGuard('StylePresetField'); +// #endregion + // #region ColorField export const zColorFieldValue = zColorField.optional(); const zColorFieldInputInstance = zFieldInputInstanceBase.extend({ @@ -1257,6 +1285,7 @@ export const zStatefulFieldValue = z.union([ zImageFieldValue, zImageFieldCollectionValue, zBoardFieldValue, + zStylePresetFieldValue, zModelIdentifierFieldValue, zColorFieldValue, zSchedulerFieldValue, @@ -1284,6 +1313,7 @@ const zStatefulFieldInputInstance = z.union([ zImageFieldInputInstance, zImageFieldCollectionInputInstance, zBoardFieldInputInstance, + zStylePresetFieldInputInstance, zModelIdentifierFieldInputInstance, zColorFieldInputInstance, zSchedulerFieldInputInstance, @@ -1310,6 +1340,7 @@ const zStatefulFieldInputTemplate = z.union([ zImageFieldInputTemplate, zImageFieldCollectionInputTemplate, zBoardFieldInputTemplate, + zStylePresetFieldInputTemplate, zModelIdentifierFieldInputTemplate, zColorFieldInputTemplate, zSchedulerFieldInputTemplate, @@ -1337,6 +1368,7 @@ const zStatefulFieldOutputTemplate = z.union([ zImageFieldOutputTemplate, zImageFieldCollectionOutputTemplate, zBoardFieldOutputTemplate, + zStylePresetFieldOutputTemplate, zModelIdentifierFieldOutputTemplate, zColorFieldOutputTemplate, zSchedulerFieldOutputTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts index 1c14ab1f4d0..ef7b92efdd8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts @@ -12,6 +12,7 @@ const FIELD_VALUE_FALLBACK_MAP: Record = ModelIdentifierField: undefined, SchedulerField: 'dpmpp_3m_k', StringField: '', + StylePresetField: undefined, FloatGeneratorField: undefined, IntegerGeneratorField: undefined, StringGeneratorField: undefined, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts index 342dead58ca..27a0b21a7c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts @@ -23,6 +23,7 @@ import type { StringFieldCollectionInputTemplate, StringFieldInputTemplate, StringGeneratorFieldInputTemplate, + StylePresetFieldInputTemplate, } from 'features/nodes/types/field'; import { getFloatGeneratorArithmeticSequenceDefaults, @@ -289,6 +290,20 @@ const buildBoardFieldInputTemplate: FieldInputTemplateBuilder = ({ + schemaObject, + baseField, + fieldType, +}) => { + const template: StylePresetFieldInputTemplate = { + ...baseField, + type: fieldType, + default: schemaObject.default ?? undefined, + }; + + return template; +}; + const buildImageFieldInputTemplate: FieldInputTemplateBuilder = ({ schemaObject, baseField, @@ -460,6 +475,7 @@ const TEMPLATE_BUILDER_MAP: Record Date: Tue, 16 Dec 2025 09:04:45 +0100 Subject: [PATCH 3/3] fix schema.ts on windows. --- invokeai/frontend/web/src/services/api/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 4226c412505..b9c15d3e62a 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -13010,14 +13010,14 @@ export type components = { * Convert Cache Dir * Format: path * @description Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions). - * @default models\.convert_cache + * @default models/.convert_cache */ convert_cache_dir?: string; /** * Download Cache Dir * Format: path * @description Path to the directory that contains dynamically downloaded models. - * @default models\.download_cache + * @default models/.download_cache */ download_cache_dir?: string; /**