a whole bunch of stuff

This commit is contained in:
Mary Hipp 2024-08-06 15:31:13 -04:00
parent 857d74bbfe
commit 2604fd9fde
36 changed files with 686 additions and 472 deletions

View File

@ -1,19 +1,13 @@
from typing import Optional
from fastapi import APIRouter, Body, HTTPException, Path, Query
from fastapi import APIRouter, Body, HTTPException, Path
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.style_preset_records.style_preset_records_common import (
StylePresetChanges,
StylePresetNotFoundError,
StylePresetRecordDTO,
StylePresetWithoutId,
StylePresetRecordOrderBy,
)
style_presets_router = APIRouter(prefix="/v1/style_presets", tags=["style_presets"])
@ -78,23 +72,9 @@ async def create_style_preset(
"/",
operation_id="list_style_presets",
responses={
200: {"model": PaginatedResults[StylePresetRecordDTO]},
200: {"model": list[StylePresetRecordDTO]},
},
)
async def list_style_presets(
page: int = Query(default=0, description="The page to get"),
per_page: int = Query(default=10, description="The number of style presets per page"),
order_by: StylePresetRecordOrderBy = Query(
default=StylePresetRecordOrderBy.Name, description="The attribute to order by"
),
direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"),
query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
) -> PaginatedResults[StylePresetRecordDTO]:
async def list_style_presets() -> list[StylePresetRecordDTO]:
"""Gets a page of style presets"""
return ApiDependencies.invoker.services.style_preset_records.get_many(
page=page,
per_page=per_page,
order_by=order_by,
direction=direction,
query=query,
)
return ApiDependencies.invoker.services.style_preset_records.get_many()

View File

@ -1,13 +1,9 @@
from abc import ABC, abstractmethod
from typing import Optional
from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.style_preset_records.style_preset_records_common import (
StylePresetChanges,
StylePresetRecordDTO,
StylePresetWithoutId,
StylePresetRecordOrderBy,
)
@ -35,13 +31,6 @@ class StylePresetRecordsStorageBase(ABC):
pass
@abstractmethod
def get_many(
self,
page: int,
per_page: int,
order_by: StylePresetRecordOrderBy,
direction: SQLiteDirection,
query: Optional[str],
) -> PaginatedResults[StylePresetRecordDTO]:
def get_many(self) -> list[StylePresetRecordDTO]:
"""Gets many workflows."""
pass

View File

@ -10,13 +10,6 @@ class StylePresetNotFoundError(Exception):
"""Raised when a style preset is not found"""
class StylePresetRecordOrderBy(str, Enum, metaclass=MetaEnum):
"""The order by options for workflow records"""
CreatedAt = "created_at"
Name = "name"
class PresetData(BaseModel, extra="forbid"):
positive_prompt: str = Field(description="Positive prompt")
negative_prompt: str = Field(description="Negative prompt")

View File

@ -11,7 +11,6 @@ from invokeai.app.services.style_preset_records.style_preset_records_common impo
StylePresetNotFoundError,
StylePresetRecordDTO,
StylePresetWithoutId,
StylePresetRecordOrderBy,
)
from invokeai.app.util.misc import uuid_string
@ -128,50 +127,21 @@ class SqliteStylePresetRecordsStorage(StylePresetRecordsStorageBase):
def get_many(
self,
page: int,
per_page: int,
order_by: StylePresetRecordOrderBy,
direction: SQLiteDirection,
query: Optional[str] = None,
) -> PaginatedResults[StylePresetRecordDTO]:
) -> list[StylePresetRecordDTO]:
try:
self._lock.acquire()
# sanitize!
assert order_by in StylePresetRecordOrderBy
assert direction in SQLiteDirection
count_query = "SELECT COUNT(*) FROM style_presets"
main_query = """
SELECT
*
FROM style_presets
ORDER BY name ASC
"""
main_params: list[int | str] = []
count_params: list[int | str] = []
stripped_query = query.strip() if query else None
if stripped_query:
wildcard_query = "%" + stripped_query + "%"
main_query += " AND name LIKE ? "
count_query += " AND name LIKE ?;"
main_params.extend([wildcard_query, wildcard_query])
count_params.extend([wildcard_query, wildcard_query])
main_query += f" ORDER BY {order_by.value} {direction.value} LIMIT ? OFFSET ?;"
main_params.extend([per_page, page * per_page])
self._cursor.execute(main_query, main_params)
self._cursor.execute(main_query)
rows = self._cursor.fetchall()
style_presets = [StylePresetRecordDTO.from_dict(dict(row)) for row in rows]
self._cursor.execute(count_query, count_params)
total = self._cursor.fetchone()[0]
pages = total // per_page + (total % per_page > 0)
return PaginatedResults(
items=style_presets,
page=page,
per_page=per_page,
pages=pages,
total=total,
)
return style_presets
except Exception:
self._conn.rollback()
raise

View File

@ -53,7 +53,6 @@ import type { AppDispatch, RootState } from 'app/store/store';
import { addArchivedOrDeletedBoardListener } from './listeners/addArchivedOrDeletedBoardListener';
import { addEnqueueRequestedUpscale } from './listeners/enqueueRequestedUpscale';
import { addActiveStylePresetChanged } from './listeners/activeStylePresetChanged';
export const listenerMiddleware = createListenerMiddleware();
@ -147,7 +146,6 @@ addAdHocPostProcessingRequestedListener(startAppListening);
// Prompts
addDynamicPromptsListener(startAppListening);
addActiveStylePresetChanged(startAppListening)
addSetDefaultSettingsListener(startAppListening);
addControlAdapterPreprocessor(startAppListening);

View File

@ -1,31 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { negativePromptChanged, positivePromptChanged, } from 'features/controlLayers/store/controlLayersSlice';
import { activeStylePresetChanged, calculatedNegPromptChanged, calculatedPosPromptChanged } from '../../../../../features/stylePresets/store/stylePresetSlice';
import { isAnyOf } from '@reduxjs/toolkit';
export const addActiveStylePresetChanged = (startAppListening: AppStartListening) => {
startAppListening({
matcher: isAnyOf(activeStylePresetChanged, positivePromptChanged, negativePromptChanged),
effect: async (action, { dispatch, getState }) => {
const state = getState();
const activeStylePreset = state.stylePreset.activeStylePreset;
const positivePrompt = state.controlLayers.present.positivePrompt
const negativePrompt = state.controlLayers.present.negativePrompt
if (!activeStylePreset) {
return;
}
const { positive_prompt: presetPositivePrompt, negative_prompt: presetNegativePrompt } = activeStylePreset.preset_data;
const calculatedPosPrompt = presetPositivePrompt.includes('{prompt}') ? presetPositivePrompt.replace('{prompt}', positivePrompt) : `${positivePrompt} ${presetPositivePrompt}`
const calculatedNegPrompt = presetNegativePrompt.includes('{prompt}') ? presetNegativePrompt.replace('{prompt}', negativePrompt) : `${negativePrompt} ${presetNegativePrompt}`
dispatch(calculatedPosPromptChanged(calculatedPosPrompt))
dispatch(calculatedNegPromptChanged(calculatedNegPrompt))
},
});
};

View File

@ -29,6 +29,7 @@ import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/up
import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetModalSlice } from 'features/stylePresets/store/stylePresetModalSlice';
import { stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { configSlice } from 'features/system/store/configSlice';
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
@ -47,7 +48,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer';
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware';
import { stylePresetSlice } from '../../features/stylePresets/store/stylePresetSlice';
const allReducers = {
[canvasSlice.name]: canvasSlice.reducer,

View File

@ -22,11 +22,10 @@ import {
} from './constants';
import { addLoRAs } from './generation/addLoRAs';
import { addSDXLLoRas } from './generation/addSDXLLoRAs';
import { getBoardField, getSDXLStylePrompts } from './graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils';
export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise<GraphType> => {
const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale;
assert(model, 'No model found in state');
@ -99,7 +98,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
let modelNode;
if (model.base === 'sdxl') {
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
posCondNode = g.addNode({
type: 'sdxl_compel_prompt',
@ -140,6 +139,8 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise
vae: vae ?? undefined,
});
} else {
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
posCondNode = g.addNode({
type: 'compel',
id: POSITIVE_CONDITIONING,

View File

@ -16,7 +16,7 @@ import {
SDXL_REFINER_POSITIVE_CONDITIONING,
SDXL_REFINER_SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { NonNullableGraph } from 'services/api/types';
import { isRefinerMainModelModelConfig } from 'services/api/types';
@ -59,7 +59,7 @@ export const addSDXLRefinerToGraph = async (
const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId : SDXL_MODEL_LOADER;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
// Unplug SDXL Latents Generation To Latents To Image
graph.edges = graph.edges.filter((e) => !(e.source.node_id === baseNodeId && ['latents'].includes(e.source.field)));

View File

@ -16,7 +16,7 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
@ -51,7 +51,6 @@ export const buildCanvasImageToImageGraph = async (
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -71,6 +70,8 @@ export const buildCanvasImageToImageGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node

View File

@ -19,7 +19,7 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -58,7 +58,6 @@ export const buildCanvasInpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
if (!model) {
log.error('No model found in state');
@ -79,6 +78,8 @@ export const buildCanvasInpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: CANVAS_INPAINT_GRAPH,
nodes: {

View File

@ -23,7 +23,7 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -70,7 +70,6 @@ export const buildCanvasOutpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
if (!model) {
log.error('No model found in state');
@ -91,6 +90,8 @@ export const buildCanvasOutpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: CANVAS_OUTPAINT_GRAPH,
nodes: {

View File

@ -16,7 +16,7 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
@ -51,7 +51,6 @@ export const buildCanvasSDXLImageToImageGraph = async (
seamlessYAxis,
img2imgStrength: strength,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -75,7 +74,7 @@ export const buildCanvasSDXLImageToImageGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the

View File

@ -19,7 +19,7 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -58,7 +58,6 @@ export const buildCanvasSDXLInpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -83,7 +82,7 @@ export const buildCanvasSDXLInpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: SDXL_CANVAS_INPAINT_GRAPH,

View File

@ -23,7 +23,7 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -70,7 +70,6 @@ export const buildCanvasSDXLOutpaintGraph = async (
canvasCoherenceEdgeSize,
maskBlur,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { refinerModel, refinerStart } = state.sdxl;
@ -94,7 +93,7 @@ export const buildCanvasSDXLOutpaintGraph = async (
const use_cpu = shouldUseCpuNoise;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const graph: NonNullableGraph = {
id: SDXL_CANVAS_OUTPAINT_GRAPH,

View File

@ -14,7 +14,7 @@ import {
SDXL_REFINER_SEAMLESS,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -44,7 +44,6 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -67,7 +66,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
let modelLoaderNodeId = SDXL_MODEL_LOADER;
// Construct Style Prompt
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the

View File

@ -14,7 +14,7 @@ import {
POSITIVE_CONDITIONING,
SEAMLESS,
} from 'features/nodes/util/graph/constants';
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getIsIntermediate, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
@ -44,7 +44,6 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
seamlessXAxis,
seamlessYAxis,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
@ -64,6 +63,8 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
let modelLoaderNodeId = MAIN_MODEL_LOADER;
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node

View File

@ -22,7 +22,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
import type { GraphType } from 'features/nodes/util/graph/generation/Graph';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { Invocation } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@ -40,11 +40,12 @@ export const buildGenerationTabGraph = async (state: RootState): Promise<GraphTy
seed,
vae,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { width, height } = state.controlLayers.present.size;
assert(model, 'No model found in state');
const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state);
const g = new Graph(CONTROL_LAYERS_GRAPH);
const modelLoader = g.addNode({
type: 'main_model_loader',

View File

@ -19,7 +19,7 @@ import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefi
import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless';
import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
import { Graph } from 'features/nodes/util/graph/generation/Graph';
import { getBoardField, getSDXLStylePrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
import type { Invocation, NonNullableGraph } from 'services/api/types';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
@ -36,14 +36,13 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
vaePrecision,
vae,
} = state.generation;
const { positivePrompt, negativePrompt } = state.controlLayers.present;
const { width, height } = state.controlLayers.present.size;
const { refinerModel, refinerStart } = state.sdxl;
assert(model, 'No model found in state');
const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state);
const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state);
const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH);
const modelLoader = g.addNode({

View File

@ -1,5 +1,6 @@
import type { RootState } from 'app/store/store';
import type { BoardField } from 'features/nodes/types/common';
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
/**
@ -14,13 +15,30 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
};
/**
* Gets the SDXL style prompts, based on the concat setting.
* Gets the prompts, modified for the active style preset.
*/
export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => {
export const getPresetModifiedPrompts = (state: RootState): { positivePrompt: string; negativePrompt: string, positiveStylePrompt?: string; negativeStylePrompt?: string } => {
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
state.controlLayers.present;
const { activeStylePreset } = state.stylePreset
if (activeStylePreset) {
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(activeStylePreset.preset_data.positive_prompt, positivePrompt)
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(activeStylePreset.preset_data.negative_prompt, negativePrompt)
return {
positivePrompt: presetModifiedPositivePrompt,
negativePrompt: presetModifiedNegativePrompt,
positiveStylePrompt: shouldConcatPrompts ? presetModifiedPositivePrompt : positivePrompt2,
negativeStylePrompt: shouldConcatPrompts ? presetModifiedNegativePrompt : negativePrompt2,
};
}
return {
positivePrompt,
negativePrompt,
positiveStylePrompt: shouldConcatPrompts ? positivePrompt : positivePrompt2,
negativeStylePrompt: shouldConcatPrompts ? negativePrompt : negativePrompt2,
};

View File

@ -7,7 +7,7 @@ import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPo
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { memo } from 'react';
const concatPromptsSelector = createSelector(
@ -19,16 +19,14 @@ const concatPromptsSelector = createSelector(
export const Prompts = memo(() => {
const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
const calculatedPosPrompt = useAppSelector((s) => s.stylePreset.calculatedPosPrompt);
const calculatedNegPrompt = useAppSelector((s) => s.stylePreset.calculatedNegPrompt);
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
return (
<Flex flexDir="column" gap={2}>
<StylePresetMenuTrigger />
<ParamPositivePrompt />
<Flex>{calculatedPosPrompt}</Flex>
<Flex>{presetModifiedPositivePrompt}</Flex>
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
<ParamNegativePrompt />
<Flex>{calculatedNegPrompt}</Flex>
<Flex>{presetModifiedNegativePrompt}</Flex>
{!shouldConcatPrompts && <ParamSDXLNegativeStylePrompt />}
</Flex>
);

View File

@ -0,0 +1,72 @@
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { activeStylePresetChanged } from 'features/stylePresets/store/stylePresetSlice';
import type { MouseEventHandler} from 'react';
import { useCallback } from 'react';
import { CgPushDown } from 'react-icons/cg';
import { PiXBold } from 'react-icons/pi';
export const ActiveStylePreset = () => {
const { activeStylePreset } = useAppSelector((s) => s.stylePreset);
const dispatch = useAppDispatch();
const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts();
const handleClearActiveStylePreset = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(activeStylePresetChanged(null));
},
[dispatch]
);
const handleFlattenPrompts = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
dispatch(positivePromptChanged(presetModifiedPositivePrompt));
dispatch(negativePromptChanged(presetModifiedNegativePrompt));
dispatch(activeStylePresetChanged(null));
},
[dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt]
);
if (!activeStylePreset) {
return <>Choose Preset</>;
}
return (
<>
<Flex justifyContent="space-between" w="full" alignItems="center">
<Flex gap="2">
<ModelImage image_url={null} />
<Flex flexDir="column">
<Text variant="subtext" fontSize="xs">
Prompt Style
</Text>
<Text fontSize="md" fontWeight="semibold">
{activeStylePreset.name}
</Text>
</Flex>
</Flex>
<Flex gap="1">
<IconButton
onClick={handleFlattenPrompts}
variant="ghost"
size="md"
aria-label="Flatten"
icon={<CgPushDown />}
/>
<IconButton
onClick={handleClearActiveStylePreset}
variant="ghost"
size="md"
aria-label="Clear"
icon={<PiXBold />}
/>
</Flex>
</Flex>
</>
);
};

View File

@ -1,56 +1,49 @@
import { Button, Flex, FormControl, FormLabel, Input, Textarea } from '@invoke-ai/ui-library';
import { Button, Flex, FormControl, FormLabel, Icon, Input, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useStylePresetFields } from 'features/stylePresets/hooks/useStylePresetFields';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { toast } from 'features/toast/toast';
import type { ChangeEventHandler } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import type { SubmitHandler} from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { PiBracketsCurlyBold } from 'react-icons/pi';
import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
import { useCreateStylePresetMutation, useUpdateStylePresetMutation } from 'services/api/endpoints/stylePresets';
import { StylePresetPromptField } from './StylePresetPromptField';
export type StylePresetFormData = {
name: string;
positivePrompt: string;
negativePrompt: string;
};
export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordDTO | null }) => {
const [createStylePreset] = useCreateStylePresetMutation();
const [updateStylePreset] = useUpdateStylePresetMutation();
const dispatch = useAppDispatch();
const [name, setName] = useState(updatingPreset ? updatingPreset.name : '');
const [posPrompt, setPosPrompt] = useState(updatingPreset ? updatingPreset.preset_data.positive_prompt : '');
const [negPrompt, setNegPrompt] = useState(updatingPreset ? updatingPreset.preset_data.negative_prompt : '');
const stylePresetFieldDefaults = useStylePresetFields(updatingPreset);
const handleChangeName = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
setName(e.target.value);
}, []);
const { handleSubmit, control, formState, reset, register } = useForm<StylePresetFormData>({
defaultValues: stylePresetFieldDefaults,
});
const handleChangePosPrompt = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => {
setPosPrompt(e.target.value);
}, []);
const handleChangeNegPrompt = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => {
setNegPrompt(e.target.value);
}, []);
useEffect(() => {
if (updatingPreset) {
setName(updatingPreset.name);
setPosPrompt(updatingPreset.preset_data.positive_prompt);
setNegPrompt(updatingPreset.preset_data.negative_prompt);
} else {
setName('');
setPosPrompt('');
setNegPrompt('');
}
}, [updatingPreset]);
const handleClickSave = useCallback(async () => {
const handleClickSave = useCallback<SubmitHandler<StylePresetFormData>>(
async (data) => {
try {
if (updatingPreset) {
await updateStylePreset({
id: updatingPreset.id,
changes: { name, preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt } },
changes: {
name: data.name,
preset_data: { positive_prompt: data.positivePrompt, negative_prompt: data.negativePrompt },
},
}).unwrap();
} else {
await createStylePreset({
name: name,
preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt },
name: data.name,
preset_data: { positive_prompt: data.positivePrompt, negative_prompt: data.negativePrompt },
}).unwrap();
}
} catch (error) {
@ -62,23 +55,26 @@ export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePrese
dispatch(updatingStylePresetChanged(null));
dispatch(isModalOpenChanged(false));
}, [dispatch, updatingPreset, name, posPrompt, negPrompt, updateStylePreset, createStylePreset]);
},
[dispatch, updatingPreset, updateStylePreset, createStylePreset]
);
return (
<Flex flexDir="column" gap="4">
<FormControl>
<FormLabel>Name</FormLabel>
<Input value={name} onChange={handleChangeName} />
<Input size="md" {...register('name')} />
</FormControl>
<FormControl>
<FormLabel>Positive Prompt</FormLabel>
<Textarea value={posPrompt} onChange={handleChangePosPrompt} />
</FormControl>
<FormControl>
<FormLabel>Negative Prompt</FormLabel>
<Textarea value={negPrompt} onChange={handleChangeNegPrompt} />
</FormControl>
<Button onClick={handleClickSave}>Save</Button>
<Flex flexDir="column" bgColor="base.750" borderRadius="base" padding="10px" gap="10px">
<Text variant="subtext">
Use the <Icon as={PiBracketsCurlyBold} /> button to specify where your manual prompt should be included in the
template. If you do not provide one, the template will be appended to your prompt.
</Text>
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
</Flex>
<Button onClick={handleSubmit(handleClickSave)}>Save</Button>
</Flex>
);
};

View File

@ -0,0 +1,31 @@
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { PiCaretDownBold } from 'react-icons/pi';
import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
import { StylePresetListItem } from './StylePresetListItem';
export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordDTO[] }) => {
const { onToggle, isOpen } = useDisclosure({ defaultIsOpen: true });
if (!data.length) {
return <></>;
}
return (
<Flex flexDir="column">
<Button variant="unstyled" onClick={onToggle}>
<Flex gap="2" alignItems="center">
<Icon boxSize={4} as={PiCaretDownBold} transform={isOpen ? undefined : 'rotate(-90deg)'} fill="base.500" />
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
{title}
</Text>
</Flex>
</Button>
<Collapse in={isOpen}>
{data.map((preset) => (
<StylePresetListItem preset={preset} key={preset.id} />
))}
</Collapse>
</Flex>
);
};

View File

@ -1,14 +1,17 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { Badge, Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { activeStylePresetChanged, isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
import { useCallback } from 'react';
import { PiPencilBold, PiTrashBold } from 'react-icons/pi';
import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
import { activeStylePresetChanged, isMenuOpenChanged } from '../store/stylePresetSlice';
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }) => {
const dispatch = useAppDispatch();
const [deleteStylePreset] = useDeleteStylePresetMutation();
const activeStylePreset = useAppSelector((s) => s.stylePreset.activeStylePreset);
const handleClickEdit = useCallback(() => {
dispatch(updatingStylePresetChanged(preset));
@ -27,10 +30,47 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }
}, [preset]);
return (
<>
<Flex flexDir="column" gap="2">
<Flex
gap="4"
onClick={handleClickApply}
cursor="pointer"
_hover={{ backgroundColor: 'base.750' }}
padding="10px"
borderRadius="base"
alignItems="center"
w="full"
>
<ModelImage image_url={null} />
<Flex flexDir="column" w="full">
<Flex w="full" justifyContent="space-between">
<Flex alignItems="center" gap="2">
<Text fontSize="md">{preset.name}</Text>
<Flex flexDir="column" layerStyle="third" borderRadius="base" padding="10px">
{activeStylePreset && activeStylePreset.id === preset.id && (
<Badge
color="invokeBlue.400"
borderColor="invokeBlue.700"
borderWidth={1}
bg="transparent"
flexShrink={0}
>
Active
</Badge>
)}
</Flex>
<Flex alignItems="center" gap="1">
<IconButton size="sm" variant="ghost" aria-label="Edit" onClick={handleClickEdit} icon={<PiPencilBold />} />
<IconButton
size="sm"
variant="ghost"
aria-label="Delete"
onClick={handleDeletePreset}
icon={<PiTrashBold />}
/>
</Flex>
</Flex>
<Flex flexDir="column">
<Text fontSize="xs">
<Text as="span" fontWeight="semibold">
Positive prompt:
@ -43,11 +83,8 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }
</Text>{' '}
{preset.preset_data.negative_prompt}
</Text>
<Button onClick={handleClickEdit}>Edit</Button>
<Button onClick={handleDeletePreset}>Delete</Button>
<Button onClick={handleClickApply}>Apply</Button>
</Flex>
</Flex>
</>
</Flex>
);
};

View File

@ -1,13 +1,42 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/stylePresetModalSlice';
import { useCallback } from 'react';
import { PiPlusBold } from 'react-icons/pi';
import type { StylePresetRecordDTO} from 'services/api/endpoints/stylePresets';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import { StylePresetListItem } from './StylePresetListItem';
import { StylePresetList } from './StylePresetList';
import StylePresetSearch from './StylePresetSearch';
export const StylePresetMenu = () => {
const { data } = useListStylePresetsQuery({});
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
const { data } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data, error, isLoading }) => {
const filteredData =
data?.filter((preset) => preset.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY;
const groupedData = filteredData.reduce(
(acc: { defaultPresets: StylePresetRecordDTO[]; presets: StylePresetRecordDTO[] }, preset) => {
if (preset.is_default) {
acc.defaultPresets.push(preset);
} else {
acc.presets.push(preset);
}
return acc;
},
{ defaultPresets: [], presets: [] }
);
return {
data: groupedData,
error,
isLoading,
};
},
});
const dispatch = useAppDispatch();
const handleClickAddNew = useCallback(() => {
@ -16,19 +45,30 @@ export const StylePresetMenu = () => {
}, [dispatch]);
return (
<>
<Flex flexDir="column" gap="2">
<Flex flexDir="column" gap="2" padding="10px">
<Flex alignItems="center" gap="10" w="full" justifyContent="space-between">
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
Style Presets
</Text>
<Button size="sm" onClick={handleClickAddNew}>
Add New
</Button>
<StylePresetSearch />
<IconButton
icon={<PiPlusBold />}
tooltip="Create Preset"
aria-label="Create Preset"
onClick={handleClickAddNew}
size="md"
variant="link"
w={8}
h={8}
/>
</Flex>
{data?.items.map((preset) => <StylePresetListItem preset={preset} key={preset.id} />)}
{data.presets.length === 0 && data.defaultPresets.length === 0 && (
<Text m="20px" textAlign="center">
No matching presets
</Text>
)}
<StylePresetList title="My Presets" data={data.presets} />
<StylePresetList title="Default Presets" data={data.defaultPresets} />
</Flex>
</>
);
};

View File

@ -1,34 +1,31 @@
import { Button, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { StylePresetMenu } from './StylePresetMenu';
import { useAppDispatch, useAppSelector } from '../../../app/store/storeHooks';
import { Flex, Icon } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isMenuOpenChanged } from 'features/stylePresets/store/stylePresetSlice';
import { useCallback } from 'react';
import { isMenuOpenChanged } from '../store/stylePresetSlice';
import { PiCaretDownBold } from 'react-icons/pi';
import { ActiveStylePreset } from './ActiveStylePreset';
export const StylePresetMenuTrigger = () => {
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
const dispatch = useAppDispatch();
const handleClose = useCallback(() => {
dispatch(isMenuOpenChanged(false));
}, [dispatch]);
const handleToggle = useCallback(() => {
dispatch(isMenuOpenChanged(!isMenuOpen));
}, [dispatch, isMenuOpen]);
return (
<Popover isOpen={isMenuOpen} onClose={handleClose}>
<PopoverTrigger>
<Button size="sm" onClick={handleToggle}>
Style Presets
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<StylePresetMenu />
</PopoverBody>
</PopoverContent>
</Popover>
<Flex
onClick={handleToggle}
backgroundColor="base.800"
justifyContent="space-between"
alignItems="center"
padding="5px 10px"
borderRadius="base"
>
<ActiveStylePreset />
<Icon as={PiCaretDownBold} />
</Flex>
);
};

View File

@ -0,0 +1,61 @@
import { Flex, FormControl, FormLabel, IconButton, Textarea } from '@invoke-ai/ui-library';
import type { ChangeEventHandler } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import type { UseControllerProps } from 'react-hook-form';
import { useController } from 'react-hook-form';
import { PiBracketsCurlyBold } from 'react-icons/pi';
import type { StylePresetFormData } from './StylePresetForm';
import { PRESET_PLACEHOLDER } from '../hooks/usePresetModifiedPrompts';
interface Props extends UseControllerProps<StylePresetFormData> {
label: string;
}
export const StylePresetPromptField = (props: Props) => {
const { field } = useController(props);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
(v) => {
field.onChange(v.target.value);
},
[field]
);
const value = useMemo(() => {
return field.value;
}, [field.value]);
const insertPromptPlaceholder = useCallback(() => {
if (textareaRef.current) {
const cursorPos = textareaRef.current.selectionStart;
const textBeforeCursor = value.slice(0, cursorPos);
const textAfterCursor = value.slice(cursorPos);
const newValue = textBeforeCursor + PRESET_PLACEHOLDER + textAfterCursor;
field.onChange(newValue);
} else {
field.onChange(value + PRESET_PLACEHOLDER);
}
}, [value, field, textareaRef]);
const isPromptPresent = useMemo(() => value.includes(PRESET_PLACEHOLDER), [value]);
return (
<FormControl orientation="vertical">
<Flex alignItems="center" gap="1">
<FormLabel>{props.label}</FormLabel>
<IconButton
onClick={insertPromptPlaceholder}
size="sm"
icon={<PiBracketsCurlyBold />}
aria-label="Insert placeholder"
isDisabled={isPromptPresent}
/>
</Flex>
<Textarea size="sm" ref={textareaRef} value={value} onChange={onChange} />
</FormControl>
);
};

View File

@ -0,0 +1,60 @@
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { searchTermChanged } from 'features/stylePresets/store/stylePresetSlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
const StylePresetSearch = () => {
const dispatch = useAppDispatch();
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
const { t } = useTranslation();
const handlePresetSearch = useCallback(
(newSearchTerm: string) => {
dispatch(searchTermChanged(newSearchTerm));
},
[dispatch]
);
const clearPresetSearch = useCallback(() => {
dispatch(searchTermChanged(''));
}, [dispatch]);
const handleKeydown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
// exit search mode on escape
if (e.key === 'Escape') {
clearPresetSearch();
}
},
[clearPresetSearch]
);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
handlePresetSearch(e.target.value);
},
[handlePresetSearch]
);
return (
<InputGroup>
<Input placeholder="Search by name" value={searchTerm} onKeyDown={handleKeydown} onChange={handleChange} />
{searchTerm && searchTerm.length && (
<InputRightElement h="full" pe={2}>
<IconButton
onClick={clearPresetSearch}
size="sm"
variant="link"
aria-label={t('boards.clearSearch')}
icon={<PiXBold />}
/>
</InputRightElement>
)}
</InputGroup>
);
};
export default memo(StylePresetSearch);

View File

@ -0,0 +1,25 @@
import { useAppSelector } from "app/store/storeHooks"
export const PRESET_PLACEHOLDER = `{prompt}`
export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: string) => {
return presetPrompt.includes(PRESET_PLACEHOLDER) ? presetPrompt.replace(new RegExp(PRESET_PLACEHOLDER), currentPrompt) : `${currentPrompt} ${presetPrompt}`
}
export const usePresetModifiedPrompts = () => {
const activeStylePreset = useAppSelector(s => s.stylePreset.activeStylePreset)
const { positivePrompt, negativePrompt } = useAppSelector(s => s.controlLayers.present)
if (!activeStylePreset) {
return { presetModifiedPositivePrompt: positivePrompt, presetModifiedNegativePrompt: negativePrompt }
}
const { positive_prompt: presetPositivePrompt, negative_prompt: presetNegativePrompt } = activeStylePreset.preset_data;
const presetModifiedPositivePrompt = buildPresetModifiedPrompt(presetPositivePrompt, positivePrompt)
const presetModifiedNegativePrompt = buildPresetModifiedPrompt(presetNegativePrompt, negativePrompt)
return { presetModifiedPositivePrompt, presetModifiedNegativePrompt }
}

View File

@ -0,0 +1,17 @@
import { useMemo } from 'react';
import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
export const useStylePresetFields = (preset: StylePresetRecordDTO | null) => {
const stylePresetFieldDefaults = useMemo(() => {
return {
name: preset ? preset.name : '',
positivePrompt: preset ? preset.preset_data.positive_prompt : '',
negativePrompt: preset ? preset.preset_data.negative_prompt : ''
};
}, [
preset
]);
return stylePresetFieldDefaults;
};

View File

@ -9,8 +9,7 @@ import type { StylePresetState } from './types';
export const initialState: StylePresetState = {
isMenuOpen: false,
activeStylePreset: null,
calculatedPosPrompt: undefined,
calculatedNegPrompt: undefined
searchTerm: ""
};
@ -24,15 +23,12 @@ export const stylePresetSlice = createSlice({
activeStylePresetChanged: (state, action: PayloadAction<StylePresetRecordDTO | null>) => {
state.activeStylePreset = action.payload;
},
calculatedPosPromptChanged: (state, action: PayloadAction<string | undefined>) => {
state.calculatedPosPrompt = action.payload;
},
calculatedNegPromptChanged: (state, action: PayloadAction<string | undefined>) => {
state.calculatedNegPrompt = action.payload;
searchTermChanged: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
},
});
export const { isMenuOpenChanged, activeStylePresetChanged, calculatedPosPromptChanged, calculatedNegPromptChanged } = stylePresetSlice.actions;
export const { isMenuOpenChanged, activeStylePresetChanged, searchTermChanged } = stylePresetSlice.actions;
export const selectStylePresetSlice = (state: RootState) => state.stylePreset;

View File

@ -8,7 +8,6 @@ export type StylePresetModalState = {
export type StylePresetState = {
isMenuOpen: boolean;
activeStylePreset: StylePresetRecordDTO | null;
calculatedPosPrompt?: string
calculatedNegPrompt?: string
searchTerm: string
}

View File

@ -1,5 +1,5 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { Box, Flex, Portal,Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
@ -13,10 +13,12 @@ import { ControlSettingsAccordion } from 'features/settingsAccordions/components
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const overlayScrollbarsStyles: CSSProperties = {
@ -58,11 +60,31 @@ const ParametersPanelTextToImage = () => {
[dispatch]
);
const ref = useRef<HTMLDivElement>(null);
const isMenuOpen = useAppSelector((s) => s.stylePreset.isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
<Portal containerRef={ref}>
{isMenuOpen && (
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
<OverlayScrollbarsComponent
defer
style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options}
// backgroundColor="rgba(0,0,0,0.5)"
>
<Flex gap={2} flexDirection="column" h="full" w="full" layerStyle="second">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
</Box>
)}
</Portal>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />

View File

@ -67,11 +67,10 @@ export const stylePresetsApi = api.injectEndpoints({
}),
listStylePresets: build.query<
paths['/api/v1/style_presets/']['get']['responses']['200']['content']['application/json'],
NonNullable<paths['/api/v1/style_presets/']['get']['parameters']['query']>
void
>({
query: (params) => ({
query: () => ({
url: buildStylePresetsUrl(),
params,
}),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
}),

View File

@ -7345,147 +7345,147 @@ export type components = {
project_id: string | null;
};
InvocationOutputMap: {
crop_latents: components["schemas"]["LatentsOutput"];
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"];
color_map_image_processor: components["schemas"]["ImageOutput"];
img_paste: components["schemas"]["ImageOutput"];
image_collection: components["schemas"]["ImageCollectionOutput"];
img_channel_offset: components["schemas"]["ImageOutput"];
compel: components["schemas"]["ConditioningOutput"];
denoise_latents: components["schemas"]["LatentsOutput"];
t2i_adapter: components["schemas"]["T2IAdapterOutput"];
pair_tile_image: components["schemas"]["PairTileImageOutput"];
invert_tensor_mask: components["schemas"]["MaskOutput"];
img_conv: components["schemas"]["ImageOutput"];
img_hue_adjust: components["schemas"]["ImageOutput"];
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
conditioning: components["schemas"]["ConditioningOutput"];
mlsd_image_processor: components["schemas"]["ImageOutput"];
image_mask_to_tensor: components["schemas"]["MaskOutput"];
float_to_int: components["schemas"]["IntegerOutput"];
img_scale: components["schemas"]["ImageOutput"];
color: components["schemas"]["ColorOutput"];
midas_depth_image_processor: components["schemas"]["ImageOutput"];
mul: components["schemas"]["IntegerOutput"];
pidi_image_processor: components["schemas"]["ImageOutput"];
img_crop: components["schemas"]["ImageOutput"];
metadata_item: components["schemas"]["MetadataItemOutput"];
sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
metadata: components["schemas"]["MetadataOutput"];
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
lblend: components["schemas"]["LatentsOutput"];
canvas_paste_back: components["schemas"]["ImageOutput"];
lresize: components["schemas"]["LatentsOutput"];
sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"];
normalbae_image_processor: components["schemas"]["ImageOutput"];
random_range: components["schemas"]["IntegerCollectionOutput"];
img_chan: components["schemas"]["ImageOutput"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
img_nsfw: components["schemas"]["ImageOutput"];
mask_combine: components["schemas"]["ImageOutput"];
color_correct: components["schemas"]["ImageOutput"];
latents: components["schemas"]["LatentsOutput"];
seamless: components["schemas"]["SeamlessModeOutput"];
lscale: components["schemas"]["LatentsOutput"];
string_join_three: components["schemas"]["StringOutput"];
lineart_image_processor: components["schemas"]["ImageOutput"];
lineart_anime_image_processor: components["schemas"]["ImageOutput"];
face_mask_detection: components["schemas"]["FaceMaskOutput"];
esrgan: components["schemas"]["ImageOutput"];
mask_edge: components["schemas"]["ImageOutput"];
segment_anything_processor: components["schemas"]["ImageOutput"];
cv_inpaint: components["schemas"]["ImageOutput"];
tomask: components["schemas"]["ImageOutput"];
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
infill_cv2: components["schemas"]["ImageOutput"];
spandrel_image_to_image: components["schemas"]["ImageOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
img_resize: components["schemas"]["ImageOutput"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
controlnet: components["schemas"]["ControlOutput"];
merge_tiles_to_image: components["schemas"]["ImageOutput"];
mediapipe_face_processor: components["schemas"]["ImageOutput"];
img_mul: components["schemas"]["ImageOutput"];
image: components["schemas"]["ImageOutput"];
merge_metadata: components["schemas"]["MetadataOutput"];
add: components["schemas"]["IntegerOutput"];
mask_from_id: components["schemas"]["ImageOutput"];
vae_loader: components["schemas"]["VAEOutput"];
ideal_size: components["schemas"]["IdealSizeOutput"];
noise: components["schemas"]["NoiseOutput"];
sub: components["schemas"]["IntegerOutput"];
freeu: components["schemas"]["UNetOutput"];
lora_selector: components["schemas"]["LoRASelectorOutput"];
integer: components["schemas"]["IntegerOutput"];
l2i: components["schemas"]["ImageOutput"];
integer_collection: components["schemas"]["IntegerCollectionOutput"];
rand_float: components["schemas"]["FloatOutput"];
iterate: components["schemas"]["IterateInvocationOutput"];
sdxl_compel_prompt: components["schemas"]["ConditioningOutput"];
content_shuffle_image_processor: components["schemas"]["ImageOutput"];
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
zoe_depth_image_processor: components["schemas"]["ImageOutput"];
hed_image_processor: components["schemas"]["ImageOutput"];
infill_lama: components["schemas"]["ImageOutput"];
boolean: components["schemas"]["BooleanOutput"];
float: components["schemas"]["FloatOutput"];
range_of_size: components["schemas"]["IntegerCollectionOutput"];
float_collection: components["schemas"]["FloatCollectionOutput"];
img_channel_multiply: components["schemas"]["ImageOutput"];
prompt_from_file: components["schemas"]["StringCollectionOutput"];
infill_rgba: components["schemas"]["ImageOutput"];
rectangle_mask: components["schemas"]["MaskOutput"];
depth_anything_image_processor: components["schemas"]["ImageOutput"];
boolean_collection: components["schemas"]["BooleanCollectionOutput"];
rand_int: components["schemas"]["IntegerOutput"];
latents_collection: components["schemas"]["LatentsCollectionOutput"];
string_split: components["schemas"]["String2Output"];
round_float: components["schemas"]["FloatOutput"];
canny_image_processor: components["schemas"]["ImageOutput"];
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
float_range: components["schemas"]["FloatCollectionOutput"];
img_ilerp: components["schemas"]["ImageOutput"];
unsharp_mask: components["schemas"]["ImageOutput"];
img_watermark: components["schemas"]["ImageOutput"];
model_identifier: components["schemas"]["ModelIdentifierOutput"];
string_split_neg: components["schemas"]["StringPosNegOutput"];
calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"];
collect: components["schemas"]["CollectInvocationOutput"];
core_metadata: components["schemas"]["MetadataOutput"];
img_pad_crop: components["schemas"]["ImageOutput"];
i2l: components["schemas"]["LatentsOutput"];
tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"];
save_image: components["schemas"]["ImageOutput"];
show_image: components["schemas"]["ImageOutput"];
string_collection: components["schemas"]["StringCollectionOutput"];
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"];
infill_patchmatch: components["schemas"]["ImageOutput"];
main_model_loader: components["schemas"]["ModelLoaderOutput"];
lora_loader: components["schemas"]["LoRALoaderOutput"];
string: components["schemas"]["StringOutput"];
heuristic_resize: components["schemas"]["ImageOutput"];
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
div: components["schemas"]["IntegerOutput"];
blank_image: components["schemas"]["ImageOutput"];
float_math: components["schemas"]["FloatOutput"];
infill_tile: components["schemas"]["ImageOutput"];
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
face_off: components["schemas"]["FaceOffOutput"];
tile_image_processor: components["schemas"]["ImageOutput"];
leres_image_processor: components["schemas"]["ImageOutput"];
face_identifier: components["schemas"]["ImageOutput"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
scheduler: components["schemas"]["SchedulerOutput"];
img_lerp: components["schemas"]["ImageOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
merge_metadata: components["schemas"]["MetadataOutput"];
lresize: components["schemas"]["LatentsOutput"];
string_split_neg: components["schemas"]["StringPosNegOutput"];
img_channel_multiply: components["schemas"]["ImageOutput"];
add: components["schemas"]["IntegerOutput"];
lscale: components["schemas"]["LatentsOutput"];
string_replace: components["schemas"]["StringOutput"];
step_param_easing: components["schemas"]["FloatCollectionOutput"];
sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"];
collect: components["schemas"]["CollectInvocationOutput"];
face_off: components["schemas"]["FaceOffOutput"];
ideal_size: components["schemas"]["IdealSizeOutput"];
float_to_int: components["schemas"]["IntegerOutput"];
mlsd_image_processor: components["schemas"]["ImageOutput"];
sub: components["schemas"]["IntegerOutput"];
midas_depth_image_processor: components["schemas"]["ImageOutput"];
rectangle_mask: components["schemas"]["MaskOutput"];
img_watermark: components["schemas"]["ImageOutput"];
img_ilerp: components["schemas"]["ImageOutput"];
pidi_image_processor: components["schemas"]["ImageOutput"];
vae_loader: components["schemas"]["VAEOutput"];
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
float_range: components["schemas"]["FloatCollectionOutput"];
spandrel_image_to_image_autoscale: components["schemas"]["ImageOutput"];
ip_adapter: components["schemas"]["IPAdapterOutput"];
tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
integer_math: components["schemas"]["IntegerOutput"];
range: components["schemas"]["IntegerCollectionOutput"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
denoise_latents: components["schemas"]["LatentsOutput"];
image_collection: components["schemas"]["ImageCollectionOutput"];
t2i_adapter: components["schemas"]["T2IAdapterOutput"];
normalbae_image_processor: components["schemas"]["ImageOutput"];
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"];
mul: components["schemas"]["IntegerOutput"];
image_mask_to_tensor: components["schemas"]["MaskOutput"];
face_mask_detection: components["schemas"]["FaceMaskOutput"];
save_image: components["schemas"]["ImageOutput"];
blank_image: components["schemas"]["ImageOutput"];
conditioning: components["schemas"]["ConditioningOutput"];
img_chan: components["schemas"]["ImageOutput"];
string_split: components["schemas"]["String2Output"];
segment_anything_processor: components["schemas"]["ImageOutput"];
unsharp_mask: components["schemas"]["ImageOutput"];
boolean_collection: components["schemas"]["BooleanCollectionOutput"];
color: components["schemas"]["ColorOutput"];
range_of_size: components["schemas"]["IntegerCollectionOutput"];
face_identifier: components["schemas"]["ImageOutput"];
div: components["schemas"]["IntegerOutput"];
invert_tensor_mask: components["schemas"]["MaskOutput"];
step_param_easing: components["schemas"]["FloatCollectionOutput"];
merge_tiles_to_image: components["schemas"]["ImageOutput"];
latents: components["schemas"]["LatentsOutput"];
lineart_anime_image_processor: components["schemas"]["ImageOutput"];
mediapipe_face_processor: components["schemas"]["ImageOutput"];
infill_lama: components["schemas"]["ImageOutput"];
compel: components["schemas"]["ConditioningOutput"];
round_float: components["schemas"]["FloatOutput"];
string_join: components["schemas"]["StringOutput"];
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
content_shuffle_image_processor: components["schemas"]["ImageOutput"];
metadata: components["schemas"]["MetadataOutput"];
lineart_image_processor: components["schemas"]["ImageOutput"];
zoe_depth_image_processor: components["schemas"]["ImageOutput"];
controlnet: components["schemas"]["ControlOutput"];
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
img_lerp: components["schemas"]["ImageOutput"];
scheduler: components["schemas"]["SchedulerOutput"];
l2i: components["schemas"]["ImageOutput"];
img_paste: components["schemas"]["ImageOutput"];
tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
depth_anything_image_processor: components["schemas"]["ImageOutput"];
prompt_from_file: components["schemas"]["StringCollectionOutput"];
integer: components["schemas"]["IntegerOutput"];
img_mul: components["schemas"]["ImageOutput"];
rand_int: components["schemas"]["IntegerOutput"];
string_join_three: components["schemas"]["StringOutput"];
img_blur: components["schemas"]["ImageOutput"];
float: components["schemas"]["FloatOutput"];
sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
crop_latents: components["schemas"]["LatentsOutput"];
model_identifier: components["schemas"]["ModelIdentifierOutput"];
show_image: components["schemas"]["ImageOutput"];
img_channel_offset: components["schemas"]["ImageOutput"];
i2l: components["schemas"]["LatentsOutput"];
core_metadata: components["schemas"]["MetadataOutput"];
infill_cv2: components["schemas"]["ImageOutput"];
lora_selector: components["schemas"]["LoRASelectorOutput"];
rand_float: components["schemas"]["FloatOutput"];
float_math: components["schemas"]["FloatOutput"];
main_model_loader: components["schemas"]["ModelLoaderOutput"];
image: components["schemas"]["ImageOutput"];
infill_patchmatch: components["schemas"]["ImageOutput"];
noise: components["schemas"]["NoiseOutput"];
tile_image_processor: components["schemas"]["ImageOutput"];
spandrel_image_to_image: components["schemas"]["ImageOutput"];
hed_image_processor: components["schemas"]["ImageOutput"];
heuristic_resize: components["schemas"]["ImageOutput"];
img_hue_adjust: components["schemas"]["ImageOutput"];
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"];
canvas_paste_back: components["schemas"]["ImageOutput"];
ip_adapter: components["schemas"]["IPAdapterOutput"];
img_nsfw: components["schemas"]["ImageOutput"];
range: components["schemas"]["IntegerCollectionOutput"];
string: components["schemas"]["StringOutput"];
seamless: components["schemas"]["SeamlessModeOutput"];
integer_collection: components["schemas"]["IntegerCollectionOutput"];
iterate: components["schemas"]["IterateInvocationOutput"];
img_scale: components["schemas"]["ImageOutput"];
pair_tile_image: components["schemas"]["PairTileImageOutput"];
canny_image_processor: components["schemas"]["ImageOutput"];
integer_math: components["schemas"]["IntegerOutput"];
leres_image_processor: components["schemas"]["ImageOutput"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
float_collection: components["schemas"]["FloatCollectionOutput"];
string_collection: components["schemas"]["StringCollectionOutput"];
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
mask_combine: components["schemas"]["ImageOutput"];
latents_collection: components["schemas"]["LatentsCollectionOutput"];
mask_edge: components["schemas"]["ImageOutput"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
img_conv: components["schemas"]["ImageOutput"];
color_correct: components["schemas"]["ImageOutput"];
lblend: components["schemas"]["LatentsOutput"];
mask_from_id: components["schemas"]["ImageOutput"];
img_pad_crop: components["schemas"]["ImageOutput"];
metadata_item: components["schemas"]["MetadataItemOutput"];
lora_loader: components["schemas"]["LoRALoaderOutput"];
cv_inpaint: components["schemas"]["ImageOutput"];
color_map_image_processor: components["schemas"]["ImageOutput"];
img_resize: components["schemas"]["ImageOutput"];
random_range: components["schemas"]["IntegerCollectionOutput"];
img_crop: components["schemas"]["ImageOutput"];
freeu: components["schemas"]["UNetOutput"];
infill_tile: components["schemas"]["ImageOutput"];
infill_rgba: components["schemas"]["ImageOutput"];
tomask: components["schemas"]["ImageOutput"];
sdxl_compel_prompt: components["schemas"]["ConditioningOutput"];
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"];
boolean: components["schemas"]["BooleanOutput"];
};
/**
* InvocationStartedEvent
@ -10142,34 +10142,6 @@ export type components = {
/** Ui Order */
ui_order: number | null;
};
/** PaginatedResults[StylePresetRecordDTO] */
PaginatedResults_StylePresetRecordDTO_: {
/**
* Page
* @description Current Page
*/
page: number;
/**
* Pages
* @description Total number of pages
*/
pages: number;
/**
* Per Page
* @description Number of items per page
*/
per_page: number;
/**
* Total
* @description Total number of items in result
*/
total: number;
/**
* Items
* @description Items
*/
items: components["schemas"]["StylePresetRecordDTO"][];
};
/** PaginatedResults[WorkflowRecordListItemDTO] */
PaginatedResults_WorkflowRecordListItemDTO_: {
/**
@ -12700,12 +12672,6 @@ export type components = {
*/
is_default: boolean;
};
/**
* StylePresetRecordOrderBy
* @description The order by options for workflow records
* @enum {string}
*/
StylePresetRecordOrderBy: "created_at" | "name";
/** StylePresetWithoutId */
StylePresetWithoutId: {
/**
@ -16331,31 +16297,11 @@ export type operations = {
* @description Gets a page of style presets
*/
list_style_presets: {
parameters: {
query?: {
/** @description The page to get */
page?: number;
/** @description The number of style presets per page */
per_page?: number;
/** @description The attribute to order by */
order_by?: components["schemas"]["StylePresetRecordOrderBy"];
/** @description The direction to order by */
direction?: components["schemas"]["SQLiteDirection"];
/** @description The text to query by (matches name and description) */
query?: string | null;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PaginatedResults_StylePresetRecordDTO_"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
"application/json": components["schemas"]["StylePresetRecordDTO"][];
};
};
};