Merge pull request #361 from presenton/feat/option-to-disable-image-generation
feat/option to disable image generation
This commit is contained in:
commit
18bfdbf19b
15 changed files with 327 additions and 201 deletions
|
|
@ -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
100
README.md
|
|
@ -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.**
|
||||
|
||||

|
||||
|
||||
|
||||
> [!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
|
||||
|
||||

|
||||
|
||||
### 2. Select theme
|
||||
|
||||

|
||||
|
||||
### 3. Review and edit outline
|
||||
|
||||

|
||||
|
||||
### 4. Select theme
|
||||
|
||||

|
||||
|
||||
### 5. Present on app
|
||||
|
||||

|
||||
|
||||
### 6. Change theme
|
||||
|
||||

|
||||
|
||||
### 7. Export presentation as PDF and PPTX
|
||||
|
||||

|
||||
|
||||
## Community
|
||||
|
||||
[Discord](https://discord.gg/9ZsKKxudNE)
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 !== "";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue