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
| Layer | Technology | Entry Point |
|---|---|---|
| API | FastAPI | src/api/routes/images.py — /api/images/* |
| Service | Python (Pillow for thumbnails) | src/services/image_service.py |
| Agent tool | LangChain StructuredTool | src/services/image_tools.py |
| Placeholder substitution | Regex post-processor | src/utils/image_utils.py |
| Database model | SQLAlchemy | src/database/models/image.py |
| Frontend library | React + TypeScript | frontend/src/components/ImageLibrary/ |
| Frontend types | TypeScript | frontend/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_datastores raw bytes; base64 encoding happens on readoriginal_filenamemust be unique among active images (case-insensitive); soft-deleted images free their name for reusethumbnail_base64is a complete data URI ready for<img src=...>;Nonefor 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
| File | Responsibility | Key Details |
|---|---|---|
src/database/models/image.py | ORM model | ImageAsset — bytea storage, JSON tags, soft delete |
src/services/image_service.py | Upload, search, retrieve, delete | Validation (5MB, allowed types), Pillow thumbnails, base64 encoding |
src/services/image_tools.py | Agent tool wrapper | search_images — returns metadata JSON, never base64 |
src/utils/image_utils.py | Placeholder substitution | Regex {{image:(\d+)}} → data:{mime};base64,... |
src/api/routes/images.py | REST API | CRUD + base64 data endpoint |
src/api/services/chat_service.py | Integration 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.py | Prompt construction | Conditional IMAGE GUIDELINES section; search_images tool binding |
src/core/settings_db.py | Settings loader | Extracts image_guidelines from selected slide style |
src/api/routes/settings/slide_styles.py | Slide style CRUD | image_guidelines field in schemas and handlers |
frontend/src/components/ImageLibrary/ImageLibrary.tsx | Image gallery | Grid view, multi-file drag-drop upload, category filter, search |
frontend/src/components/ImageLibrary/ImagePicker.tsx | Modal picker | Wraps ImageLibrary with select callback |
frontend/src/components/ChatPanel/ChatInput.tsx | Paste-to-chat | Clipboard paste, upload, attach preview, "Save to library" toggle |
frontend/src/components/config/SlideStyleForm.tsx | Style editor | Separate Image Guidelines Monaco editor + Insert Image Ref button |
frontend/src/types/image.ts | TypeScript types | ImageAsset, ImageListResponse, ImageDataResponse |
frontend/src/services/api.ts | API client | uploadImage, listImages, updateImage, deleteImage |
State / Data Flow
1. Image upload
- User drops/selects one or more files in ImageLibrary (multi-file drag-drop and file picker supported), or pastes into ChatInput
- Frontend validates each file client-side (type + size), then sends one
POST /api/images/uploadper file (sequential, with progress indicator "Uploading 2 of 5...") image_service.upload_image()validates type, size, and unique filename (case-insensitive check against active images; rejects duplicates with 400)- Pillow generates 150x150 thumbnail (PNG for RGBA, JPEG otherwise;
Nonefor SVG) - Raw bytes + thumbnail + metadata saved to
image_assetstable - Response returns
ImageResponsewith thumbnail for immediate display - Per-file errors (validation failures, duplicate names) are collected and displayed together in the UI
2. Paste-to-chat
- User pastes image into ChatInput textarea (
onPastehandler) - Frontend uploads via
api.uploadImage()withsave_to_libraryflag - If
save_to_library = false, category is overridden to"ephemeral" - Uploaded
ImageAssetadded toattachedImagesstate - On send,
image_idsarray included inChatRequest ChatService._inject_image_context()appends image metadata to the user message text
3. Agent uses images in slides
- 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 - Agent receives message (possibly with
[Attached images]context) - Agent calls
search_imagestool if user explicitly requested images - Tool returns JSON metadata (id, filename, description, tags, usage example) — never base64
- Agent outputs HTML with
{{image:ID}}placeholders ChatService._substitute_images_for_response()converts placeholders to base64 data URIs at the API response boundary- Backend deck cache always stores the placeholder form; base64 only exists in API responses
- Frontend receives self-contained HTML with embedded images
4. Image guidelines (branding flow)
- 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) image_guidelinessaved toSlideStyleLibrary.image_guidelinescolumn- On settings load,
settings_db.load_settings_from_database()extractsimage_guidelinesintosettings.prompts["image_guidelines"] agent._create_prompt()checks ifimage_guidelinesis non-empty- If set: appends
IMAGE GUIDELINESsection to system prompt with the verbatim text; agent uses pre-validated IDs directly - If empty: agent only uses
search_imageswhen user explicitly requests images - After generation,
substitute_image_placeholders()resolves all{{image:ID}}references regardless of source
Interfaces / API Table
Image API (/api/images)
| Method | Path | Purpose | Request | Response |
|---|---|---|---|---|
| POST | /upload | Upload image | Multipart: file, tags (JSON), description, category, save_to_library | ImageResponse (201) |
| GET | / | List/search images | Query: category, query | ImageListResponse |
| GET | /{id} | Get image metadata | — | ImageResponse |
| GET | /{id}/data | Get full base64 data | — | ImageDataResponse |
| PUT | /{id} | Update metadata | JSON: tags, description, category | ImageResponse |
| DELETE | /{id} | Soft delete | — | 204 |
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
| Constraint | Value | Enforced in |
|---|---|---|
| Max file size | 5MB | image_service.py + ChatInput.tsx + ImageLibrary.tsx |
| Allowed MIME types | png, jpeg, gif, svg+xml | image_service.py + ImageLibrary.tsx |
| Unique filename | Case-insensitive among active images | image_service.py (server-side) |
| Thumbnail size | 150x150 | image_service.py |
Error handling
- Invalid type/size:
ValueErrorraised inimage_service, returned as 400 from API - Duplicate filename:
ValueErrorraised inimage_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
| Category | Purpose | Visible in library? |
|---|---|---|
branding | Logos, brand assets | Yes |
content | User-uploaded content images | Yes |
background | Slide backgrounds | Yes |
ephemeral | Paste-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
CATEGORIESinImageLibrary.tsx, updatesearch_imagestool description, and agent prompt if needed - Supporting new image formats: Add MIME type to
ALLOWED_TYPESin bothimage_service.pyandImageLibrary.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 usessvgpathtools+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_datacolumn causes performance issues with many images, adddeferred(image_data)to the model or use explicit column selection in search queries - Image guidelines format: The
image_guidelinesfield 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; existingimage_datacolumn can be updated in place
Cross-References
- Backend Overview — agent lifecycle, ChatService, request flow
- Frontend Overview — component layout, state management
- Real-time Streaming — SSE streaming through which chat responses (with images) are delivered
- Database Configuration — PostgreSQL/Lakebase setup,
create_allschema management - Slide Parser & Script Management — how HTML slides are parsed and managed after image substitution