Skip to content

Conversation

@manuelkiessling
Copy link
Member

@manuelkiessling manuelkiessling commented Feb 10, 2026

Closes #90.

PhotoBuilder Feature Implementation Plan

Architecture Overview

New vertical src/PhotoBuilder/ following the existing vertical slice pattern, communicating with other verticals via facades.

graph LR
    PhotoBuilder -->|"readWorkspaceFile (dist HTML)"| WorkspaceMgmt
    PhotoBuilder -->|"getProjectInfo (API key, S3 config)"| ProjectMgmt
    PhotoBuilder -->|"uploadAsset (S3)"| RemoteContentAssets
    PhotoBuilder -->|"getAccountInfoByEmail"| Account
    ChatBasedContentEditor -.->|"CTA link in dist files"| PhotoBuilder
Loading

Facade dependencies to add to vertical-wiring.md:

  • PhotoBuilder -> WorkspaceMgmt: readWorkspaceFile (to get page HTML for prompt generation)
  • PhotoBuilder -> ProjectMgmt: getProjectInfo (LLM API key, S3 credentials, manifest URLs)
  • PhotoBuilder -> RemoteContentAssets: uploadAsset (upload generated images to S3 media store)
  • PhotoBuilder -> Account: getAccountInfoByEmail (user identity for access validation)

Vertical Structure

src/PhotoBuilder/
├── Domain/
│   ├── Entity/
│   │   ├── PhotoSession.php
│   │   └── PhotoImage.php
│   ├── Enum/
│   │   ├── PhotoSessionStatus.php
│   │   └── PhotoImageStatus.php
│   └── Service/
│       └── PhotoBuilderService.php
├── Infrastructure/
│   ├── Adapter/
│   │   ├── PromptGeneratorInterface.php
│   │   ├── OpenAiPromptGenerator.php
│   │   ├── ImageGeneratorInterface.php
│   │   └── OpenAiImageGenerator.php
│   ├── Handler/
│   │   ├── GenerateImagePromptsHandler.php
│   │   └── GenerateImageHandler.php
│   ├── Message/
│   │   ├── GenerateImagePromptsMessage.php
│   │   └── GenerateImageMessage.php
│   └── Storage/
│       └── GeneratedImageStorage.php
├── Presentation/
│   ├── Controller/
│   │   └── PhotoBuilderController.php
│   └── Resources/
│       ├── assets/controllers/
│       │   ├── photo_builder_controller.ts
│       │   └── photo_image_controller.ts
│       └── templates/
│           └── photo_builder.twig

1. Domain Layer

Entities

**PhotoSession** - tracks one photo generation session per page:

  • id (UUID, PK)
  • workspaceId (string)
  • conversationId (string) - to navigate back to the content editor
  • pagePath (string) - e.g. index.html
  • systemPrompt (text) - generated system prompt with page HTML baked in
  • userPrompt (text) - user-editable portion
  • status (enum)
  • createdAt (datetime)

**PhotoImage** - tracks each of the IMAGE_COUNT images:

  • id (UUID, PK)
  • session (ManyToOne -> PhotoSession)
  • position (int, 0 to IMAGE_COUNT-1)
  • prompt (text, nullable)
  • suggestedFileName (string, nullable) - descriptive name like cozy-cafe-winter-scene.jpg
  • status (enum: pending, generating, completed, failed)
  • storagePath (string, nullable) - relative path in var/photo-builder/
  • errorMessage (string, nullable)

Enums

**PhotoSessionStatus**: generating_prompts, prompts_ready, generating_images, images_ready, failed

**PhotoImageStatus**: pending, generating, completed, failed

Service

**PhotoBuilderService** orchestrates:

  • Defines IMAGE_COUNT = 5 as a public constant — the single source of truth for how many images are generated per session. Referenced by the prompt generator (system prompt text, expected tool call count), the session creation logic, and passed to the frontend via the controller.
  • Creating sessions with IMAGE_COUNT empty PhotoImage slots
  • Updating prompts from LLM output (delivered via agent tool calls)
  • Coordinating status transitions

2. Infrastructure Layer

Prompt Generation (NeuronAI Agent with Tool)

