Merge pull request #361 from presenton/feat/option-to-disable-image-generation

feat/option to disable image generation
This commit is contained in:
Saurav Niraula 2025-11-28 00:49:24 +05:45 committed by GitHub
commit 18bfdbf19b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 327 additions and 201 deletions

View file

@ -24,7 +24,7 @@ ENV TEMP_DIRECTORY=/tmp/presenton
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
# RUN curl -fsSL http://ollama.com/install.sh | sh
RUN curl -fsSL http://ollama.com/install.sh | sh
# Install dependencies for FastAPI
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \

100
README.md
View file

@ -14,16 +14,13 @@
# Open-Source AI Presentation Generator and API (Gamma, Beautiful AI, Decktopus Alternative)
**Presenton** is an open-source application for generating presentations with AI — all running locally on your device. Stay in control of your data and privacy while using models like OpenAI and Gemini, or use your own hosted models through Ollama.
__✨ Now, generate presentations with your existing PPTX file! Just upload your presentation file to create template design and then use that template to generate on brand and on design presentation on any topic.__
**✨ Now, generate presentations with your existing PPTX file! Just upload your presentation file to create template design and then use that template to generate on brand and on design presentation on any topic.**
![Demo](readme_assets/demo.gif)
> [!NOTE]
> **Enterprise Inquiries:**
> [!NOTE] > **Enterprise Inquiries:**
> For enterprise use, custom deployments, or partnership opportunities, contact us at **[suraj@presenton.ai](mailto:suraj@presenton.ai)**.
> [!IMPORTANT]
@ -32,28 +29,28 @@ __✨ Now, generate presentations with your existing PPTX file! Just upload your
> [!TIP]
> For detailed setup guides, API documentation, and advanced configuration options, visit our **[Official Documentation](https://docs.presenton.ai)**
## ✨ More Freedom with AI Presentations
Presenton gives you complete control over your AI presentation workflow. Choose your models, customize your experience, and keep your data private.
***Custom Templates & Themes** — Create unlimited presentation designs with HTML and Tailwind CSS
***AI Template Generation** — Create presentation templates from existing Powerpoint documents.
***Flexible Generation** — Build presentations from prompts or uploaded documents
***Export Ready** — Save as PowerPoint (PPTX) and PDF with professional formatting
***Built-In MCP Server** — Generate presentations over Model Context Protocol
***Bring Your Own Key** — Use your own API keys for OpenAI, Google Gemini, Anthropic Claude, or any compatible provider. Only pay for what you use, no hidden fees or subscriptions.
***Ollama Integration** — Run open-source models locally with full privacy
***OpenAI API Compatible** — Connect to any OpenAI-compatible endpoint with your own models
***Multi-Provider Support** — Mix and match text and image generation providers
***Versatile Image Generation** — Choose from DALL-E 3, Gemini Flash, Pexels, or Pixabay
***Rich Media Support** — Icons, charts, and custom graphics for professional presentations
***Runs Locally** — All processing happens on your device, no cloud dependencies
***API Deployment** — Host as your own API service for your team
***Fully Open-Source** — Apache 2.0 licensed, inspect, modify, and contribute
***Docker Ready** — One-command deployment with GPU support for local models
-**Custom Templates & Themes** — Create unlimited presentation designs with HTML and Tailwind CSS
-**AI Template Generation** — Create presentation templates from existing Powerpoint documents.
-**Flexible Generation** — Build presentations from prompts or uploaded documents
-**Export Ready** — Save as PowerPoint (PPTX) and PDF with professional formatting
-**Built-In MCP Server** — Generate presentations over Model Context Protocol
-**Bring Your Own Key** — Use your own API keys for OpenAI, Google Gemini, Anthropic Claude, or any compatible provider. Only pay for what you use, no hidden fees or subscriptions.
-**Ollama Integration** — Run open-source models locally with full privacy
-**OpenAI API Compatible** — Connect to any OpenAI-compatible endpoint with your own models
-**Multi-Provider Support** — Mix and match text and image generation providers
-**Versatile Image Generation** — Choose from DALL-E 3, Gemini Flash, Pexels, or Pixabay
-**Rich Media Support** — Icons, charts, and custom graphics for professional presentations
-**Runs Locally** — All processing happens on your device, no cloud dependencies
-**API Deployment** — Host as your own API service for your team
-**Fully Open-Source** — Apache 2.0 licensed, inspect, modify, and contribute
-**Docker Ready** — One-command deployment with GPU support for local models
## Presenton Cloud
<a href="https://presenton.ai" target="_blank" align="center">
<img src="readme_assets/cloud-banner.png" height="350" alt="Presenton Logo" />
@ -64,16 +61,19 @@ Presenton gives you complete control over your AI presentation workflow. Choose
#### 1. Start Presenton
##### Linux/MacOS (Bash/Zsh Shell):
```bash
docker run -it --name presenton -p 5000:80 -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
##### Windows (PowerShell):
```bash
docker run -it --name presenton -p 5000:80 -v "${PWD}\app_data:/app_data" ghcr.io/presenton/presenton:latest
```
#### 2. Open Presenton
Open http://localhost:5000 on browser of your choice to use Presenton.
> **Note: You can replace 5000 with any other port number of your choice to run Presenton on a different port number.**
@ -101,7 +101,9 @@ You may want to directly provide your API KEYS as environment variables and keep
You can also set the following environment variables to customize the image generation provider and API keys:
- **DISABLE_IMAGE_GENERATION**: If **true**, Image Generation will be disabled for slides.
- **IMAGE_PROVIDER=[pexels/pixabay/gemini_flash/dall-e-3]**: Select the image provider of your choice.
- Required if **DISABLE_IMAGE_GENERATION** is not set to **true**.
- Defaults to **dall-e-3** for OpenAI models, **gemini_flash** for Google models if not set.
- **PEXELS_API_KEY=[Your Pexels API Key]**: Required if using **pexels** as the image provider.
- **PIXABAY_API_KEY=[Your Pixabay API Key]**: Required if using **pixabay** as the image provider.
@ -109,32 +111,37 @@ You can also set the following environment variables to customize the image gene
- **OPENAI_API_KEY=[Your OpenAI API Key]**: Required if using **dall-e-3** as the image provider.
You can disable anonymous telemetry using the following environment variable:
- **DISABLE_ANONYMOUS_TELEMETRY=[true/false]**: Set this to **true** to disable anonymous telemetry.
- **DISABLE_ANONYMOUS_TELEMETRY=[true/false]**: Set this to **true** to disable anonymous telemetry.
> **Note:** You can freely choose both the LLM (text generation) and the image provider. Supported image providers: **pexels**, **pixabay**, **gemini_flash** (Google), and **dall-e-3** (OpenAI).
### Using OpenAI
```bash
docker run -it --name presenton -p 5000:80 -e LLM="openai" -e OPENAI_API_KEY="******" -e IMAGE_PROVIDER="dall-e-3" -e CAN_CHANGE_KEYS="false" -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
### Using Google
```bash
docker run -it --name presenton -p 5000:80 -e LLM="google" -e GOOGLE_API_KEY="******" -e IMAGE_PROVIDER="gemini_flash" -e CAN_CHANGE_KEYS="false" -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
### Using Ollama
```bash
docker run -it --name presenton -p 5000:80 -e LLM="ollama" -e OLLAMA_MODEL="llama3.2:3b" -e IMAGE_PROVIDER="pexels" -e PEXELS_API_KEY="*******" -e CAN_CHANGE_KEYS="false" -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
### Using Anthropic
```bash
docker run -it --name presenton -p 5000:80 -e LLM="anthropic" -e ANTHROPIC_API_KEY="******" -e IMAGE_PROVIDER="pexels" -e PEXELS_API_KEY="******" -e CAN_CHANGE_KEYS="false" -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
### Using OpenAI Compatible API
```bash
docker run -it -p 5000:80 -e CAN_CHANGE_KEYS="false" -e LLM="custom" -e CUSTOM_LLM_URL="http://*****" -e CUSTOM_LLM_API_KEY="*****" -e CUSTOM_MODEL="llama3.2:3b" -e IMAGE_PROVIDER="pexels" -e PEXELS_API_KEY="********" -v "./app_data:/app_data" ghcr.io/presenton/presenton:latest
```
@ -163,29 +170,29 @@ Content-Type: `application/json`
#### Request Body
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| content | string | Yes | The content for generating the presentation |
| slides_markdown | string[] \| null | No | The markdown for the slides |
| instructions | string \| null | No | The instruction for generating the presentation |
| tone | string | No | The tone to use for the text (default: "default"). Available options: "default", "casual", "professional", "funny", "educational", "sales_pitch" |
| verbosity | string | No | How verbose the text should be (default: "standard"). Available options: "concise", "standard", "text-heavy" |
| web_search | boolean | No | Whether to enable web search (default: false) |
| n_slides | integer | No | Number of slides to generate (default: 8) |
| language | string | No | Language for the presentation (default: "English") |
| template | string | No | Template to use for the presentation (default: "general") |
| include_table_of_contents | boolean | No | Whether to include a table of contents (default: false) |
| include_title_slide | boolean | No | Whether to include a title slide (default: true) |
| files | string[] \| null | No | Files to use for the presentation. Use /api/v1/ppt/files/upload to upload files |
| export_as | string | No | Export format (default: "pptx"). Available options: "pptx", "pdf" |
| Parameter | Type | Required | Description |
| ------------------------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| content | string | Yes | The content for generating the presentation |
| slides_markdown | string[] \| null | No | The markdown for the slides |
| instructions | string \| null | No | The instruction for generating the presentation |
| tone | string | No | The tone to use for the text (default: "default"). Available options: "default", "casual", "professional", "funny", "educational", "sales_pitch" |
| verbosity | string | No | How verbose the text should be (default: "standard"). Available options: "concise", "standard", "text-heavy" |
| web_search | boolean | No | Whether to enable web search (default: false) |
| n_slides | integer | No | Number of slides to generate (default: 8) |
| language | string | No | Language for the presentation (default: "English") |
| template | string | No | Template to use for the presentation (default: "general") |
| include_table_of_contents | boolean | No | Whether to include a table of contents (default: false) |
| include_title_slide | boolean | No | Whether to include a title slide (default: true) |
| files | string[] \| null | No | Files to use for the presentation. Use /api/v1/ppt/files/upload to upload files |
| export_as | string | No | Export format (default: "pptx"). Available options: "pptx", "pdf" |
#### Response
```json
{
"presentation_id": "string",
"path": "string",
"edit_path": "string"
"presentation_id": "string",
"path": "string",
"edit_path": "string"
}
```
@ -218,42 +225,51 @@ curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
For detailed info checkout [API documentation](https://docs.presenton.ai/using-presenton-api).
### API Tutorials
- [Generate Presentations via API in 5 minutes](https://docs.presenton.ai/tutorial/generate-presentation-over-api)
- [Create Presentations from CSV using AI](https://docs.presenton.ai/tutorial/generate-presentation-from-csv)
- [Create Data Reports Using AI](https://docs.presenton.ai/tutorial/create-data-reports-using-ai)
## Roadmap
- [x] Support for custom HTML templates by developers
- [x] Support for accessing custom templates over API
- [x] Implement MCP server
- [ ] Ability for users to change system prompt
- [X] Support external SQL database
- [x] Support external SQL database
## UI Features
### 1. Add prompt, select number of slides and language
![Demo](readme_assets/images/prompting.png)
### 2. Select theme
![Demo](readme_assets/images/select-theme.png)
### 3. Review and edit outline
![Demo](readme_assets/images/outline.png)
### 4. Select theme
![Demo](readme_assets/images/select-theme.png)
### 5. Present on app
![Demo](readme_assets/images/present.png)
### 6. Change theme
![Demo](readme_assets/images/change-theme.png)
### 7. Export presentation as PDF and PPTX
![Demo](readme_assets/images/export-presentation.png)
## Community
[Discord](https://discord.gg/9ZsKKxudNE)
## License

View file

@ -1,6 +1,5 @@
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from utils.get_env import get_can_change_keys_env
from utils.user_config import update_env_with_user_config

View file

@ -27,6 +27,7 @@ class UserConfig(BaseModel):
CUSTOM_MODEL: Optional[str] = None
# Image Provider
DISABLE_IMAGE_GENERATION: Optional[bool] = None
IMAGE_PROVIDER: Optional[str] = None
PEXELS_API_KEY: Optional[str] = None
PIXABAY_API_KEY: Optional[str] = None

View file

@ -10,6 +10,7 @@ from utils.download_helpers import download_file
from utils.get_env import get_pexels_api_key_env
from utils.get_env import get_pixabay_api_key_env
from utils.image_provider import (
is_image_generation_disabled,
is_pixels_selected,
is_pixabay_selected,
is_gemini_flash_selected,
@ -19,12 +20,15 @@ import uuid
class ImageGenerationService:
def __init__(self, output_directory: str):
self.output_directory = output_directory
self.is_image_generation_disabled = is_image_generation_disabled()
self.image_gen_func = self.get_image_gen_func()
def get_image_gen_func(self):
if self.is_image_generation_disabled:
return None
if is_pixabay_selected():
return self.get_image_from_pixabay
elif is_pixels_selected():
@ -46,6 +50,10 @@ class ImageGenerationService:
otherwise it uses the full image prompt with theme.
- Output Directory is used for saving the generated image not the stock provider.
"""
if self.is_image_generation_disabled:
print("Image generation is disabled. Using placeholder image.")
return "/static/images/placeholder.jpg"
if not self.image_gen_func:
print("No image generation function found. Using placeholder image.")
return "/static/images/placeholder.jpg"

View file

@ -73,6 +73,10 @@ def get_pexels_api_key_env():
return os.getenv("PEXELS_API_KEY")
def get_disable_image_generation_env():
return os.getenv("DISABLE_IMAGE_GENERATION")
def get_image_provider_env():
return os.getenv("IMAGE_PROVIDER")

View file

@ -1,11 +1,17 @@
from enums.image_provider import ImageProvider
from utils.get_env import (
get_disable_image_generation_env,
get_google_api_key_env,
get_image_provider_env,
get_openai_api_key_env,
get_pexels_api_key_env,
get_pixabay_api_key_env,
)
from utils.parsers import parse_bool_or_none
def is_image_generation_disabled() -> bool:
return parse_bool_or_none(get_disable_image_generation_env()) or False
def is_pixels_selected() -> bool:

View file

@ -28,7 +28,10 @@ from utils.llm_provider import (
is_ollama_selected,
)
from utils.ollama import pull_ollama_model
from utils.image_provider import get_selected_image_provider
from utils.image_provider import (
get_selected_image_provider,
is_image_generation_disabled,
)
async def check_llm_and_image_provider_api_or_model_availability():
@ -104,6 +107,10 @@ async def check_llm_and_image_provider_api_or_model_availability():
if custom_model not in available_models:
raise Exception(f"Model {custom_model} is not available")
# Skip image provider and API key checks if image generation is disabled
if is_image_generation_disabled():
return
# Check for Image Provider and API keys
selected_image_provider = get_selected_image_provider()
if not selected_image_provider:

View file

@ -69,6 +69,10 @@ def set_pixabay_api_key_env(value):
os.environ["PIXABAY_API_KEY"] = value
def set_disable_image_generation_env(value):
os.environ["DISABLE_IMAGE_GENERATION"] = value
def set_tool_calls_env(value):
os.environ["TOOL_CALLS"] = value
@ -82,4 +86,4 @@ def set_extended_reasoning_env(value):
def set_web_grounding_env(value):
os.environ["WEB_GROUNDING"] = value
os.environ["WEB_GROUNDING"] = value

View file

@ -8,6 +8,7 @@ from utils.get_env import (
get_custom_llm_api_key_env,
get_custom_llm_url_env,
get_custom_model_env,
get_disable_image_generation_env,
get_disable_thinking_env,
get_google_api_key_env,
get_google_model_env,
@ -31,6 +32,7 @@ from utils.set_env import (
set_custom_llm_api_key_env,
set_custom_llm_url_env,
set_custom_model_env,
set_disable_image_generation_env,
set_disable_thinking_env,
set_extended_reasoning_env,
set_google_api_key_env,
@ -56,7 +58,7 @@ def get_user_config():
if os.path.exists(user_config_path):
with open(user_config_path, "r") as f:
existing_config = UserConfig(**json.load(f))
except Exception as e:
except Exception:
print("Error while loading user config")
pass
@ -76,6 +78,11 @@ def get_user_config():
or get_custom_llm_api_key_env(),
CUSTOM_MODEL=existing_config.CUSTOM_MODEL or get_custom_model_env(),
IMAGE_PROVIDER=existing_config.IMAGE_PROVIDER or get_image_provider_env(),
DISABLE_IMAGE_GENERATION=(
existing_config.DISABLE_IMAGE_GENERATION
if existing_config.DISABLE_IMAGE_GENERATION is not None
else (parse_bool_or_none(get_disable_image_generation_env()) or False)
),
PIXABAY_API_KEY=existing_config.PIXABAY_API_KEY or get_pixabay_api_key_env(),
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or get_pexels_api_key_env(),
TOOL_CALLS=(
@ -127,6 +134,8 @@ def update_env_with_user_config():
set_custom_llm_api_key_env(user_config.CUSTOM_LLM_API_KEY)
if user_config.CUSTOM_MODEL:
set_custom_model_env(user_config.CUSTOM_MODEL)
if user_config.DISABLE_IMAGE_GENERATION is not None:
set_disable_image_generation_env(str(user_config.DISABLE_IMAGE_GENERATION))
if user_config.IMAGE_PROVIDER:
set_image_provider_env(user_config.IMAGE_PROVIDER)
if user_config.PIXABAY_API_KEY:

View file

@ -46,20 +46,32 @@ export async function POST(request: Request) {
OPENAI_MODEL: userConfig.OPENAI_MODEL || existingConfig.OPENAI_MODEL,
GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,
GOOGLE_MODEL: userConfig.GOOGLE_MODEL || existingConfig.GOOGLE_MODEL,
ANTHROPIC_API_KEY: userConfig.ANTHROPIC_API_KEY || existingConfig.ANTHROPIC_API_KEY,
ANTHROPIC_MODEL: userConfig.ANTHROPIC_MODEL || existingConfig.ANTHROPIC_MODEL,
ANTHROPIC_API_KEY:
userConfig.ANTHROPIC_API_KEY || existingConfig.ANTHROPIC_API_KEY,
ANTHROPIC_MODEL:
userConfig.ANTHROPIC_MODEL || existingConfig.ANTHROPIC_MODEL,
OLLAMA_URL: userConfig.OLLAMA_URL || existingConfig.OLLAMA_URL,
OLLAMA_MODEL: userConfig.OLLAMA_MODEL || existingConfig.OLLAMA_MODEL,
CUSTOM_LLM_URL: userConfig.CUSTOM_LLM_URL || existingConfig.CUSTOM_LLM_URL,
CUSTOM_LLM_API_KEY:
userConfig.CUSTOM_LLM_API_KEY || existingConfig.CUSTOM_LLM_API_KEY,
CUSTOM_MODEL: userConfig.CUSTOM_MODEL || existingConfig.CUSTOM_MODEL,
DISABLE_IMAGE_GENERATION:
userConfig.DISABLE_IMAGE_GENERATION === undefined
? existingConfig.DISABLE_IMAGE_GENERATION
: userConfig.DISABLE_IMAGE_GENERATION,
PIXABAY_API_KEY:
userConfig.PIXABAY_API_KEY || existingConfig.PIXABAY_API_KEY,
IMAGE_PROVIDER: userConfig.IMAGE_PROVIDER || existingConfig.IMAGE_PROVIDER,
PEXELS_API_KEY: userConfig.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
TOOL_CALLS: userConfig.TOOL_CALLS === undefined ? existingConfig.TOOL_CALLS : userConfig.TOOL_CALLS,
DISABLE_THINKING: userConfig.DISABLE_THINKING === undefined ? existingConfig.DISABLE_THINKING : userConfig.DISABLE_THINKING,
TOOL_CALLS:
userConfig.TOOL_CALLS === undefined
? existingConfig.TOOL_CALLS
: userConfig.TOOL_CALLS,
DISABLE_THINKING:
userConfig.DISABLE_THINKING === undefined
? existingConfig.DISABLE_THINKING
: userConfig.DISABLE_THINKING,
EXTENDED_REASONING:
userConfig.EXTENDED_REASONING === undefined
? existingConfig.EXTENDED_REASONING

View file

@ -3,6 +3,7 @@ import { useState, useEffect } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs";
import { Check, ChevronsUpDown, Info } from "lucide-react";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import {
Command,
CommandEmpty,
@ -49,6 +50,7 @@ export default function LLMProviderSelection({
}: LLMProviderSelectionProps) {
const [llmConfig, setLlmConfig] = useState<LLMConfig>(initialLLMConfig);
const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
const isImageGenerationDisabled = llmConfig.DISABLE_IMAGE_GENERATION ?? false;
useEffect(() => {
onConfigChange(llmConfig);
@ -62,12 +64,21 @@ export default function LLMProviderSelection({
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) ||
(llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_MODEL);
const needsApiKey =
((llmConfig.IMAGE_PROVIDER === "dall-e-3" || llmConfig.LLM === "openai") && !llmConfig.OPENAI_API_KEY) ||
((llmConfig.IMAGE_PROVIDER === "gemini_flash" || llmConfig.LLM === "google") && !llmConfig.GOOGLE_API_KEY) ||
(llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_API_KEY) ||
(llmConfig.IMAGE_PROVIDER === "pexels" && !llmConfig.PEXELS_API_KEY) ||
(llmConfig.IMAGE_PROVIDER === "pixabay" && !llmConfig.PIXABAY_API_KEY);
const needsProviderApiKey =
(llmConfig.LLM === "openai" && !llmConfig.OPENAI_API_KEY) ||
(llmConfig.LLM === "google" && !llmConfig.GOOGLE_API_KEY) ||
(llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_API_KEY);
const needsImageProviderApiKey =
!llmConfig.DISABLE_IMAGE_GENERATION &&
(
(llmConfig.IMAGE_PROVIDER === "dall-e-3" && !llmConfig.OPENAI_API_KEY) ||
(llmConfig.IMAGE_PROVIDER === "gemini_flash" && !llmConfig.GOOGLE_API_KEY) ||
(llmConfig.IMAGE_PROVIDER === "pexels" && !llmConfig.PEXELS_API_KEY) ||
(llmConfig.IMAGE_PROVIDER === "pixabay" && !llmConfig.PIXABAY_API_KEY)
);
const needsApiKey = needsProviderApiKey || needsImageProviderApiKey;
const needsOllamaUrl = (llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_URL);
@ -101,20 +112,29 @@ export default function LLMProviderSelection({
}, [llmConfig.USE_CUSTOM_URL]);
useEffect(() => {
let updates: any = {};
if (!llmConfig.IMAGE_PROVIDER) {
if (llmConfig.LLM === "openai") {
updates.IMAGE_PROVIDER = "dall-e-3";
} else if (llmConfig.LLM === "google") {
updates.IMAGE_PROVIDER = "gemini_flash";
} else {
updates.IMAGE_PROVIDER = "pexels";
setLlmConfig((prevConfig) => {
const updates: Partial<LLMConfig> = {};
if (!prevConfig.DISABLE_IMAGE_GENERATION && !prevConfig.IMAGE_PROVIDER) {
if (prevConfig.LLM === "openai") {
updates.IMAGE_PROVIDER = "dall-e-3";
} else if (prevConfig.LLM === "google") {
updates.IMAGE_PROVIDER = "gemini_flash";
} else {
updates.IMAGE_PROVIDER = "pexels";
}
}
}
if (!llmConfig.OLLAMA_URL) {
updates.OLLAMA_URL = "http://localhost:11434";
}
setLlmConfig({ ...llmConfig, ...updates });
if (!prevConfig.OLLAMA_URL) {
updates.OLLAMA_URL = "http://localhost:11434";
}
if (Object.keys(updates).length === 0) {
return prevConfig;
}
return { ...prevConfig, ...updates };
});
}, []);
return (
@ -198,134 +218,160 @@ export default function LLMProviderSelection({
</TabsContent>
</Tabs>
{/* Image Provider Selection */}
{/* Image Generation Toggle */}
<div className="my-8">
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Image Provider
</label>
<div className="w-full">
<Popover
open={openImageProviderSelect}
onOpenChange={setOpenImageProviderSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openImageProviderSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{llmConfig.IMAGE_PROVIDER
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label ||
llmConfig.IMAGE_PROVIDER
: "Select image provider"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search provider..." />
<CommandList>
<CommandEmpty>No provider found.</CommandEmpty>
<CommandGroup>
{Object.values(IMAGE_PROVIDERS).map(
(provider, index) => (
<CommandItem
key={index}
value={provider.value}
onSelect={(value) => {
input_field_changed(value, "image_provider");
setOpenImageProviderSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
llmConfig.IMAGE_PROVIDER === provider.value
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900 capitalize">
{provider.label}
</span>
</div>
<span className="text-xs text-gray-600 leading-relaxed">
{provider.description}
</span>
</div>
</div>
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Disable Image Generation
</label>
<Switch
checked={isImageGenerationDisabled}
onCheckedChange={(checked) => {
input_field_changed(checked, "disable_image_generation");
if (checked) {
setOpenImageProviderSelect(false);
}
}}
/>
</div>
<p className="text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
When enabled, slides will not include automatically generated images.
</p>
</div>
{/* Dynamic API Key Input for Image Provider */}
{llmConfig.IMAGE_PROVIDER &&
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
(() => {
const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
// Show info message when using same API key as main provider
if (provider.value === "dall-e-3" && llmConfig.LLM === "openai") {
return <></>;
}
if (provider.value === "gemini_flash" && llmConfig.LLM === "google") {
return <></>;
}
// Show API key input for other providers
return (
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
{provider.apiKeyFieldLabel}
</label>
<div className="relative">
<input
type="text"
placeholder={`Enter your ${provider.apiKeyFieldLabel}`}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={
provider.apiKeyField === "PEXELS_API_KEY"
? llmConfig.PEXELS_API_KEY || ""
: provider.apiKeyField === "PIXABAY_API_KEY"
? llmConfig.PIXABAY_API_KEY || ""
: ""
}
onChange={(e) => {
if (provider.apiKeyField === "PEXELS_API_KEY") {
input_field_changed(e.target.value, "pexels_api_key");
} else if (provider.apiKeyField === "PIXABAY_API_KEY") {
input_field_changed(e.target.value, "pixabay_api_key");
}
}}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
API key for {provider.label} image generation
</p>
{!isImageGenerationDisabled && (
<>
{/* Image Provider Selection */}
<div className="my-8">
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Image Provider
</label>
<div className="w-full">
<Popover
open={openImageProviderSelect}
onOpenChange={setOpenImageProviderSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openImageProviderSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{llmConfig.IMAGE_PROVIDER
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label ||
llmConfig.IMAGE_PROVIDER
: "Select image provider"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search provider..." />
<CommandList>
<CommandEmpty>No provider found.</CommandEmpty>
<CommandGroup>
{Object.values(IMAGE_PROVIDERS).map(
(provider, index) => (
<CommandItem
key={index}
value={provider.value}
onSelect={(value) => {
input_field_changed(value, "image_provider");
setOpenImageProviderSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
llmConfig.IMAGE_PROVIDER === provider.value
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900 capitalize">
{provider.label}
</span>
</div>
<span className="text-xs text-gray-600 leading-relaxed">
{provider.description}
</span>
</div>
</div>
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
})()}
</div>
{/* Dynamic API Key Input for Image Provider */}
{llmConfig.IMAGE_PROVIDER &&
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
(() => {
const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
// Show info message when using same API key as main provider
if (provider.value === "dall-e-3" && llmConfig.LLM === "openai") {
return <></>;
}
if (provider.value === "gemini_flash" && llmConfig.LLM === "google") {
return <></>;
}
// Show API key input for other providers
return (
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
{provider.apiKeyFieldLabel}
</label>
<div className="relative">
<input
type="text"
placeholder={`Enter your ${provider.apiKeyFieldLabel}`}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={
provider.apiKeyField === "PEXELS_API_KEY"
? llmConfig.PEXELS_API_KEY || ""
: provider.apiKeyField === "PIXABAY_API_KEY"
? llmConfig.PIXABAY_API_KEY || ""
: ""
}
onChange={(e) => {
if (provider.apiKeyField === "PEXELS_API_KEY") {
input_field_changed(e.target.value, "pexels_api_key");
} else if (provider.apiKeyField === "PIXABAY_API_KEY") {
input_field_changed(e.target.value, "pixabay_api_key");
}
}}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
API key for {provider.label} image generation
</p>
</div>
);
})()}
</>
)}
{/* Model Information */}
<div className="mb-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
@ -348,12 +394,19 @@ export default function LLMProviderSelection({
: llmConfig.LLM === "openai"
? llmConfig.OPENAI_MODEL ?? "xxxxx"
: "xxxxx"}{" "}
for text generation and{" "}
{llmConfig.IMAGE_PROVIDER &&
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER].label
: "xxxxx"}{" "}
for images
for text generation{" "}
{isImageGenerationDisabled ? (
"and image generation is disabled."
) : (
<>
and{" "}
{llmConfig.IMAGE_PROVIDER &&
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER].label
: "xxxxx"}{" "}
for images
</>
)}
</p>
</div>
</div>

View file

@ -23,6 +23,7 @@ export interface LLMConfig {
CUSTOM_MODEL?: string;
// Image providers
DISABLE_IMAGE_GENERATION?: boolean;
IMAGE_PROVIDER?: string;
PEXELS_API_KEY?: string;
PIXABAY_API_KEY?: string;

View file

@ -42,6 +42,7 @@ export const updateLLMConfig = (
pexels_api_key: "PEXELS_API_KEY",
pixabay_api_key: "PIXABAY_API_KEY",
image_provider: "IMAGE_PROVIDER",
disable_image_generation: "DISABLE_IMAGE_GENERATION",
use_custom_url: "USE_CUSTOM_URL",
tool_calls: "TOOL_CALLS",
disable_thinking: "DISABLE_THINKING",

View file

@ -16,7 +16,7 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
if (!llmConfig.LLM) return false;
if (!llmConfig.IMAGE_PROVIDER) return false;
if (!llmConfig.DISABLE_IMAGE_GENERATION && !llmConfig.IMAGE_PROVIDER) return false;
const isOpenAIConfigValid =
llmConfig.OPENAI_MODEL !== "" &&
@ -58,7 +58,12 @@ export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
llmConfig.CUSTOM_MODEL !== null &&
llmConfig.CUSTOM_MODEL !== undefined;
const shouldValidateImages = !llmConfig.DISABLE_IMAGE_GENERATION;
const isImageConfigValid = () => {
if (!shouldValidateImages) {
return true;
}
switch (llmConfig.IMAGE_PROVIDER) {
case "pexels":
return llmConfig.PEXELS_API_KEY && llmConfig.PEXELS_API_KEY !== "";