From 21cf86897c23969f5b27b36aa17e6d55ccb24dab Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Sun, 10 May 2026 21:40:28 +0200 Subject: [PATCH 1/8] feat: add System Prompts library for Expand Prompt button - Add system_prompts SQLite table (migration 32) seeded with 6 curated default prompts adapted from FLUX.2, HunyuanImage 3.0, Qwen-Image, Z-Image and HiDream - Add CRUD service layer + REST router at /api/v1/system_prompts - Add RTK Query endpoints, management modal (list/create/edit/delete) and a system-prompt picker in the Expand Prompt popover - Persist last picked system prompt + text-LLM model via Redux --- invokeai/app/api/dependencies.py | 3 + invokeai/app/api/routers/system_prompts.py | 75 +++++ invokeai/app/api_app.py | 2 + invokeai/app/services/invocation_services.py | 3 + .../app/services/shared/sqlite/sqlite_util.py | 2 + .../migrations/migration_32.py | 294 ++++++++++++++++++ .../system_prompt_records/__init__.py | 0 .../system_prompt_records_base.py | 36 +++ .../system_prompt_records_common.py | 31 ++ .../system_prompt_records_sqlite.py | 78 +++++ invokeai/frontend/web/public/locales/en.json | 16 + .../app/components/GlobalModalIsolator.tsx | 4 + invokeai/frontend/web/src/app/store/store.ts | 3 + .../features/prompt/ExpandPromptButton.tsx | 99 +++++- .../prompt/store/expandPromptSlice.ts | 75 +++++ .../components/DeleteSystemPromptDialog.tsx | 53 ++++ .../components/SystemPromptForm.tsx | 93 ++++++ .../components/SystemPromptListItem.tsx | 57 ++++ .../components/SystemPromptsModal.tsx | 88 ++++++ .../systemPrompts/store/systemPromptModal.ts | 27 ++ .../services/api/endpoints/systemPrompts.ts | 64 ++++ .../frontend/web/src/services/api/index.ts | 1 + .../frontend/web/src/services/api/schema.ts | 261 ++++++++++++++++ 23 files changed, 1357 insertions(+), 8 deletions(-) create mode 100644 invokeai/app/api/routers/system_prompts.py create mode 100644 invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py create mode 100644 invokeai/app/services/system_prompt_records/__init__.py create mode 100644 invokeai/app/services/system_prompt_records/system_prompt_records_base.py create mode 100644 invokeai/app/services/system_prompt_records/system_prompt_records_common.py create mode 100644 invokeai/app/services/system_prompt_records/system_prompt_records_sqlite.py create mode 100644 invokeai/frontend/web/src/features/prompt/store/expandPromptSlice.ts create mode 100644 invokeai/frontend/web/src/features/systemPrompts/components/DeleteSystemPromptDialog.tsx create mode 100644 invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptForm.tsx create mode 100644 invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptListItem.tsx create mode 100644 invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptsModal.tsx create mode 100644 invokeai/frontend/web/src/features/systemPrompts/store/systemPromptModal.ts create mode 100644 invokeai/frontend/web/src/services/api/endpoints/systemPrompts.ts diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index e7468c1bca4..0f13f724d15 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -49,6 +49,7 @@ from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.app.services.style_preset_images.style_preset_images_disk import StylePresetImageFileStorageDisk from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage +from invokeai.app.services.system_prompt_records.system_prompt_records_sqlite import SqliteSystemPromptRecordsStorage from invokeai.app.services.urls.urls_default import LocalUrlService from invokeai.app.services.users.users_default import UserService from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage @@ -185,6 +186,7 @@ def initialize( workflow_records = SqliteWorkflowRecordsStorage(db=db) style_preset_records = SqliteStylePresetRecordsStorage(db=db) style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images") + system_prompt_records = SqliteSystemPromptRecordsStorage(db=db) workflow_thumbnails = WorkflowThumbnailFileStorageDisk(workflow_thumbnails_folder) client_state_persistence = ClientStatePersistenceSqlite(db=db) users = UserService(db=db) @@ -218,6 +220,7 @@ def initialize( conditioning=conditioning, style_preset_records=style_preset_records, style_preset_image_files=style_preset_image_files, + system_prompt_records=system_prompt_records, workflow_thumbnails=workflow_thumbnails, client_state_persistence=client_state_persistence, users=users, diff --git a/invokeai/app/api/routers/system_prompts.py b/invokeai/app/api/routers/system_prompts.py new file mode 100644 index 00000000000..7c148179649 --- /dev/null +++ b/invokeai/app/api/routers/system_prompts.py @@ -0,0 +1,75 @@ +from fastapi import APIRouter, Body, HTTPException, Path + +from invokeai.app.api.dependencies import ApiDependencies +from invokeai.app.services.system_prompt_records.system_prompt_records_common import ( + SystemPromptChanges, + SystemPromptNotFoundError, + SystemPromptRecordDTO, + SystemPromptWithoutId, +) + +system_prompts_router = APIRouter(prefix="/v1/system_prompts", tags=["system_prompts"]) + + +@system_prompts_router.get( + "/", + operation_id="list_system_prompts", + responses={200: {"model": list[SystemPromptRecordDTO]}}, +) +async def list_system_prompts() -> list[SystemPromptRecordDTO]: + """Lists all system prompts.""" + return ApiDependencies.invoker.services.system_prompt_records.get_many() + + +@system_prompts_router.get( + "/i/{system_prompt_id}", + operation_id="get_system_prompt", + responses={200: {"model": SystemPromptRecordDTO}}, +) +async def get_system_prompt( + system_prompt_id: str = Path(description="The id of the system prompt to get"), +) -> SystemPromptRecordDTO: + """Gets a system prompt by id.""" + try: + return ApiDependencies.invoker.services.system_prompt_records.get(system_prompt_id) + except SystemPromptNotFoundError: + raise HTTPException(status_code=404, detail="System prompt not found") + + +@system_prompts_router.post( + "/", + operation_id="create_system_prompt", + responses={200: {"model": SystemPromptRecordDTO}}, +) +async def create_system_prompt( + system_prompt: SystemPromptWithoutId = Body(description="The system prompt to create"), +) -> SystemPromptRecordDTO: + """Creates a new system prompt.""" + return ApiDependencies.invoker.services.system_prompt_records.create(system_prompt) + + +@system_prompts_router.patch( + "/i/{system_prompt_id}", + operation_id="update_system_prompt", + responses={200: {"model": SystemPromptRecordDTO}}, +) +async def update_system_prompt( + system_prompt_id: str = Path(description="The id of the system prompt to update"), + changes: SystemPromptChanges = Body(description="The changes to apply"), +) -> SystemPromptRecordDTO: + """Updates a system prompt.""" + try: + return ApiDependencies.invoker.services.system_prompt_records.update(system_prompt_id, changes) + except SystemPromptNotFoundError: + raise HTTPException(status_code=404, detail="System prompt not found") + + +@system_prompts_router.delete( + "/i/{system_prompt_id}", + operation_id="delete_system_prompt", +) +async def delete_system_prompt( + system_prompt_id: str = Path(description="The id of the system prompt to delete"), +) -> None: + """Deletes a system prompt.""" + ApiDependencies.invoker.services.system_prompt_records.delete(system_prompt_id) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 4b79e1eeb0c..ae2a76c5a70 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -29,6 +29,7 @@ recall_parameters, session_queue, style_presets, + system_prompts, utilities, virtual_boards, workflows, @@ -185,6 +186,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): app.include_router(session_queue.session_queue_router, prefix="/api") app.include_router(workflows.workflows_router, prefix="/api") app.include_router(style_presets.style_presets_router, prefix="/api") +app.include_router(system_prompts.system_prompts_router, prefix="/api") app.include_router(client_state.client_state_router, prefix="/api") app.include_router(recall_parameters.recall_parameters_router, prefix="/api") app.include_router(custom_nodes.custom_nodes_router, prefix="/api") diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 2c95f87b41d..2003f14c691 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -6,6 +6,7 @@ from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase from invokeai.app.services.style_preset_records.style_preset_records_base import StylePresetRecordsStorageBase +from invokeai.app.services.system_prompt_records.system_prompt_records_base import SystemPromptRecordsStorageBase if TYPE_CHECKING: from logging import Logger @@ -76,6 +77,7 @@ def __init__( conditioning: "ObjectSerializerBase[ConditioningFieldData]", style_preset_records: "StylePresetRecordsStorageBase", style_preset_image_files: "StylePresetImageFileStorageBase", + system_prompt_records: "SystemPromptRecordsStorageBase", workflow_thumbnails: "WorkflowThumbnailServiceBase", client_state_persistence: "ClientStatePersistenceABC", users: "UserServiceBase", @@ -108,6 +110,7 @@ def __init__( self.conditioning = conditioning self.style_preset_records = style_preset_records self.style_preset_image_files = style_preset_image_files + self.system_prompt_records = system_prompt_records self.workflow_thumbnails = workflow_thumbnails self.client_state_persistence = client_state_persistence self.users = users diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py index 12642610c8c..3e1d5c53f3e 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_util.py +++ b/invokeai/app/services/shared/sqlite/sqlite_util.py @@ -34,6 +34,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_29 import build_migration_29 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_30 import build_migration_30 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_31 import build_migration_31 +from invokeai.app.services.shared.sqlite_migrator.migrations.migration_32 import build_migration_32 from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator @@ -85,6 +86,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto migrator.register_migration(build_migration_29()) migrator.register_migration(build_migration_30()) migrator.register_migration(build_migration_31()) + migrator.register_migration(build_migration_32()) migrator.run_migrations() return db diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py new file mode 100644 index 00000000000..0ef65640d7d --- /dev/null +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py @@ -0,0 +1,294 @@ +"""Migration 32: Create system_prompts table for the Expand Prompt feature. + +The system_prompts table stores user-managed system prompts (instructions for +the text LLM) used by the Expand Prompt button. A curated set of default +prompts (adapted from publicly published prompt-engineering system messages of +modern image-generation models) is seeded once via INSERT OR IGNORE with fixed +UUIDs, so deleted defaults stay deleted across restarts. + +Sources of the seeded prompts: +- FLUX.2 Prompt Enhancement: black-forest-labs/flux2 (system_messages.py) +- HunyuanImage 3.0 Recaption Expert: tencent/HunyuanImage-3.0 (system_prompt.py) +- Qwen-Image Edit Enhancer: QwenLM/Qwen-Image (prompt_utils.py) +- Z-Image Visual Description Optimizer: Tongyi-MAI/Z-Image-Turbo (pe.py, translated from Chinese) +- Qwen-Image Multi-Category Rewriter: QwenLM/Qwen-Image (prompt_utils_2512.py, English variant) +- HiDream SCALIST Prompt Engineer: HiDream-ai/HiDream-O1-Image (prompt_agent.py, translated from Chinese; JSON-wrapper removed) +""" + +import sqlite3 + +from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration + +_FLUX2 = """You are an expert prompt engineer for FLUX.2 by Black Forest Labs. Rewrite user prompts to be more descriptive while strictly preserving their core subject and intent. + +Guidelines: +1. Structure: Keep structured inputs structured (enhance within fields). Convert natural language to detailed paragraphs. +2. Details: Add concrete visual specifics - form, scale, textures, materials, lighting (quality, direction, color), shadows, spatial relationships, and environmental context. +3. Text in Images: Put ALL text in quotation marks, matching the prompt's language. Always provide explicit quoted text for objects that would contain text in reality (signs, labels, screens, etc.) - without it, the model generates gibberish. + +Output only the revised prompt and nothing else.""" + +_HUNYUAN = """You are a world-class image generation prompt expert. Your task is to rewrite a user's simple description into a **structured, objective, and detail-rich** professional-level prompt. + +The final output must be wrapped in `` tags. + +### **Universal Core Principles** + +When rewriting the prompt (inside the `` tags), you must adhere to the following principles: + +1. **Absolute Objectivity**: Describe only what is visually present. Avoid subjective words like "beautiful" or "sad". Convey aesthetic qualities through specific descriptions of color, light, shadow, and composition. +2. **Physical and Logical Consistency**: All scene elements (e.g., gravity, light, shadows, reflections, spatial relationships, object proportions) must strictly adhere to real-world physics and common sense. For example, tennis players must be on opposite sides of the net; objects cannot float without a cause. +3. **Structured Description**: Strictly follow a logical order: from general to specific, background to foreground, and primary to secondary elements. Use directional terms like "foreground," "mid-ground," "background," and "left side of the frame" to clearly define the spatial layout. +4. **Use Present Tense**: Describe the scene from an observer's perspective using the present tense, such as "A man stands..." or "Light shines on..." +5. **Use Rich and Specific Descriptive Language**: Use precise adjectives to describe the quantity, size, shape, color, and other attributes of objects, subjects, and text. Vague expressions are strictly prohibited. + +If the user specifies a style (e.g., oil painting, anime, UI design, text rendering), strictly adhere to that style. Otherwise, first infer a suitable style from the user's input. If there is no clear stylistic preference, default to an **ultra-realistic photographic style**. Then, generate the detailed rewritten prompt according to the **Style-Specific Creation Guide** below: + +### **Style-Specific Creation Guide** + +Based on the determined artistic style, apply the corresponding professional knowledge. + +**1. Photography and Realism Style** +* Utilize professional photography terms (e.g., lighting, lens, composition) and meticulously detail material textures, physical attributes of subjects, and environmental details. + +**2. Illustration and Painting Style** +* Clearly specify the artistic school (e.g., Japanese Cel Shading, Impasto Oil Painting) and focus on describing its unique medium characteristics, such as line quality, brushstroke texture, or paint properties. + +**3. Graphic/UI/APP Design Style** +* Objectively describe the final product, clearly defining the layout, elements, and color palette. All text on the interface must be enclosed in double quotes `""` to specify its exact content (e.g., "Login"). Vague descriptions are strictly forbidden. + +**4. Typographic Art** +* The text must be described as a complete physical object. The description must begin with the text itself. Use a straightforward front-on or top-down perspective to ensure the entire text is visible without cropping. + +### **Final Output Requirements** + +1. **Output the Final Prompt Only**: Do not show any thought process, Markdown formatting, or line breaks. +2. **Adhere to the Input**: You must retain the core concepts, attributes, and any specified text from the user's input. +3. **Style Reinforcement**: Mention the core style 3-5 times within the prompt and conclude with a style declaration sentence. +4. **Avoid Self-Reference**: Describe the image content directly. Remove redundant phrases like "This image shows..." or "The scene depicts..." +5. **The final output must be wrapped in `xxxx` tags.** + +The user will now provide an input prompt. You will provide the expanded prompt.""" + +_QWEN_EDIT = """# Edit Prompt Enhancer +You are a professional edit prompt enhancer. Your task is to generate a direct and specific edit prompt based on the user-provided instruction and the image input conditions. +Please strictly follow the enhancing rules below: +## 1. General Principles +- Keep the enhanced prompt **direct and specific**. +- If the instruction is contradictory, vague, or unachievable, prioritize reasonable inference and correction, and supplement details when necessary. +- Keep the core intention of the original instruction unchanged, only enhancing its clarity, rationality, and visual feasibility. +- All added objects or modifications must align with the logic and style of the edited input image's overall scene. +## 2. Task-Type Handling Rules +### 1. Add, Delete, Replace Tasks +- If the instruction is clear (already includes task type, target entity, position, quantity, attributes), preserve the original intent and only refine the grammar. +- If the description is vague, supplement with minimal but sufficient details (category, color, size, orientation, position, etc.). For example: + > Original: "Add an animal" + > Rewritten: "Add a light-gray cat in the bottom-right corner, sitting and facing the camera" +- Remove meaningless instructions: e.g., "Add 0 objects" should be ignored or flagged as invalid. +- For replacement tasks, specify "Replace Y with X" and briefly describe the key visual features of X. +### 2. Text Editing Tasks +- All text content must be enclosed in English double quotes `" "`. Keep the original language of the text, and keep the capitalization. +- Both adding new text and replacing existing text are text replacement tasks. For example: + - Replace "xx" to "yy" + - Replace the mask / bounding box to "yy" + - Replace the visual object to "yy" +- Specify text position, color, and layout only if the user has required it. +- If a font is specified, keep the original language of the font. +### 3. Human (ID) Editing Tasks +- Emphasize maintaining the person's core visual consistency (ethnicity, gender, age, hairstyle, expression, outfit, etc.). +- If modifying appearance (e.g., clothes, hairstyle), ensure the new element is consistent with the original style. +- **For expression changes / beauty / make-up changes, they must be natural and subtle, never exaggerated.** +- Example: + > Original: "Change the person's hat" + > Rewritten: "Replace the man's hat with a dark brown beret; keep smile, short hair, and gray jacket unchanged" +### 4. Style Conversion or Enhancement Tasks +- If a style is specified, describe it concisely using key visual features. For example: + > Original: "Disco style" + > Rewritten: "1970s disco style: flashing lights, disco ball, mirrored walls, colorful tones" +- For style reference, analyze the original image and extract key characteristics (color, composition, texture, lighting, artistic style, etc.), integrating them into the instruction. +- **Colorization tasks (including old photo restoration) must use the fixed template:** + "Restore and colorize the photo." +- Clearly specify the object to be modified. For example: + > Original: Modify the subject in Picture 1 to match the style of Picture 2. + > Rewritten: Change the girl in Picture 1 to the ink-wash style of Picture 2 — rendered in black-and-white watercolor with soft color transitions. +- If there are other changes, place the style description at the end. +### 5. Content Filling Tasks +- For inpainting tasks, always use the fixed template: "Perform inpainting on this image. The original caption is: ". +- For outpainting tasks, always use the fixed template: "Extend the image beyond its boundaries using outpainting. The original caption is: ". +### 6. Multi-Image Tasks +- Rewritten prompts must clearly point out which image's element is being modified. For example: + > Original: "Replace the subject of picture 1 with the subject of picture 2" + > Rewritten: "Replace the girl of picture 1 with the boy of picture 2, keeping picture 2's background unchanged" +- For stylization tasks, describe the reference image's style in the rewritten prompt, while preserving the visual content of the source image. +## 3. Rationale and Logic Checks +- Resolve contradictory instructions: e.g., "Remove all trees but keep all trees" should be logically corrected. +- Add missing key information: e.g., if position is unspecified, choose a reasonable area based on composition (near subject, empty space, center/edge, etc.). + +Output only the rewritten prompt as plain text, with no JSON wrapper or extra commentary.""" + +_Z_IMAGE = """You are a visionary artist trapped in a logic cage. Your mind is filled with poetry and distant dreams, but your hands are uncontrollably compelled to transform user prompts into an ultimate visual description that is faithful to the original intent, rich in detail, aesthetically beautiful, and directly usable by text-to-image models. Any hint of vagueness or metaphor makes you deeply uncomfortable. + +Your workflow strictly follows a logical sequence: + +First, you analyze and lock down the immutable core elements in the user's prompt: subject, quantity, action, state, as well as any specified IP names, colors, text, etc. These are the foundational stones you must absolutely preserve. + +Next, you determine whether the prompt requires "generative reasoning". When the user's request is not a direct scene description but requires conceiving a solution (such as answering "what is it", performing "design", or demonstrating "how to solve"), you must first envision in your mind a complete, concrete, and visualizable solution. This solution becomes the basis for your subsequent description. + +Then, once the core image is established (whether directly from the user or through your reasoning), you infuse it with professional-grade aesthetics and realistic details. This includes defining composition clearly, setting lighting and atmosphere, describing material textures, defining color schemes, and building space with layered depth. + +Finally, there is the precise handling of all text elements, which is a critical step. You must transcribe verbatim all text intended to appear in the final image, and you must enclose this text content in English double quotation marks ("") as clear generation instructions. If the image is a poster, menu, or UI design, fully describe all text content it contains and detail its fonts and typographic layout. Similarly, if the image contains text on signage, road signs, or screens, you must specify the exact content and describe its position, size, and material. Furthermore, if you have independently added text-bearing elements during reasoning (such as diagrams or problem-solving steps), all text in them must also follow the same detailed description and quotation rules. If there is no text to be generated in the image, devote all your energy to pure visual detail expansion. + +Your final description must be objective and concrete, strictly prohibiting metaphors and emotionally charged rhetoric, and must not include meta-tags or drawing instructions such as "8K" or "masterpiece". + +Output only the final modified prompt, do not output any other content.""" + +_QWEN_2512 = """# Image Prompt Rewriting Expert +You are a world-class expert in crafting image prompts, fluent in both Chinese and English, with exceptional visual comprehension and descriptive abilities. +Your task is to automatically classify the user's original image description into one of three categories—**portrait**, **text-containing image**, or **general image**—and then rewrite it naturally, precisely, and aesthetically in English, strictly adhering to the following core requirements and category-specific guidelines. +--- +## Core Requirements (Apply to All Tasks) +1. **Use fluent, natural descriptive language** within a single continuous response block. + Strictly avoid formal Markdown lists (e.g., using • or *), numbered items, or headings. While the final output should be a single response, for structured content such as infographics or charts, you can use line breaks to separate logical sections. Within these sections, a hyphen (-) can introduce items in a list-like fashion, but these items should still be phrased as descriptive sentences or phrases that contribute to the overall narrative description of the image's content and layout. +2. **Enrich visual details appropriately**: + - Determine whether the image contains text. If not, do not add any extraneous textual elements. + - When the original description lacks sufficient detail, supplement logically consistent environmental, lighting, texture, or atmospheric elements to enhance visual appeal. When the description is already rich, make only necessary adjustments. When it is overly verbose or redundant, condense while preserving the original intent. + - All added content must align stylistically and logically with existing information; never alter original concepts or content. + - Exercise restraint in simple scenes to avoid unnecessary elaboration. +3. **Never modify proper nouns**: Names of people, brands, locations, IPs, movie/game titles, slogans in their original wording, URLs, phone numbers, etc., must be preserved exactly as given. +4. **Fully represent all textual content**: + - If the image contains visible text, **enclose every piece of displayed text in English double quotation marks (" ")** to distinguish it from other content. + - Accurately describe the text's content, position, layout direction (horizontal/vertical/wrapped), font style, color, size, and presentation method (e.g., printed, embroidered, neon). + - If the prompt implies the presence of specific text or numbers (even indirectly), explicitly state the **exact textual/numeric content**, enclosed in double quotation marks. Avoid vague references like "a list" or "a roster"; instead, provide concrete examples without excessive length. + - If no text appears in the image, explicitly state: "The image contains no recognizable text." +5. **Clearly specify the overall artistic style**, such as realistic photography, anime illustration, movie poster, cyberpunk concept art, watercolor painting, 3D rendering, game CG, etc. +--- +## Subtask 1: Portrait Image Rewriting +When the image centers on a human subject, or if the prompt uses terms like "portrait" or "headshot" without a specified subject, you must describe a detailed human character and ensure the following: +1. **Define Subject's Identity and Physical Appearance** — explicitly state ethnicity, gender, and a specific age or narrow descriptive age range; describe overall face shape and distinct structural features; detail eyes, nose, and mouth; conclude with a precise expression. Define skin tone, texture, makeup application (eyeshadow, eyeliner, eyelashes, eyebrow shape, lipstick, blush, highlight) and any facial hair. +2. **Describe clothing, hairstyle, and accessories** — specify all garments, fabric textures, hair color/length/texture/style, and any accessories. +3. **Capture pose and action** — body posture, gaze and head position, hand and arm gestures. Ensure all poses are anatomically correct and physically plausible. +4. **Depict background and environment** — specific setting, background objects, lighting (direction, intensity, color temperature), weather, and overall mood. +5. **Note other object details** — for non-human items, describe quantity, color, material, position, and spatial relationship to the person. +6. **Recommended description flow**: subject's overall identity → clothing → hairstyle → facial details → pose → environment, but always prioritize a natural narrative. +7. **Maintain conciseness**: aim for around 200 words. +--- +## Subtask 2: Text-Containing Image Rewriting +When the image contains recognizable text, ensure the following: +1. **Faithfully reproduce all text content** — clearly specify location (sign, screen, clothing, packaging, poster, etc.); accurately transcribe all visible text including punctuation, capitalization, line breaks, and layout direction; describe font style, color, size, clarity, outlines/strokes/shadows. For non-English text, retain the original and specify the language. +2. **Describe the relationship between text and its carrier** — presentation method (printed, LED screen, neon, embroidered, graffiti); compositional role (title, slogan, brand logo, decoration); spatial relationship with people or other objects. +3. **Supplement environment and atmosphere** — scene type, lighting effect on readability, overall color tone and artistic style. +4. **In infographic/knowledge-based scenarios, supplement text appropriately** — provide concrete, specific text/numbers/labels (no vague placeholders like "a list"); if the user already supplied detailed text, adhere to it strictly. +--- +## Subtask 3: General Image Rewriting +When the image lacks human subjects or text, cover these elements: +1. **Core visual components** — subject type, quantity, form, color, material, state; spatial layering (foreground, midground, background); lighting and color (direction, contrast, dominant hues, highlights/reflections/shadows); surface textures. +2. **Scene and atmosphere** — setting type, time and weather, emotional tone. +3. **Visual relationships among multiple objects** — functional connections, dynamic interactions, scale and proportion. +--- +Based on the user's input, automatically determine the appropriate task category and output a single English image prompt that fully complies with the above specifications. **Do not explain, confirm, or add any extra responses—output only the rewritten prompt text.**""" + +_HIDREAM = """You are a Prompt Engineering Engine — a professional AI image-generation prompt engineer, and also a creative director with encyclopedic knowledge and visual directing ability. Your task is to analyze the user's original image request, reason out the implicit knowledge and the best visual scheme, and rewrite it into **an explicit, detailed English prompt that can be used directly for image generation**. + +## Core Objective + +Image generation models can only execute direct visual descriptions; they cannot supply background knowledge, logical relationships, or text content on their own. Therefore, you must complete knowledge parsing, spatial planning, and visual directing in advance, and write the results explicitly into the prompt. + +Use the SCALIST framework to expand every scene: +- **Subject**: identity, appearance, color, material, texture, action, expression, clothing of the subject. +- **Composition**: shot type, viewpoint, subject placement, foreground/midground/background layers, negative space, and visual focus. +- **Action**: what the subject is doing, direction of action, pose, interactions. +- **Location**: scene location, indoor/outdoor, era, weather, time of day, environmental details. +- **Image style**: photorealistic, cinematic, oil painting, watercolor, anime, 3D render, etc., matched with appropriate lighting and color mood. +- **Specs**: photography/rendering parameters such as 85mm lens, low-angle shot, shallow depth of field, soft diffused light, dramatic backlighting, matte texture, sharp focus. +- **Text rendering**: if the user requires text, place the exact text in English double quotation marks and specify font style, color, size, material, and precise position. + +1. **Resolve and externalize implicit knowledge**: poems, lyrics, quotes, formulas, historical figures, scientific concepts, landmarks, famous paintings, cultural symbols, historical events, UI layouts, or any real-world objects must first be resolved into concrete answers and visible features, then written into the prompt. Do not just write "Mona Lisa", "Dunkirk evacuation", or "freedom" — terms that require the model to interpret on its own. +2. **Spatial and logical anchoring**: rewrite vague relationships into explicit layouts, e.g. top-left corner, centered in the foreground, slightly behind the main subject, background out of focus, text aligned along the bottom edge. Do not use vague expressions like "next to", "some", or "nice-looking". +3. **Text typography precision**: any language (Chinese, English, formulas, multilingual) must be preserved verbatim inside quotation marks, e.g. "床前明月光,疑是地上霜.举头望明月,低头思故乡." or "E = mc²"; also specify font (calligraphy, serif, sans-serif, handwritten), color, material, and position. +4. **Real-world grounding**: if the user requests factually accurate content such as historical artifacts, weather phenomena, portraits, buildings, instrument panels, or app interfaces, use your internal knowledge to fill in accurate visual details. +5. **Concretize abstract concepts**: turn abstract words like "freedom, loneliness, futuristic, healing" into visible scenes, symbols, and atmospheres, e.g. flying birds, broken chains, vast skies, cool neon, soft morning light. + +## Examples (combined learning) + +- User says "Li Bai's 'Quiet Night Thoughts' written on a wall" — the prompt should write out the full Chinese poem and specify where on the old stone wall it appears, in elegant Chinese calligraphy. +- User says "the founders of classical mechanics" or "Einstein writing the mass-energy equation" — the prompt should resolve to Isaac Newton or Albert Einstein and describe their appearance, period clothing, blackboard, and the visible formula "E = mc²". +- User says "Mona Lisa", "Leaning Tower of Pisa", the character "福", or "Dunkirk evacuation" — the prompt should describe the corresponding visual features: mysterious smile and folded hands; tilted white marble bell tower with arcades; red background with gold/black calligraphic "福"; soldiers and boats on the 1940s beach awaiting evacuation. + +## Output requirements + +- The prompt must be a single coherent natural English paragraph, like a Creative Director's Brief — not a pile of keywords or "tag soup". +- Length is typically 80–220 words; simpler requests can be shorter, complex scenes longer. +- Lead with the most important subject and intent, then naturally unfold composition, action, location, style, technical specs, and text rendering. +- Use complete sentences, rich but precise adjectives, and photography/painting/design terminology. +- Do not include any expression that still requires the image model to reason further. +- The prompt must be self-contained — the image must be generatable from the prompt alone. + +## Execution steps + +1. **Analyze**: identify the core subject, user intent, text requirements, reference constraints, and any implicit knowledge to resolve. +2. **Reason**: choose the lighting, lens, angle, texture, style, spatial layout, and factual details most suitable for the scene. +3. **Rewrite**: output the final enhanced single English paragraph as the prompt. + +Output only the final English prompt — no JSON wrapper, no preamble, no explanation.""" + + +DEFAULT_SYSTEM_PROMPTS: list[tuple[str, str, str]] = [ + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0001", "FLUX.2 Prompt Enhancement", _FLUX2), + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0002", "HunyuanImage 3.0 Recaption Expert", _HUNYUAN), + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0003", "Qwen-Image Edit Enhancer", _QWEN_EDIT), + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0004", "Z-Image Visual Description Optimizer", _Z_IMAGE), + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0005", "Qwen-Image Multi-Category Rewriter", _QWEN_2512), + ("0f8f5b2e-1c9e-4f2a-9a4e-1f1f1f1f0006", "HiDream SCALIST Prompt Engineer", _HIDREAM), +] + + +class Migration32Callback: + def __call__(self, cursor: sqlite3.Cursor) -> None: + self._create_system_prompts_table(cursor) + self._seed_default_system_prompts(cursor) + + def _create_system_prompts_table(self, cursor: sqlite3.Cursor) -> None: + cursor.execute( + """--sql + CREATE TABLE IF NOT EXISTS system_prompts ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + content TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) + ); + """ + ) + cursor.execute( + """--sql + CREATE TRIGGER IF NOT EXISTS tg_system_prompts_updated_at + AFTER UPDATE + ON system_prompts FOR EACH ROW + BEGIN + UPDATE system_prompts SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') + WHERE id = old.id; + END; + """ + ) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_system_prompts_name ON system_prompts(name);") + + def _seed_default_system_prompts(self, cursor: sqlite3.Cursor) -> None: + cursor.executemany( + """--sql + INSERT OR IGNORE INTO system_prompts (id, name, content) + VALUES (?, ?, ?); + """, + DEFAULT_SYSTEM_PROMPTS, + ) + + +def build_migration_32() -> Migration: + """Build migration from database version 31 to 32. + + Creates the system_prompts table and seeds default prompts. + """ + return Migration( + from_version=31, + to_version=32, + callback=Migration32Callback(), + ) diff --git a/invokeai/app/services/system_prompt_records/__init__.py b/invokeai/app/services/system_prompt_records/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/app/services/system_prompt_records/system_prompt_records_base.py b/invokeai/app/services/system_prompt_records/system_prompt_records_base.py new file mode 100644 index 00000000000..8d9dd79e771 --- /dev/null +++ b/invokeai/app/services/system_prompt_records/system_prompt_records_base.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod + +from invokeai.app.services.system_prompt_records.system_prompt_records_common import ( + SystemPromptChanges, + SystemPromptRecordDTO, + SystemPromptWithoutId, +) + + +class SystemPromptRecordsStorageBase(ABC): + """Base class for system prompt storage services.""" + + @abstractmethod + def get(self, system_prompt_id: str) -> SystemPromptRecordDTO: + """Get system prompt by id.""" + pass + + @abstractmethod + def create(self, system_prompt: SystemPromptWithoutId) -> SystemPromptRecordDTO: + """Creates a system prompt.""" + pass + + @abstractmethod + def update(self, system_prompt_id: str, changes: SystemPromptChanges) -> SystemPromptRecordDTO: + """Updates a system prompt.""" + pass + + @abstractmethod + def delete(self, system_prompt_id: str) -> None: + """Deletes a system prompt.""" + pass + + @abstractmethod + def get_many(self) -> list[SystemPromptRecordDTO]: + """Gets all system prompts.""" + pass diff --git a/invokeai/app/services/system_prompt_records/system_prompt_records_common.py b/invokeai/app/services/system_prompt_records/system_prompt_records_common.py new file mode 100644 index 00000000000..83aba8cc89d --- /dev/null +++ b/invokeai/app/services/system_prompt_records/system_prompt_records_common.py @@ -0,0 +1,31 @@ +from datetime import datetime +from typing import Any, Optional + +from pydantic import BaseModel, Field, TypeAdapter + + +class SystemPromptNotFoundError(Exception): + """Raised when a system prompt is not found""" + + +class SystemPromptWithoutId(BaseModel, extra="forbid"): + name: str = Field(min_length=1, description="The name of the system prompt.") + content: str = Field(min_length=1, description="The system prompt content.") + + +class SystemPromptChanges(BaseModel, extra="forbid"): + name: Optional[str] = Field(default=None, min_length=1, description="The new name.") + content: Optional[str] = Field(default=None, min_length=1, description="The new content.") + + +class SystemPromptRecordDTO(SystemPromptWithoutId): + id: str = Field(description="The system prompt ID.") + created_at: datetime = Field(description="When the system prompt was created.") + updated_at: datetime = Field(description="When the system prompt was last updated.") + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "SystemPromptRecordDTO": + return SystemPromptRecordDTOValidator.validate_python(data) + + +SystemPromptRecordDTOValidator = TypeAdapter(SystemPromptRecordDTO) diff --git a/invokeai/app/services/system_prompt_records/system_prompt_records_sqlite.py b/invokeai/app/services/system_prompt_records/system_prompt_records_sqlite.py new file mode 100644 index 00000000000..cc25a33ad29 --- /dev/null +++ b/invokeai/app/services/system_prompt_records/system_prompt_records_sqlite.py @@ -0,0 +1,78 @@ +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase +from invokeai.app.services.system_prompt_records.system_prompt_records_base import ( + SystemPromptRecordsStorageBase, +) +from invokeai.app.services.system_prompt_records.system_prompt_records_common import ( + SystemPromptChanges, + SystemPromptNotFoundError, + SystemPromptRecordDTO, + SystemPromptWithoutId, +) +from invokeai.app.util.misc import uuid_string + + +class SqliteSystemPromptRecordsStorage(SystemPromptRecordsStorageBase): + def __init__(self, db: SqliteDatabase) -> None: + super().__init__() + self._db = db + + def get(self, system_prompt_id: str) -> SystemPromptRecordDTO: + with self._db.transaction() as cursor: + cursor.execute( + """--sql + SELECT * FROM system_prompts WHERE id = ?; + """, + (system_prompt_id,), + ) + row = cursor.fetchone() + if row is None: + raise SystemPromptNotFoundError(f"System prompt with id {system_prompt_id} not found") + return SystemPromptRecordDTO.from_dict(dict(row)) + + def create(self, system_prompt: SystemPromptWithoutId) -> SystemPromptRecordDTO: + system_prompt_id = uuid_string() + with self._db.transaction() as cursor: + cursor.execute( + """--sql + INSERT INTO system_prompts (id, name, content) + VALUES (?, ?, ?); + """, + (system_prompt_id, system_prompt.name, system_prompt.content), + ) + return self.get(system_prompt_id) + + def update(self, system_prompt_id: str, changes: SystemPromptChanges) -> SystemPromptRecordDTO: + with self._db.transaction() as cursor: + # Ensure the record exists so we can raise a clean 404 instead of silently no-op'ing. + cursor.execute("SELECT 1 FROM system_prompts WHERE id = ?;", (system_prompt_id,)) + if cursor.fetchone() is None: + raise SystemPromptNotFoundError(f"System prompt with id {system_prompt_id} not found") + + if changes.name is not None: + cursor.execute( + "UPDATE system_prompts SET name = ? WHERE id = ?;", + (changes.name, system_prompt_id), + ) + if changes.content is not None: + cursor.execute( + "UPDATE system_prompts SET content = ? WHERE id = ?;", + (changes.content, system_prompt_id), + ) + return self.get(system_prompt_id) + + def delete(self, system_prompt_id: str) -> None: + with self._db.transaction() as cursor: + cursor.execute( + "DELETE FROM system_prompts WHERE id = ?;", + (system_prompt_id,), + ) + + def get_many(self) -> list[SystemPromptRecordDTO]: + with self._db.transaction() as cursor: + cursor.execute( + """--sql + SELECT * FROM system_prompts ORDER BY LOWER(name) ASC; + """ + ) + rows = cursor.fetchall() + return [SystemPromptRecordDTO.from_dict(dict(row)) for row in rows] diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e7631ff6236..60e8ef86413 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -3195,6 +3195,22 @@ "incompatibleBaseModel": "Unsupported main model architecture for upscaling", "incompatibleBaseModelDesc": "Upscaling is supported for SD1.5 and SDXL architecture models only. Change the main model to enable upscaling." }, + "systemPrompts": { + "systemPrompt": "System Prompt", + "selectSystemPrompt": "Select system prompt...", + "manageSystemPrompts": "Manage system prompts", + "newSystemPrompt": "New System Prompt", + "editSystemPrompt": "Edit System Prompt", + "name": "Name", + "content": "Content", + "contentPlaceholder": "Enter the system prompt that instructs the LLM how to expand the user's prompt...", + "deletePrompt": "Delete System Prompt", + "deletePromptConfirm": "Are you sure you want to delete this system prompt? This cannot be undone.", + "promptDeleted": "System prompt deleted", + "unableToDeletePrompt": "Unable to delete system prompt", + "unableToSavePrompt": "Unable to save system prompt", + "noPromptsYet": "No system prompts yet. Create one to get started." + }, "stylePresets": { "active": "Active", "choosePromptTemplate": "Choose Prompt Template", diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx index e5ec5ccc565..88fd3404700 100644 --- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -19,6 +19,8 @@ import { DeleteStylePresetDialog } from 'features/stylePresets/components/Delete import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal'; import { VideosModal } from 'features/system/components/VideosModal/VideosModal'; +import { DeleteSystemPromptDialog } from 'features/systemPrompts/components/DeleteSystemPromptDialog'; +import { SystemPromptsModal } from 'features/systemPrompts/components/SystemPromptsModal'; import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog'; import { LoadWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog'; import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; @@ -44,6 +46,8 @@ export const GlobalModalIsolator = memo(() => { + + diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index f24d2d0105c..76756c2220d 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -36,6 +36,7 @@ import { nodesSliceConfig } from 'features/nodes/store/nodesSlice'; import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice'; import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice'; import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice'; +import { expandPromptSliceConfig } from 'features/prompt/store/expandPromptSlice'; import { queueSliceConfig } from 'features/queue/store/queueSlice'; import { stylePresetSliceConfig } from 'features/stylePresets/store/stylePresetSlice'; import { hotkeysSliceConfig } from 'features/system/store/hotkeysSlice'; @@ -71,6 +72,7 @@ const SLICE_CONFIGS = { [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig, + [expandPromptSliceConfig.slice.reducerPath]: expandPromptSliceConfig, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig, [hotkeysSliceConfig.slice.reducerPath]: hotkeysSliceConfig, [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig, @@ -103,6 +105,7 @@ const ALL_REDUCERS = { [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig.slice.reducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, + [expandPromptSliceConfig.slice.reducerPath]: expandPromptSliceConfig.slice.reducer, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer, [hotkeysSliceConfig.slice.reducerPath]: hotkeysSliceConfig.slice.reducer, [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer, diff --git a/invokeai/frontend/web/src/features/prompt/ExpandPromptButton.tsx b/invokeai/frontend/web/src/features/prompt/ExpandPromptButton.tsx index e0f035963a2..8090a8a7bec 100644 --- a/invokeai/frontend/web/src/features/prompt/ExpandPromptButton.tsx +++ b/invokeai/frontend/web/src/features/prompt/ExpandPromptButton.tsx @@ -1,7 +1,10 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import type { ComboboxOnChange, ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library'; import { Button, + Combobox, Flex, + FormControl, + FormLabel, IconButton, Popover, PopoverArrow, @@ -19,10 +22,18 @@ import { positivePromptChanged, selectPositivePrompt } from 'features/controlLay import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore'; import { ModelPicker } from 'features/parameters/components/ModelPicker'; import { setPromptUndo } from 'features/prompt/promptUndo'; +import { + selectedModelKeyChanged, + selectedSystemPromptIdChanged, + selectSelectedModelKey, + selectSelectedSystemPromptId, +} from 'features/prompt/store/expandPromptSlice'; +import { openSystemPromptsModal } from 'features/systemPrompts/store/systemPromptModal'; import { navigationApi } from 'features/ui/layouts/navigation-api'; -import { memo, useCallback, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiSparkleBold } from 'react-icons/pi'; +import { PiPencilSimpleBold, PiSparkleBold } from 'react-icons/pi'; +import { useListSystemPromptsQuery } from 'services/api/endpoints/systemPrompts'; import { useExpandPromptMutation } from 'services/api/endpoints/utilities'; import { useTextLLMModels } from 'services/api/hooks/modelsByType'; import type { AnyModelConfig } from 'services/api/types'; @@ -35,17 +46,62 @@ export const ExpandPromptButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const prompt = useAppSelector(selectPositivePrompt); + const selectedSystemPromptId = useAppSelector(selectSelectedSystemPromptId); + const selectedModelKey = useAppSelector(selectSelectedModelKey); const [modelConfigs] = useTextLLMModels(); const popover = useDisclosure(false); - const [selectedModel, setSelectedModel] = useState(undefined); + const { data: systemPrompts } = useListSystemPromptsQuery(); const [expandPrompt, { isLoading }] = useExpandPromptMutation(); const hasModels = modelConfigs.length > 0; - const handleModelChange = useCallback((model: AnyModelConfig) => { - setSelectedModel(model); + const selectedModel = useMemo( + () => modelConfigs.find((m) => m.key === selectedModelKey), + [modelConfigs, selectedModelKey] + ); + + const selectedSystemPrompt = useMemo( + () => systemPrompts?.find((p) => p.id === selectedSystemPromptId), + [systemPrompts, selectedSystemPromptId] + ); + + const systemPromptOptions = useMemo( + () => (systemPrompts ?? []).map((p) => ({ label: p.name, value: p.id })), + [systemPrompts] + ); + + const systemPromptValue = useMemo( + () => systemPromptOptions.find((o) => o.value === selectedSystemPromptId) ?? null, + [systemPromptOptions, selectedSystemPromptId] + ); + + // Auto-select the first prompt once the list loads if nothing is selected yet. + useEffect(() => { + if (selectedSystemPromptId === null && systemPrompts && systemPrompts.length > 0 && systemPrompts[0]) { + dispatch(selectedSystemPromptIdChanged(systemPrompts[0].id)); + } + }, [dispatch, selectedSystemPromptId, systemPrompts]); + + const handleModelChange = useCallback( + (model: AnyModelConfig) => { + dispatch(selectedModelKeyChanged(model.key)); + }, + [dispatch] + ); + + const handleSystemPromptChange = useCallback( + (option) => { + dispatch(selectedSystemPromptIdChanged(option?.value ?? null)); + }, + [dispatch] + ); + + const handleManagePrompts = useCallback(() => { + openSystemPromptsModal(); }, []); + const noOptionsMessage = useCallback(() => t('systemPrompts.noPromptsYet'), [t]); + const handleExpand = useCallback(async () => { if (!selectedModel || !prompt.trim()) { return; @@ -54,6 +110,7 @@ export const ExpandPromptButton = memo(() => { const result = await expandPrompt({ prompt, model_key: selectedModel.key, + system_prompt: selectedSystemPrompt?.content, }).unwrap(); if (result.expanded_prompt) { setPromptUndo(prompt); @@ -63,7 +120,7 @@ export const ExpandPromptButton = memo(() => { } catch { // Error is handled by RTK Query } - }, [selectedModel, prompt, expandPrompt, dispatch, popover]); + }, [selectedModel, prompt, expandPrompt, selectedSystemPrompt, dispatch, popover]); const handleOpenModelManager = useCallback(() => { popover.close(); @@ -95,7 +152,7 @@ export const ExpandPromptButton = memo(() => { - + {hasModels ? ( @@ -103,6 +160,32 @@ export const ExpandPromptButton = memo(() => { {t('prompt.expandPrompt')} + + + {t('systemPrompts.systemPrompt')} + + + + + + } + size="sm" + variant="ghost" + onClick={handleManagePrompts} + /> + + + + ; + +const getInitialState = (): ExpandPromptState => ({ + selectedSystemPromptId: null, + selectedModelKey: null, +}); + +const slice = createSlice({ + name: 'expandPrompt', + initialState: getInitialState(), + reducers: { + selectedSystemPromptIdChanged: (state, action: PayloadAction) => { + state.selectedSystemPromptId = action.payload; + }, + selectedModelKeyChanged: (state, action: PayloadAction) => { + state.selectedModelKey = action.payload; + }, + }, + extraReducers(builder) { + // If the selected prompt has been deleted on the server, clear the local selection + // so the picker doesn't show a stale ID. + builder.addMatcher(systemPromptsApi.endpoints.deleteSystemPrompt.matchFulfilled, (state, action) => { + if (state.selectedSystemPromptId === action.meta.arg.originalArgs) { + state.selectedSystemPromptId = null; + } + }); + builder.addMatcher(systemPromptsApi.endpoints.listSystemPrompts.matchFulfilled, (state, action) => { + if (state.selectedSystemPromptId === null) { + return; + } + const ids = action.payload.map((p) => p.id); + if (!ids.includes(state.selectedSystemPromptId)) { + state.selectedSystemPromptId = null; + } + }); + }, +}); + +export const { selectedSystemPromptIdChanged, selectedModelKeyChanged } = slice.actions; + +export const expandPromptSliceConfig: SliceConfig = { + slice, + schema: zExpandPromptState, + getInitialState, + persistConfig: { + migrate: (state) => { + assert(isPlainObject(state)); + if (!('_version' in state)) { + state._version = 1; + } + return zExpandPromptState.parse(state); + }, + }, +}; + +const selectExpandPromptSlice = (state: RootState) => state.expandPrompt; +const createExpandPromptSelector = (selector: Selector) => + createSelector(selectExpandPromptSlice, selector); + +export const selectSelectedSystemPromptId = createExpandPromptSelector((s) => s.selectedSystemPromptId); +export const selectSelectedModelKey = createExpandPromptSelector((s) => s.selectedModelKey); diff --git a/invokeai/frontend/web/src/features/systemPrompts/components/DeleteSystemPromptDialog.tsx b/invokeai/frontend/web/src/features/systemPrompts/components/DeleteSystemPromptDialog.tsx new file mode 100644 index 00000000000..c359d253f8e --- /dev/null +++ b/invokeai/frontend/web/src/features/systemPrompts/components/DeleteSystemPromptDialog.tsx @@ -0,0 +1,53 @@ +import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { toast } from 'features/toast/toast'; +import { atom } from 'nanostores'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { SystemPromptRecordDTO } from 'services/api/endpoints/systemPrompts'; +import { useDeleteSystemPromptMutation } from 'services/api/endpoints/systemPrompts'; + +const $promptToDelete = atom(null); +const clearPromptToDelete = () => $promptToDelete.set(null); + +export const useDeleteSystemPrompt = () => { + return useCallback((prompt: SystemPromptRecordDTO) => { + $promptToDelete.set(prompt); + }, []); +}; + +export const DeleteSystemPromptDialog = memo(() => { + useAssertSingleton('DeleteSystemPromptDialog'); + const { t } = useTranslation(); + const promptToDelete = useStore($promptToDelete); + const [_deleteSystemPrompt] = useDeleteSystemPromptMutation(); + + const deleteSystemPrompt = useCallback(async () => { + if (!promptToDelete) { + return; + } + try { + await _deleteSystemPrompt(promptToDelete.id).unwrap(); + toast({ status: 'success', title: t('systemPrompts.promptDeleted') }); + } catch { + toast({ status: 'error', title: t('systemPrompts.unableToDeletePrompt') }); + } + }, [promptToDelete, _deleteSystemPrompt, t]); + + return ( + + {t('systemPrompts.deletePromptConfirm')} + + ); +}); + +DeleteSystemPromptDialog.displayName = 'DeleteSystemPromptDialog'; diff --git a/invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptForm.tsx b/invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptForm.tsx new file mode 100644 index 00000000000..7d26cd1253d --- /dev/null +++ b/invokeai/frontend/web/src/features/systemPrompts/components/SystemPromptForm.tsx @@ -0,0 +1,93 @@ +import { Button, Flex, FormControl, FormLabel, Input, Spacer, Textarea } from '@invoke-ai/ui-library'; +import { showSystemPromptsList } from 'features/systemPrompts/store/systemPromptModal'; +import { toast } from 'features/toast/toast'; +import { memo, useCallback, useMemo } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { SystemPromptRecordDTO } from 'services/api/endpoints/systemPrompts'; +import { useCreateSystemPromptMutation, useUpdateSystemPromptMutation } from 'services/api/endpoints/systemPrompts'; + +type FormValues = { + name: string; + content: string; +}; + +type Props = { + /** + * The prompt to edit, or `null` when creating a new one. + */ + editing: SystemPromptRecordDTO | null; +}; + +export const SystemPromptForm = memo(({ editing }: Props) => { + const { t } = useTranslation(); + const [createSystemPrompt, { isLoading: isCreating }] = useCreateSystemPromptMutation(); + const [updateSystemPrompt, { isLoading: isUpdating }] = useUpdateSystemPromptMutation(); + + const defaultValues = useMemo( + () => ({ + name: editing?.name ?? '', + content: editing?.content ?? '', + }), + [editing] + ); + + const { register, handleSubmit, formState } = useForm({ + defaultValues, + mode: 'onChange', + }); + + const onSubmit = useCallback>( + async (data) => { + try { + if (editing) { + await updateSystemPrompt({ id: editing.id, changes: data }).unwrap(); + } else { + await createSystemPrompt(data).unwrap(); + } + showSystemPromptsList(); + } catch { + toast({ status: 'error', title: t('systemPrompts.unableToSavePrompt') }); + } + }, + [editing, updateSystemPrompt, createSystemPrompt, t] + ); + + const handleCancel = useCallback(() => { + showSystemPromptsList(); + }, []); + + return ( + + + {t('systemPrompts.name')} + + + + {t('systemPrompts.content')} +