**OpenAiPromptGenerator** - Uses NeuronAI Agent with OpenAI provider (same as existing content editor pattern, see ContentEditorAgent.php):

  • System prompt: includes the page HTML and instructs the model to generate IMAGE_COUNT image prompts by calling the deliver_image_prompt tool once per image
  • User prompt: the user-provided prompt (default: "The generated images should convey professionalism and competence." / German equivalent)
  • Tool-based delivery instead of JSON parsing: the agent is given a deliver_image_prompt tool with two required string parameters:
    • prompt: the image generation prompt text
    • file_name: a descriptive slug filename (e.g. cozy-cafe-winter-scene.jpg)
  • The tool's callable collects each invocation into an in-memory list. After the agent completes, the list is returned as the result — one entry per image, in order of tool calls.
  • This approach is more robust than parsing free-form JSON: tool call schemas are enforced by the API, eliminating risks of malformed output, missing fields, or markdown wrapping.

Image Generation (Direct HTTP)

**OpenAiImageGenerator** - Direct HTTP client call to OpenAI Images API (not via NeuronAI, which doesn't support image gen):

  • Endpoint: POST https://api.openai.com/v1/images/generations
  • Model: gpt-image-1 (or dall-e-3 as fallback)
  • Response format: b64_json to get image data directly (avoids relying on temporary URLs)
  • Saves the decoded image to disk via GeneratedImageStorage
  • Returns both the storage path and the LLM-suggested filename

Async Handlers (Messenger)

Following the pattern in RunEditSessionHandler.php:

**GenerateImagePromptsHandler**:

  1. Loads PhotoSession from DB
  2. Reads page HTML via WorkspaceMgmtFacade::readWorkspaceFile()
  3. Gets API key via ProjectMgmtFacade::getProjectInfo()
  4. Calls OpenAiPromptGenerator which runs the agent with the deliver_image_prompt tool; collects IMAGE_COUNT prompt+fileName pairs
  5. Updates each PhotoImage entity with prompt + suggestedFileName from the tool call results
  6. Sets session status to prompts_ready
  7. Dispatches one GenerateImageMessage per image
  8. Flushes to DB

**GenerateImageHandler**:

  1. Loads PhotoImage from DB
  2. Sets status to generating
  3. Calls OpenAiImageGenerator with the prompt
  4. Saves image to disk, updates storagePath and status to completed
  5. On failure: sets status to failed with error message
  6. Checks if all images in the session are done -> updates session status to images_ready

Image Storage

**GeneratedImageStorage** - simple filesystem adapter:

  • Base dir: %kernel.project_dir%/var/photo-builder/
  • Path pattern: {sessionId}/{position}.png
  • Methods: save(sessionId, position, imageData): string, read(storagePath): string, getAbsolutePath(storagePath): string

3. Presentation Layer

Controller

**PhotoBuilderController** with these routes:

Method Route Purpose
GET /photo-builder/{workspaceId} Render the PhotoBuilder page
POST /api/photo-builder/sessions Create session, start prompt generation
GET /api/photo-builder/sessions/{sessionId} Poll session status (prompts + images)
POST /api/photo-builder/sessions/{sessionId}/regenerate-prompts Regenerate prompts with updated user prompt
POST /api/photo-builder/images/{imageId}/regenerate Regenerate a single image
POST /api/photo-builder/images/{imageId}/update-prompt Update prompt for a single image
GET /api/photo-builder/images/{imageId}/file Serve generated image file
POST /api/photo-builder/images/{imageId}/upload-to-media-store Upload to S3

Page render (GET /photo-builder/{workspaceId}):

  • Query param: page (required), conversationId (required)
  • Loads workspace, project, validates access
  • Renders photo_builder.twig with config values for the Stimulus controller
  • Conditionally passes S3/media store config based on project.hasS3UploadConfigured()

Session poll (GET /api/photo-builder/sessions/{sessionId}):

  • Returns JSON:
{
  "status": "images_ready",
  "userPrompt": "...",
  "images": [
    {
      "id": "uuid",
      "position": 0,
      "prompt": "A wide-angle photograph...",
      "suggestedFileName": "modern-office-team.jpg",
      "status": "completed",
      "imageUrl": "/api/photo-builder/images/uuid/file",
      "errorMessage": null
    }
  ]
}

Twig Template (photo_builder.twig)

Layout:

  • Page header with "Back to editor" link
  • Working area with Stimulus controller photo-builder:
    • Loading overlay (shown during prompt generation): skeleton/spinner animation
    • User prompt section: textarea + "Regenerate image prompts" button
    • Image grid: IMAGE_COUNT cards in a responsive grid (CSS grid, 1-2-3 cols depending on breakpoint); count passed from controller as Stimulus value
    • Each card: image preview (or generating placeholder), prompt textarea, "Keep prompt" checkbox, "Regenerate image" button, "Upload to media store" button (conditional)
    • Media store sidebar (conditional): reuse the remote-asset-browser Stimulus controller pattern from chat_based_content_editor.twig lines 381-459
    • Footer: "Embed generated images into content page" CTA (conditional on S3)

Stimulus Controllers (multi-controller design)

The frontend is split across two custom controllers plus the existing remote-asset-browser, communicating via Stimulus events (same pattern as dist-files -> html-editor and remote-asset-browser -> chat-based-content-editor).

graph TB
    subgraph twig [photo_builder.twig]
        PB["photo-builder (orchestrator)"]
        PI1["photo-image #0"]
        PI2["photo-image #1"]
        PI3["photo-image ..."]
        RAB["remote-asset-browser (reused)"]
    end

    PI1 -->|"photo-image:promptEdited"| PB
    PI1 -->|"photo-image:regenerateRequested"| PB
    PI1 -->|"photo-image:uploadRequested"| PB
    PI2 -->|"photo-image:*"| PB
    PI3 -->|"photo-image:*"| PB
    RAB -->|"remote-asset-browser:uploadComplete"| PB
    PB -->|"photo-builder:stateChanged (on child el)"| PI1
    PB -->|"photo-builder:stateChanged (on child el)"| PI2
Loading

1. photo_builder_controller.ts (page orchestrator)

Mounted on the outermost page container. Manages session lifecycle and global state.

Values (from Twig):

  • createSessionUrl, csrfToken, workspaceId, pagePath, conversationId
  • editorUrl (URL to content editor for "embed" CTA navigation)
  • pollUrl (pattern with {sessionId} placeholder)
  • regeneratePromptsUrl (pattern with {sessionId} placeholder)
  • imageCount (from PhotoBuilderService::IMAGE_COUNT, passed by controller)

Targets:

  • loadingOverlay, mainContent
  • userPrompt (textarea)
  • regeneratePromptsButton, embedButton
  • imageCard (multiple — one per image, wrapping each photo-image controller)

Lifecycle:

  1. connect(): POST to createSessionUrl to create session, show loading overlay, start polling
  2. Poll session status every 1s (non-overlapping setTimeout per existing convention)
  3. On each poll response: dispatch photo-builder:stateChanged event on each imageCard target element with that image's data
  4. When prompts_ready and no images generating yet: auto-trigger image generation for all
  5. Track global "any image generating?" state to enable/disable all action CTAs

Event listeners (wired via stimulus_action in Twig):

  • photo-image:promptEdited -> handlePromptEdited(): track which images have user-edited prompts
  • photo-image:regenerateRequested -> handleRegenerateImage(): POST to regenerate single image, update global state
  • photo-image:uploadRequested -> handleUploadToMediaStore(): POST to upload image to S3
  • remote-asset-browser:uploadComplete -> handleMediaStoreUploadComplete(): refresh asset list

Key actions:

  • "Regenerate image prompts" button: POST to regeneratePromptsUrl with userPrompt + list of kept image IDs (collected from photo-image children), update global state
  • "Embed into content page" button: navigate to editorUrl with pre-filled message query param containing suggested filenames

2. photo_image_controller.ts (per image card)

Mounted on each image card element within the grid. IMAGE_COUNT instances created by Twig loop. Manages individual image presentation and user interaction.

Values (from Twig):

  • position (int)
  • regenerateUrl (pattern with {imageId} placeholder)
  • uploadUrl (pattern with {imageId} placeholder)
  • hasMediaStore (boolean)

Targets:

  • image (img element), placeholder (generating animation)
  • promptTextarea
  • keepCheckbox
  • regenerateButton, uploadButton
  • statusBadge

Internal state (tracked in controller, not Stimulus values):

  • imageId (set when first poll data arrives)
  • currentStatus (pending/generating/completed/failed)
  • keepPrompt (boolean, auto-set on prompt edit)

Event listeners:

  • photo-builder:stateChanged (dispatched by parent on this element) -> updateFromState(): receives image data (id, prompt, suggestedFileName, status, imageUrl, errorMessage), updates all targets accordingly

Dispatched events (bubble up to parent via this.dispatch()):

  • promptEdited -> becomes photo-image:promptEdited with { position, prompt } detail
  • regenerateRequested -> becomes photo-image:regenerateRequested with { position, imageId, prompt } detail
  • uploadRequested -> becomes photo-image:uploadRequested with { position, imageId, suggestedFileName } detail

Key behaviors:

  • Prompt textarea input event: auto-check "Keep prompt" checkbox, dispatch promptEdited
  • Show image when status === 'completed', show animated placeholder when status === 'generating'
  • Disable regenerate/upload buttons when parent signals generation is in progress (via a CSS class or data attribute toggled by parent)

3. remote-asset-browser (existing, reused as-is)

Embedded in the sidebar, same as in the content editor. Wired to the photo-builder orchestrator via stimulus_action in Twig for the uploadComplete event.

Twig Wiring Example

<div {{ stimulus_controller('photo-builder', { ... }) }}
     {{ stimulus_action('photo-builder', 'handlePromptEdited', 'photo-image:promptEdited') }}
     {{ stimulus_action('photo-builder', 'handleRegenerateImage', 'photo-image:regenerateRequested') }}
     {{ stimulus_action('photo-builder', 'handleUploadToMediaStore', 'photo-image:uploadRequested') }}>

    {# Loading overlay #}
    <div {{ stimulus_target('photo-builder', 'loadingOverlay') }}>...</div>

    {# Main content (hidden until prompts ready) #}
    <div {{ stimulus_target('photo-builder', 'mainContent') }} class="hidden">
        {# User prompt section #}
        <textarea {{ stimulus_target('photo-builder', 'userPrompt') }}>...</textarea>
        <button {{ stimulus_target('photo-builder', 'regeneratePromptsButton') }}>...</button>

        {# Image grid - IMAGE_COUNT cards rendered by Twig loop #}
        {% for i in range(0, imageCount - 1) %}
            <div {{ stimulus_controller('photo-image', { position: i, ... }) }}
                 {{ stimulus_target('photo-builder', 'imageCard') }}
                 {{ stimulus_action('photo-image', 'updateFromState', 'photo-builder:stateChanged') }}>
                {# image, prompt textarea, keep checkbox, buttons #}
            </div>
        {% endfor %}

        {# Embed CTA #}
        <button {{ stimulus_target('photo-builder', 'embedButton') }}>...</button>
    </div>

    {# Media store sidebar (conditional) #}
    <div {{ stimulus_controller('remote-asset-browser', { ... }) }}
         {{ stimulus_action('photo-builder', 'handleMediaStoreUploadComplete', 'remote-asset-browser:uploadComplete') }}>
        ...
    </div>
</div>

4. Content Editor Integration

Modify dist_files_controller.ts

Add a new Stimulus value:

  • photoBuilderUrlPattern (string) - e.g. /en/photo-builder/{workspaceId}?page={pagePath}&conversationId={conversationId}

In renderFiles(), add a camera/image icon CTA next to each file (between edit and preview links), only when not in readOnly mode. This CTA links to the PhotoBuilder page for that file.

Modify chat_based_content_editor.twig

Pass the new photoBuilderUrlPattern value to the dist-files Stimulus controller, and pass conversationId.

Pre-filled Chat Message

When navigating back from PhotoBuilder to the Content Editor, append a query parameter like ?prefill=Embed images a.jpg, b.jpg... into page x.html. The content editor controller reads this and pre-fills the instruction textarea. This requires a small change to ChatBasedContentEditorController::show() and the chat Stimulus controller.

5. Cross-Cutting Concerns (from documentation books)

Access Control and Organization Scoping

Per orgbook.md, projects are scoped to organizations. The PhotoBuilderController must:

  • Use #[IsGranted('ROLE_USER')] on all routes
  • Verify the authenticated user has access to the workspace/project (same pattern as ChatBasedContentEditorController — load workspace, load project, verify the user's organization matches)
  • The PhotoBuilder page is entered from an active conversation, so the workspace is already IN_CONVERSATION. The PhotoBuilder does not change workspace status — it's a side activity within the existing conversation.

Workspace Status: No Lock Needed

Per conversationbook.md, a workspace has at most one ONGOING conversation at a time. The PhotoBuilder operates within this existing conversation context (the conversationId is passed as a query parameter). It reads workspace files but does not modify them, so no additional workspace locking is required.

Reading Page HTML: dist/ not src/

Per workspace-isolation.md, workspace file access goes through WorkspaceMgmtFacade::readWorkspaceFile() with secure path validation. The PhotoBuilder reads from the dist/ directory (the built/rendered HTML), not src/, because the rendered page content is what the image generation agent needs to see.

Messenger Transport

Per conversationbook.md section 4.1, RunEditSessionMessage uses the "immediate" Symfony Messenger transport. The PhotoBuilder messages (GenerateImagePromptsMessage, GenerateImageMessage) should use the same transport. This needs to be configured in config/packages/messenger.yaml.

LLM Wire Logging

Per llm-logging-book.md, the LLM logging system captures wire-level HTTP traffic and semantic conversation events. The OpenAiPromptGenerator NeuronAI agent should support the same logging infrastructure:

  • Accept an optional $wireLogger parameter (same as ContentEditorAgent)
  • When enabled, attach the Guzzle HandlerStack middleware for wire logging
  • Register LlmConversationLogObserver for conversation-level logging
  • This ensures prompt generation calls are debuggable with mise run conversation-log

DateAndTimeService

Per archbook.md, always use DateAndTimeService instead of new DateTimeImmutable() for entity timestamps. The PhotoSession and PhotoImage entities must use this service.

Multibyte-Safe Strings

Per archbook.md, use mb_* functions for string operations where applicable (e.g., truncating prompt text for display, sanitizing filenames).

CSRF Protection

Per frontendbook.md section 3.6, all POST endpoints need CSRF validation. The controller should:

  • Generate a CSRF token in the Twig template (e.g., csrf_token('photo_builder'))
  • Pass it to the Stimulus controller as a value
  • Validate on all POST endpoints (isCsrfTokenValid)

No Named Arguments

Per .cursor/rules/02-code-standards.mdc, never use named arguments in PHP function/method calls.

6. Configuration and Infrastructure

Database Migration

New tables: photo_sessions, photo_images. Generate via mise run console make:migration.

Architecture Test

Add PhotoBuilder to the feature boundaries test in FeatureBoundariesArchTest.php (automatic since it scans src/).

Translations

Add keys to translations/messages.en.yaml and translations/messages.de.yaml:

  • photo_builder.title, photo_builder.back_to_editor
  • photo_builder.loading, photo_builder.generating_prompts, photo_builder.generating_image
  • photo_builder.user_prompt_label, photo_builder.regenerate_prompts
  • photo_builder.keep_prompt, photo_builder.regenerate_image, photo_builder.upload_to_media_store
  • photo_builder.embed_into_page, photo_builder.default_user_prompt
  • editor.generate_matching_images (for the CTA on dist files list)

Asset Registration

Register both new Stimulus controllers (photo-builder, photo-image) in assets/bootstrap.ts.

Add the new assets directory to config/packages/asset_mapper.yaml — both under framework.asset_mapper.paths and sensiolabs_typescript.source_dir (per frontendbook.md section 2.1-2.2).

Messenger Configuration

Add routing for new messages in config/packages/messenger.yaml:

App\PhotoBuilder\Infrastructure\Message\GenerateImagePromptsMessage: immediate
App\PhotoBuilder\Infrastructure\Message\GenerateImageMessage: immediate

Frontend Tests

Add Vitest tests for the new Stimulus controllers in tests/frontend/.

PHP Tests

  • Unit tests for PhotoBuilderService, GeneratedImageStorage
  • Integration tests for handlers
  • Architecture tests (auto-covered by existing boundary test)

Made with Cursor

… unit tests

New vertical for AI image generation matching web page content:
- Domain: PhotoSession/PhotoImage entities, enums, PhotoBuilderService with IMAGE_COUNT constant
- Infrastructure: PromptGenerator (NeuronAI agent with deliver_image_prompt tool),
  ImageGenerator (OpenAI Images API), GeneratedImageStorage, Messenger messages/handlers
- Tests: 45 unit tests covering entities, service logic, storage, and image generator

Co-authored-by: Cursor <cursoragent@cursor.com>
@manuelkiessling manuelkiessling marked this pull request as draft February 10, 2026 10:57
@manuelkiessling manuelkiessling self-assigned this Feb 10, 2026
@manuelkiessling manuelkiessling added the enhancement New feature or request label Feb 10, 2026
manuelkiessling and others added 17 commits February 10, 2026 12:19
…translations

- PhotoBuilderController with all API endpoints (create session, poll, regenerate,
  serve image, upload to media store)
- Twig template with loading state, user prompt, responsive image grid, media store sidebar
- Two Stimulus controllers: photo_builder_controller.ts (orchestrator) and
  photo_image_controller.ts (per-card state management)
- EN+DE translations for all PhotoBuilder UI strings
- ImagePromptResultDto to replace associative arrays at boundaries
- Registered new controllers in bootstrap.ts and asset_mapper.yaml
- Service wiring in services.yaml, Twig namespace in twig.yaml
- All quality checks pass (PHPStan, ESLint, tsc, Prettier, PHP CS Fixer)

Co-authored-by: Cursor <cursoragent@cursor.com>
…s for PhotoBuilder

- Wire PhotoBuilder CTA (camera icon) into dist_files_controller for each page
- Add prefillMessage support to chat-based-content-editor controller for the
  "Embed generated images into content page" flow
- Register PhotoBuilder entities in doctrine.yaml and generate migration
  for photo_sessions and photo_images tables
- Add Vitest tests for photo_builder_controller (23 tests) and
  photo_image_controller (25 tests)
- Add tests for PhotoBuilder CTA in dist_files_controller (5 tests) and
  prefillMessage in chat_based_content_editor_controller (3 tests)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ms, image serving

- Replace invalid placeholder strings (___SESSION_ID___) in Twig template
  with dummy UUIDs that satisfy Symfony route parameter requirements
- Use output_format instead of response_format for gpt-image-1 API
  (response_format is a dall-e-2/dall-e-3 parameter)
- Generate image URLs via Symfony router to include locale prefix,
  fixing broken image display due to missing /{_locale}/ in path
- Update vertical-wiring.md with PhotoBuilder facade dependencies
- Update corresponding unit and frontend tests

Co-authored-by: Cursor <cursoragent@cursor.com>
…dback, TestHarness

- Use etfswui-* styleguide classes on PhotoBuilder page (buttons, cards, forms)
- Add cursor-pointer to all CTAs via styleguide button classes
- Extract Remote Assets sidebar to @common.presentation/_remote_asset_browser_sidebar.html.twig
- Include shared partial in chat_based_content_editor and photo_builder
- Show 'Upload has been finished' banner on PhotoBuilder when upload completes (auto-hide 5s)
- Add PhotoBuilder TestHarness: FakePromptGenerator, FakeImageGenerator, env toggles
- PHOTO_BUILDER_SIMULATE_IMAGE_PROMPT_GENERATION and PHOTO_BUILDER_SIMULATE_IMAGE_GENERATION in .env
- Fix OpenAI image API (output_format for gpt-image-1), poll image URLs, Stimulus action wiring
- IMAGE_COUNT=1 for faster testing; docs/frontendbook.md and vertical-wiring.md updates

Co-authored-by: Cursor <cursoragent@cursor.com>
…er query params

- Show 'Upload has been finished' banner when image-card upload succeeds (not only sidebar)
- Regenerate prompts: overlay + spinner, clear unprotected prompt textareas on start
- Hide overlay when poll returns generating state; add regenerating_prompts translation
- Language switcher: preserve query string (page, conversationId) when switching locale on photo builder

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add uploadedToMediaStoreAt to PhotoImage to track S3 uploads
- Persist upload state in uploadToMediaStore endpoint; idempotent when already uploaded
- Include uploadedToMediaStore in poll response
- Change embedIntoPage to async: upload non-uploaded images first, show
  'Uploading images, please wait...' overlay, then navigate on success
- Add translations for uploading_images (EN/DE)
- Reset uploadedToMediaStoreAt when image is regenerated

Co-authored-by: Cursor <cursoragent@cursor.com>
…prompt

- Add uploadedFileName to PhotoImage for hash-prefixed S3 names in embed message
- Pass keepImageIds from regenerate prompts to handler; skip regenerating kept images
- Only dispatch image generation for changed prompts, not kept ones
- Clear uploaded state when prompt is regenerated

Co-authored-by: Cursor <cursoragent@cursor.com>
Dispatch clearPromptIfNotKept event on each child card element instead
of the parent — DOM events bubble upward, so dispatching on the parent
never reached child controllers. Also show "Generating..." text with
pulse animation immediately on non-kept prompts, disable buttons during
regeneration, and document the parent-to-child event pattern in
frontendbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ider configuration

Introduce a two-tier LLM configuration system: content editing (OpenAI-only)
and PhotoBuilder (OpenAI or Google Gemini). Projects can either reuse content
editing settings for image generation or configure a dedicated provider/key.

- Rename llmApiKey/llmModelProvider to contentEditing* scope across entity,
  DTOs, facades, controllers, templates, and tests
- Add nullable photoBuilder* LLM fields with fallback to content editing
- Extend LlmModelProvider enum with Google case and model selection methods
- Extend LlmModelName enum with gpt-image-1, gemini-3-pro-preview,
  gemini-3-pro-image-preview
- Implement GeminiImageGenerator adapter and ImageGeneratorFactory
- Parameterize ImagePromptAgent to support both OpenAI and Gemini providers
- Add Google API key verification via Gemini models endpoint
- Add PhotoBuilder LLM settings UI (Option A: reuse / Option B: dedicated)
  with provider selection, key input, verification, and one-click reuse
- Display active provider and model names on PhotoBuilder page
- Add docs/llm-usage-book.md documenting all LLM concerns and configuration

Co-authored-by: Cursor <cursoragent@cursor.com>
The Stimulus controller searched for the provider radio only within
its own element, missing sibling radios in the same fieldset.  This
caused Google Gemini keys to be verified against OpenAI, always
failing.  Widen the lookup scope to the closest fieldset/form ancestor.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nly)

Lo-res mode (1K, default) enables faster iteration; hi-res mode (2K)
produces higher quality output. The toggle is only shown when the
effective PhotoBuilder provider is Google Gemini, since OpenAI always
generates 1024x1024. Switching modes re-generates all images client-side
using current prompts at the new resolution without a page reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove fixed container_name to allow scaling, add deploy.replicas: 5.

Co-authored-by: Cursor <cursoragent@cursor.com>
After uploading images to S3 via the "Embed into page" action, poll
the remote asset manifests until all uploaded filenames are confirmed
available before redirecting. This prevents the content editor from
referencing images that haven't propagated to the CDN yet.

- Add findAvailableFileNames() to RemoteContentAssetsFacade (basename
  matching against merged manifests) so the logic stays in the
  RemoteContentAssets vertical
- Add thin POST endpoint in PhotoBuilderController that delegates to
  the facade and returns { available, allAvailable }
- Frontend polls every 3s for up to 90s, showing a spinner overlay
- Includes PHP unit tests, frontend tests, and EN/DE translations

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the faster and cheaper Flash model for generating image prompts
in PhotoBuilder when the Google provider is selected. Pro remains
the main text model for content editing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: PhotoBuilder

1 participant