Skip to main content

Image Upload & Management – Storing, serving, and embedding user images in slides

Images live entirely in PostgreSQL/Lakebase (no external storage). The agent references images via {{image:ID}} placeholders; post-processing substitutes them with base64 data URIs before the frontend receives HTML. A separate image guidelines field on slide styles controls automatic image injection without per-request tool calls.


Stack & Entry Points

LayerTechnologyEntry Point
APIFastAPIsrc/api/routes/images.py/api/images/*
ServicePython (Pillow for thumbnails)src/services/image_service.py
Agent toolLangChain StructuredToolsrc/services/image_tools.py
Placeholder substitutionRegex post-processorsrc/utils/image_utils.py
Database modelSQLAlchemysrc/database/models/image.py
Frontend libraryReact + TypeScriptfrontend/src/components/ImageLibrary/
Frontend typesTypeScriptfrontend/src/types/image.ts

Architecture Snapshot

User
├── Upload image(s) ──► POST /api/images/upload (one per file)
│ │
│ image_service.upload_image()
│ (validates type, size, unique name)
│ │
│ ┌────▼────┐
│ │Lakebase │ image_assets table
│ │ (bytea) │ raw bytes + 150x150 thumbnail
│ └────┬────┘
│ │
├── Chat message ──► ChatService
│ │
│ _replace_slide_htmls_from_cache()
│ (strips base64 from slide_context before LLM call)
│ │
│ LangChain Agent
│ ├── search_images tool (metadata only)
│ └── outputs {{image:ID}} placeholders
│ │
│ _substitute_images_for_response()
│ (regex replaces → base64 data URIs at API boundary)
│ │
└── Receives HTML ◄──────┘
with embedded images

Key Concepts / Data Contracts

ImageAsset model (src/database/models/image.py)

class ImageAsset(Base):
__tablename__ = "image_assets"

id = Column(Integer, primary_key=True)
filename = Column(String(255)) # "{uuid}.{ext}"
original_filename = Column(String(255)) # User's original name
mime_type = Column(String(50)) # image/png, image/jpeg, image/gif, image/svg+xml
size_bytes = Column(Integer)
image_data = Column(LargeBinary) # Raw bytes (max 5MB)
thumbnail_base64 = Column(Text) # "data:image/jpeg;base64,..." or None for SVG
tags = Column(JSON) # ["branding", "logo"]
description = Column(Text)
category = Column(String(50)) # 'branding', 'content', 'background', 'ephemeral'
uploaded_by = Column(String(255))
is_active = Column(Boolean) # Soft delete

Invariants:

  • image_data stores raw bytes; base64 encoding happens on read
  • original_filename must be unique among active images (case-insensitive); soft-deleted images free their name for reuse
  • thumbnail_base64 is a complete data URI ready for <img src=...>; None for SVGs (they scale natively)
  • category = "ephemeral" means paste-to-chat images not saved to library (excluded from default library view)
  • No foreign key to profiles — images are independent library items shared across all profiles

Placeholder format

The agent outputs {{image:ID}} in two contexts:

<!-- HTML img tag -->
<img src="{{image:42}}" alt="Company logo" />

<!-- CSS background -->
background-image: url('{{image:42}}');

Post-processing converts to:

<img src="data:image/png;base64,iVBOR..." alt="Company logo" />

Image guidelines (slide style field)

The SlideStyleLibrary model has an image_guidelines column (Text, nullable). When populated, this text is injected into the agent's system prompt as an IMAGE GUIDELINES section. The agent uses referenced image IDs directly without calling search_images. When empty, the agent only searches for images when the user explicitly asks.


Component Responsibilities

FileResponsibilityKey Details
src/database/models/image.pyORM modelImageAsset — bytea storage, JSON tags, soft delete
src/services/image_service.pyUpload, search, retrieve, deleteValidation (5MB, allowed types), Pillow thumbnails, base64 encoding
src/services/image_tools.pyAgent tool wrappersearch_images — returns metadata JSON, never base64
src/utils/image_utils.pyPlaceholder substitutionRegex {{image:(\d+)}}data:{mime};base64,...
src/api/routes/images.pyREST APICRUD + base64 data endpoint
src/api/services/chat_service.pyIntegration glue_replace_slide_htmls_from_cache strips base64 from inbound slide_context; _substitute_images_for_response adds base64 at API boundary; _inject_image_context for attached images
src/services/agent.pyPrompt constructionConditional IMAGE GUIDELINES section; search_images tool binding
src/core/settings_db.pySettings loaderExtracts image_guidelines from selected slide style
src/api/routes/settings/slide_styles.pySlide style CRUDimage_guidelines field in schemas and handlers
frontend/src/components/ImageLibrary/ImageLibrary.tsxImage galleryGrid view, multi-file drag-drop upload, category filter, search
frontend/src/components/ImageLibrary/ImagePicker.tsxModal pickerWraps ImageLibrary with select callback
frontend/src/components/ChatPanel/ChatInput.tsxPaste-to-chatClipboard paste, upload, attach preview, "Save to library" toggle
frontend/src/components/config/SlideStyleForm.tsxStyle editorSeparate Image Guidelines Monaco editor + Insert Image Ref button
frontend/src/types/image.tsTypeScript typesImageAsset, ImageListResponse, ImageDataResponse
frontend/src/services/api.tsAPI clientuploadImage, listImages, updateImage, deleteImage

State / Data Flow

1. Image upload

  1. User drops/selects one or more files in ImageLibrary (multi-file drag-drop and file picker supported), or pastes into ChatInput
  2. Frontend validates each file client-side (type + size), then sends one POST /api/images/upload per file (sequential, with progress indicator "Uploading 2 of 5...")
  3. image_service.upload_image() validates type, size, and unique filename (case-insensitive check against active images; rejects duplicates with 400)
  4. Pillow generates 150x150 thumbnail (PNG for RGBA, JPEG otherwise; None for SVG)
  5. Raw bytes + thumbnail + metadata saved to image_assets table
  6. Response returns ImageResponse with thumbnail for immediate display
  7. Per-file errors (validation failures, duplicate names) are collected and displayed together in the UI

2. Paste-to-chat

  1. User pastes image into ChatInput textarea (onPaste handler)
  2. Frontend uploads via api.uploadImage() with save_to_library flag
  3. If save_to_library = false, category is overridden to "ephemeral"
  4. Uploaded ImageAsset added to attachedImages state
  5. On send, image_ids array included in ChatRequest
  6. ChatService._inject_image_context() appends image metadata to the user message text

3. Agent uses images in slides

  1. If the frontend sends slide_context (for edits), ChatService._replace_slide_htmls_from_cache() swaps the frontend's base64-substituted HTML with lightweight {{image:ID}} placeholder versions from the backend deck cache — this prevents megabytes of base64 from entering the LLM prompt
  2. Agent receives message (possibly with [Attached images] context)
  3. Agent calls search_images tool if user explicitly requested images
  4. Tool returns JSON metadata (id, filename, description, tags, usage example) — never base64
  5. Agent outputs HTML with {{image:ID}} placeholders
  6. ChatService._substitute_images_for_response() converts placeholders to base64 data URIs at the API response boundary
  7. Backend deck cache always stores the placeholder form; base64 only exists in API responses
  8. Frontend receives self-contained HTML with embedded images

4. Image guidelines (branding flow)

  1. Admin edits a slide style, adds image guidelines in the dedicated Monaco editor (e.g. Place {{image:5}} as logo in top-right of every slide)
  2. image_guidelines saved to SlideStyleLibrary.image_guidelines column
  3. On settings load, settings_db.load_settings_from_database() extracts image_guidelines into settings.prompts["image_guidelines"]
  4. agent._create_prompt() checks if image_guidelines is non-empty
  5. If set: appends IMAGE GUIDELINES section to system prompt with the verbatim text; agent uses pre-validated IDs directly
  6. If empty: agent only uses search_images when user explicitly requests images
  7. After generation, substitute_image_placeholders() resolves all {{image:ID}} references regardless of source

Interfaces / API Table

Image API (/api/images)

MethodPathPurposeRequestResponse
POST/uploadUpload imageMultipart: file, tags (JSON), description, category, save_to_libraryImageResponse (201)
GET/List/search imagesQuery: category, queryImageListResponse
GET/{id}Get image metadataImageResponse
GET/{id}/dataGet full base64 dataImageDataResponse
PUT/{id}Update metadataJSON: tags, description, categoryImageResponse
DELETE/{id}Soft delete204

Slide Style API (image_guidelines field)

The image_guidelines field is included in all slide style CRUD operations at /api/settings/slide-styles. See src/api/routes/settings/slide_styles.py.

Agent tool

search_images(
query: Optional[str], # Search by filename or description
category: Optional[str], # 'branding', 'content', 'background'
tags: Optional[List[str]], # Filter by tags
) -> str # JSON: {message, images: [{id, filename, description, tags, category, mime_type, usage}]}

Operational Notes

Validation constraints

ConstraintValueEnforced in
Max file size5MBimage_service.py + ChatInput.tsx + ImageLibrary.tsx
Allowed MIME typespng, jpeg, gif, svg+xmlimage_service.py + ImageLibrary.tsx
Unique filenameCase-insensitive among active imagesimage_service.py (server-side)
Thumbnail size150x150image_service.py

Error handling

  • Invalid type/size: ValueError raised in image_service, returned as 400 from API
  • Duplicate filename: ValueError raised in image_service, returned as 400 from API; frontend shows per-file error messages
  • Image not found: 404 from API; placeholder substitution logs warning and leaves {{image:ID}} intact
  • Paste upload failure: Error shown in ChatInput UI, does not block message sending
  • Agent uses invalid ID: Placeholder stays in HTML (graceful degradation)
  • Multi-file upload partial failure: Successfully uploaded files are kept; per-file errors are collected and displayed together

Category semantics

CategoryPurposeVisible in library?
brandingLogos, brand assetsYes
contentUser-uploaded content imagesYes
backgroundSlide backgroundsYes
ephemeralPaste-to-chat (not saved)No (filtered by default)

Logging

  • Upload success/failure logged in image_service.py
  • Placeholder substitution warnings in image_utils.py
  • Attached image IDs logged in chat_service.py

Extension Guidance

  • Adding new image categories: Add to CATEGORIES in ImageLibrary.tsx, update search_images tool description, and agent prompt if needed
  • Supporting new image formats: Add MIME type to ALLOWED_TYPES in both image_service.py and ImageLibrary.tsx; update thumbnail generation if non-standard format
  • SVG export handling: SVG images from the library are automatically converted to PNG during PPTX and Google Slides export via _svg_to_png() in both converters. The conversion uses svgpathtools + Pillow (pure Python) to parse <path> elements and rasterize them. This is transparent to the user — SVGs are stored as-is in the database and only converted at export time.
  • Deferred column loading: If image_data column causes performance issues with many images, add deferred(image_data) to the model or use explicit column selection in search queries
  • Image guidelines format: The image_guidelines field is free-text — admins can use any format. The agent receives it verbatim. Consider adding structured validation if misuse becomes common
  • Adding image editing (crop/resize): Would go in image_service.py; existing image_data column can be updated in place

Cross-References