Merge branch 'feat/custom_schema_and_layout' of https://github.com/presenton/presenton into feat/custom_schema_and_layout

merge
This commit is contained in:
Suraj Jha 2025-07-25 23:33:36 +05:45
commit b93701a168
49 changed files with 1075 additions and 423 deletions

View file

@ -29,6 +29,7 @@
* ✅ **API Presentation Generation** — Host as API to generate presentations over requests
* ✅ **Ollama Support** — Run open-source models locally with Ollama integration
* ✅ **OpenAI API Compatibility** — Use any OpenAI-compatible API endpoint with your own models
* ✅ **Versatile Image Generation** — Choose from DALL-E 3, Gemini Flash, Pexels, or Pixabay for your visuals
* ✅ **Runs Locally** — All code runs on your device
* ✅ **Privacy-First** — No tracking, no data stored by us
* ✅ **Flexible** — Generate presentations from prompts or outlines
@ -74,7 +75,7 @@ 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:
- **IMAGE_PROVIDER=[pexels/pixabay/gemini_flash/dall-e-3]**: Select the image provider of your choice.
- Defaults to **dall-e-3** for OpenAI models and **gemini_flash** for Google models if not set.
- 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.
- **GOOGLE_API_KEY=[Your Google API Key]**: Required if using **gemini_flash** as the image provider.

View file

@ -3,17 +3,23 @@ import json
import os
import random
from typing import Annotated, List, Optional
import uuid
import uuid, aiohttp
from fastapi import APIRouter, Body, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import delete
from sqlmodel import select
from models.presentation_outline_model import PresentationOutlineModel, SlideOutlineModel
from models.pptx_models import PptxPresentationModel
from models.presentation_outline_model import SlideOutlineModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
from models.presentation_with_slides import PresentationWithSlides
from models.generate_presentation_api import (
GeneratePresentationRequest,
PresentationAndPath,
PresentationPathAndEditPath,
)
from services.get_layout_by_name import get_layout_by_name
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
from models.sql.slide import SlideModel
from models.sse_response import SSECompleteResponse, SSEResponse
from services import TEMP_FILE_SERVICE
@ -297,3 +303,187 @@ async def create_pptx(pptx_model: Annotated[PptxPresentationModel, Body()]):
pptx_creator.save(pptx_path)
return pptx_path
@PRESENTATION_ROUTER.post("/generate")
async def generate_presentation_api(data: Annotated[GeneratePresentationRequest, Body()]):
presentation_id = str(uuid.uuid4())
print("**" * 40)
print(f"Generating presentation with ID: {presentation_id}")
print(f"Received Body as JSON: {data.model_dump_json(indent=2)}")
# 1. Save uploaded files
file_paths = []
if data.documents:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
for upload in data.documents:
file_path = os.path.join(temp_dir, upload.filename)
with open(file_path, "wb") as f:
f.write(await upload.read())
file_paths.append(file_path)
# 2. Create Presentation Summary (if documents are provided)
summary = None
if file_paths:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
# 3. Generate Outlines
presentation_content_text = ""
async for chunk in generate_ppt_outline(
data.prompt,
data.n_slides,
data.language,
summary,
):
presentation_content_text += chunk
presentation_content_json = json.loads(presentation_content_text)
presentation_content = PresentationOutlineModel(**presentation_content_json)
outlines = presentation_content.slides[:data.n_slides]
total_outlines = len(outlines)
print("-" * 40)
print("Generated Presentation Content:", presentation_content_text)
print(f"Generated {total_outlines} outlines for the presentation")
print(f"Presentation Title: {presentation_content.title}")
# 4. Parse Layouts
layout = await get_layout_by_name(data.layout)
total_slide_layouts = len(layout.slides)
# 5. Generate Structure
if layout.ordered:
presentation_structure = layout.to_presentation_structure()
else:
presentation_structure: PresentationStructureModel = (
await generate_presentation_structure(
presentation_outline=PresentationOutlineModel(
title=presentation_content.title,
slides=outlines,
notes=presentation_content.notes,
),
presentation_layout=layout,
)
)
presentation_structure.slides = presentation_structure.slides[:total_outlines]
for index in range(total_outlines):
random_slide_index = random.randint(0, total_slide_layouts - 1)
if index >= total_outlines:
presentation_structure.slides.append(random_slide_index)
continue
if presentation_structure.slides[index] >= total_slide_layouts:
presentation_structure.slides[index] = random_slide_index
# 6. Create and Save PresentationModel
presentation = PresentationModel(
id=presentation_id,
prompt=data.prompt,
n_slides=data.n_slides,
language=data.language,
title=presentation_content.title,
summary=summary,
outlines=[each.model_dump() for each in outlines],
notes=presentation_content.notes,
layout=layout.model_dump(),
structure=presentation_structure.model_dump(),
)
with get_sql_session() as sql_session:
sql_session.add(presentation)
sql_session.commit()
sql_session.refresh(presentation)
# 7. Generate slide content and save slides
slides: List[SlideModel] = []
slide_contents: List[dict] = []
for i, slide_layout_index in enumerate(presentation_structure.slides):
slide_layout = layout.slides[slide_layout_index]
print(f"Generating content for slide {i} with layout {slide_layout.id}")
slide_content = await get_slide_content_from_type_and_outline(
slide_layout, outlines[i]
)
print(f"Generated content for slide {i}: {json.dumps(slide_content, indent=2)}")
slide = SlideModel(
presentation=presentation_id,
layout_group=layout.name,
layout=slide_layout.id,
index=i,
content=slide_content,
)
slides.append(slide)
slide_contents.append(slide_content)
# Process slides to fetch assets (images, icons, etc.)
print("Processing slides to fetch assets")
for slide in slides:
try:
await process_slide_and_fetch_assets(slide)
print(f"Processed slide {slide.index} successfully")
except Exception as e:
print(f"Error processing slide {slide.index}: {e}")
with get_sql_session() as sql_session:
sql_session.add_all(slides)
sql_session.commit()
# 8. Export as PPTX
if data.export_as == "pptx":
print("-" * 40)
print("Exporting Presentation as PPTX")
# Get the converted PPTX model from your existing Next.js service
async with aiohttp.ClientSession() as session:
async with session.get(
f"http://localhost/api/presentation_to_pptx_model?id={presentation_id}"
) as response:
if response.status != 200:
error_text = await response.text()
print(f"Failed to get PPTX model: {error_text}")
raise HTTPException(status_code=500, detail="Failed to convert presentation to PPTX model")
pptx_model_data = await response.json()
print(f"Received PPTX model data: {json.dumps(pptx_model_data, indent=2)}")
# Create PPTX file using the converted model
pptx_model = PptxPresentationModel(**pptx_model_data)
print(f"Creating PPTX with model: {pptx_model.model_dump_json(indent=2)}")
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
pptx_creator = PptxPresentationCreator(pptx_model, temp_dir)
await pptx_creator.create_ppt()
export_directory = get_exports_directory()
pptx_path = os.path.join(
export_directory, f"{presentation_content.title}.pptx"
)
pptx_creator.save(pptx_path)
presentation_and_path = PresentationAndPath(
presentation_id=presentation_id,
path=pptx_path,
)
else:
print("-" * 40)
print("Exporting Presentation as PDF")
async with aiohttp.ClientSession() as session:
async with session.post(
"http://localhost/api/export-as-pdf",
json={
"id": presentation_id,
"title": presentation_content.title,
},
) as response:
response_json = await response.json()
print(f"Received PDF export response: {json.dumps(response_json, indent=2)}")
presentation_and_path = PresentationAndPath(
presentation_id=presentation_id,
path=response_json["path"],
)
return PresentationPathAndEditPath(
**presentation_and_path.model_dump(),
edit_path=f"/presentation?id={presentation_id}",
)

View file

@ -1,10 +1,11 @@
from typing import Annotated
from typing import Annotated, Optional
from fastapi import APIRouter, Body, HTTPException
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
from services.database import get_sql_session
from utils.llm_calls.edit_slide import get_edited_slide_content
from utils.llm_calls.edit_slide_html import get_edited_slide_html
from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt
from utils.process_slides import process_old_and_new_slides_and_fetch_assets
from utils.randomizers import get_random_uuid
@ -14,10 +15,7 @@ SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"])
@SLIDE_ROUTER.post("/edit")
async def edit_slide(
id: Annotated[str, Body()],
prompt: Annotated[str, Body()]
):
async def edit_slide(id: Annotated[str, Body()], prompt: Annotated[str, Body()]):
with get_sql_session() as sql_session:
slide = sql_session.get(SlideModel, id)
@ -53,3 +51,33 @@ async def edit_slide(
sql_session.refresh(slide)
return slide
@SLIDE_ROUTER.post("/edit-html", response_model=SlideModel)
async def edit_slide_html(
id: Annotated[str, Body()],
prompt: Annotated[str, Body()],
html: Annotated[Optional[str], Body()] = None,
):
with get_sql_session() as sql_session:
slide = sql_session.get(SlideModel, id)
if not slide:
raise HTTPException(status_code=404, detail="Slide not found")
html_to_edit = html or slide.html_content
if not html_to_edit:
raise HTTPException(status_code=400, detail="No HTML to edit")
edited_slide_html = await get_edited_slide_html(prompt, html_to_edit)
# Always assign a new unique id to the slide
# This is to ensure that the nextjs can track slide updates
slide.id = get_random_uuid()
with get_sql_session() as sql_session:
sql_session.add(slide)
slide.html_content = edited_slide_html
sql_session.commit()
sql_session.refresh(slide)
return slide

View file

@ -0,0 +1,19 @@
from typing import List, Optional, Literal
from pydantic import BaseModel, Field
from fastapi import UploadFile
class GeneratePresentationRequest(BaseModel):
prompt: str
n_slides: int = Field(default=8, ge=5, le=15)
language: str = Field(default="English")
layout: str = Field(default="default")
documents: Optional[List[UploadFile]] = None
export_as: Literal["pptx", "pdf"] = Field(default="pptx")
class PresentationAndPath(BaseModel):
presentation_id: str
path: str
class PresentationPathAndEditPath(PresentationAndPath):
edit_path: str

View file

@ -1,3 +1,4 @@
from typing import Optional
from sqlmodel import SQLModel, Field, Column, JSON
from utils.randomizers import get_random_uuid
@ -10,3 +11,4 @@ class SlideModel(SQLModel, table=True):
layout: str
index: int
content: dict = Field(sa_column=Column(JSON))
html_content: Optional[str]

View file

@ -0,0 +1,18 @@
import aiohttp
from fastapi import HTTPException
from models.presentation_layout import PresentationLayoutModel
from typing import List
async def get_layout_by_name(layout_name: str) -> PresentationLayoutModel:
url = f"http://localhost/api/layout?group={layout_name}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
error_text = await response.text()
raise HTTPException(
status_code=404,
detail=f"Layout '{layout_name}' not found: {error_text}"
)
layout_json = await response.json()
# Parse the JSON into your Pydantic model
return PresentationLayoutModel(**layout_json)

View file

@ -0,0 +1,189 @@
from unittest.mock import patch, AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from fastapi import FastAPI
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
from api.v1.ppt.endpoints.presentation import PRESENTATION_ROUTER
class MockAiohttpResponse:
def __init__(self, status=200, json_data=None):
self.status = status
self._json_data = json_data or {"path": "/tmp/exports/test.pdf"}
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
pass
async def json(self):
return self._json_data
async def text(self):
return str(self._json_data)
class MockAiohttpSession:
def __init__(self, *args, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
pass
def post(self, *args, **kwargs):
return MockAiohttpResponse()
def get(self, *args, **kwargs):
pptx_model_data = {
"slides": [],
"title": "Test",
"notes": [],
"layout": {},
"structure": {},
}
return MockAiohttpResponse(json_data=pptx_model_data)
@pytest.fixture
def app():
app = FastAPI()
app.include_router(PRESENTATION_ROUTER, prefix="/api/v1/ppt")
return app
@pytest.fixture
def client(app):
return TestClient(app)
@pytest.fixture
def mock_get_layout():
async def _mock_get_layout_by_name(layout_name: str):
mock_slide = MagicMock()
mock_slide.name = "Mock Slide"
mock_slide.json_schema = {"title": "Mock Slide Title"}
mock_slide.description = "Mock slide description"
mock_layout = MagicMock(spec=PresentationLayoutModel)
mock_layout.name = layout_name
mock_layout.ordered = True
mock_layout.slides = [mock_slide]
mock_layout.model_dump = lambda: {}
mock_layout.to_presentation_structure = lambda: PresentationStructureModel(
slides=[index for index in range(len(mock_layout.slides))]
)
def to_string():
message = f"## Presentation Layout\n\n"
for index, slide in enumerate(mock_layout.slides):
message += f"### Slide Layout: {index}: \n"
message += f"- Name: {slide.name or slide.json_schema.get('title')} \n"
message += f"- Description: {slide.description} \n\n"
return message
mock_layout.to_string = to_string
return mock_layout
return _mock_get_layout_by_name
async def mock_generate_ppt_outline(*args, **kwargs):
yield '{"title": "Test", "slides": [{"title": "Slide 1", "body": "Body 1"}], "notes": []}'
@pytest.fixture(autouse=True)
def patch_presentation_api(monkeypatch, mock_get_layout):
# Patch all dependencies used in the API
patches = [
patch('api.v1.ppt.endpoints.presentation.get_layout_by_name', new=AsyncMock(side_effect=mock_get_layout)),
patch('api.v1.ppt.endpoints.presentation.TEMP_FILE_SERVICE.create_temp_dir', return_value='/tmp/mockdir'),
patch('api.v1.ppt.endpoints.presentation.DocumentsLoader'),
patch('api.v1.ppt.endpoints.presentation.generate_document_summary', new_callable=AsyncMock, return_value="mock_summary"),
patch('api.v1.ppt.endpoints.presentation.generate_ppt_outline', side_effect=mock_generate_ppt_outline),
patch('api.v1.ppt.endpoints.presentation.get_sql_session'),
patch('api.v1.ppt.endpoints.presentation.get_slide_content_from_type_and_outline', new_callable=AsyncMock, return_value={"mock": "slide_content"}),
patch('api.v1.ppt.endpoints.presentation.process_slide_and_fetch_assets', new_callable=AsyncMock),
patch('api.v1.ppt.endpoints.presentation.get_exports_directory', return_value='/tmp/exports'),
patch('api.v1.ppt.endpoints.presentation.PptxPresentationCreator'),
patch('api.v1.ppt.endpoints.presentation.aiohttp.ClientSession', return_value=MockAiohttpSession()),
]
mocks = [p.start() for p in patches]
# Setup DocumentsLoader mock
docs_loader = mocks[2]
docs_loader.return_value.load_documents = AsyncMock()
docs_loader.return_value.documents = []
# Setup PptxPresentationCreator mock for pptx test
pptx_creator = mocks[9]
pptx_creator.return_value.create_ppt = AsyncMock()
pptx_creator.return_value.save = MagicMock()
yield
for p in patches:
p.stop()
class TestPresentationGenerationAPI:
def test_generate_presentation_export_as_pdf(self, client):
response = client.post(
"/api/v1/ppt/presentation/generate",
json={
"prompt": "Create a presentation about artificial intelligence and machine learning",
"n_slides": 5,
"language": "English",
"export_as": "pdf",
"layout": "general"
}
)
assert response.status_code == 200
assert "presentation_id" in response.json()
assert "pdf" in response.json()["path"]
def test_generate_presentation_export_as_pptx(self, client):
response = client.post(
"/api/v1/ppt/presentation/generate",
json={
"prompt": "Create a presentation about artificial intelligence and machine learning",
"n_slides": 5,
"language": "English",
"export_as": "pptx",
"layout": "general"
}
)
assert response.status_code == 200
assert "presentation_id" in response.json()
assert "pptx" in response.json()["path"]
def test_generate_presentation_with_no_prompt(self, client):
response = client.post(
"/api/v1/ppt/presentation/generate",
json={
"n_slides": 5,
"language": "English",
"export_as": "pdf",
"layout": "general"
}
)
assert response.status_code == 422
def test_generate_presentation_with_n_slides_less_than_one(self, client):
response = client.post(
"/api/v1/ppt/presentation/generate",
json={
"prompt": "Create a presentation about artificial intelligence and machine learning",
"n_slides": 0,
"language": "English",
"export_as": "pdf",
"layout": "general"
}
)
assert response.status_code == 422
def test_generate_presentation_with_invalid_export_type(self, client):
response = client.post(
"/api/v1/ppt/presentation/generate",
json={
"prompt": "Create a presentation about artificial intelligence and machine learning",
"n_slides": 5,
"language": "English",
"export_as": "invalid_type",
"layout": "general"
}
)
assert response.status_code == 422

View file

@ -7,8 +7,8 @@ from models.sql.slide import SlideModel
from google.genai.types import GenerateContentConfig
from utils.llm_provider import (
get_google_llm_client,
get_large_model,
get_llm_client,
get_small_model,
is_google_selected,
)
from utils.schema_utils import remove_fields_from_schema
@ -58,7 +58,7 @@ async def get_edited_slide_content(
slide: SlideModel,
language: Optional[str] = None,
):
model = get_small_model()
model = get_large_model()
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)

View file

@ -0,0 +1,93 @@
import asyncio
from typing import Optional
from google.genai.types import GenerateContentConfig
from utils.llm_provider import (
get_google_llm_client,
get_large_model,
is_google_selected,
get_llm_client,
)
system_prompt = """
You are an expert HTML slide editor. Your task is to modify slide HTML content based on user prompts while maintaining proper structure, styling, and functionality.
Guidelines:
1. **Preserve Structure**: Maintain the overall HTML structure, including essential containers, classes, and IDs
2. **Content Updates**: Modify text, images, lists, and other content elements as requested
3. **Style Consistency**: Keep existing CSS classes and styling unless specifically asked to change them
4. **Responsive Design**: Ensure modifications work across different screen sizes
5. **Accessibility**: Maintain proper semantic HTML and accessibility attributes
6. **Clean Output**: Return only the modified HTML without explanations unless errors occur
Common Edit Types:
- Text content changes (headings, paragraphs, lists)
- Image updates (src, alt text, captions)
- Layout modifications (adding/removing sections)
- Style adjustments (colors, fonts, spacing via classes)
- Interactive elements (buttons, links, forms)
Error Handling:
- If the HTML structure is invalid, fix it while making requested changes
- If a request would break functionality, suggest an alternative approach
- For unclear prompts, make reasonable assumptions and note any ambiguities
Output Format:
Return the complete modified HTML. If the original HTML contains <style> or <script> tags, preserve them unless specifically asked to modify.
"""
def get_user_prompt(prompt: str, html: str):
return f"""
Please edit the following slide HTML based on this prompt:
**Edit Request:** {prompt}
**Current HTML:**
```html
{html}
```
Return the modified HTML with your changes applied.
"""
async def get_edited_slide_html(prompt: str, html: str):
model = get_large_model()
llm_response = None
if is_google_selected():
client = get_google_llm_client()
response = await asyncio.to_thread(
client.models.generate_content,
model=model,
contents=[get_user_prompt(prompt, html)],
config=GenerateContentConfig(
system_instruction=system_prompt,
response_mime_type="text/plain",
),
)
llm_response = response.text
else:
client = get_llm_client()
response = await client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": get_user_prompt(prompt, html)},
],
)
llm_response = response.choices[0].message.content
if not llm_response:
return html
return extract_html_from_response(llm_response) or html
def extract_html_from_response(response_text: str) -> Optional[str]:
start_index = response_text.find("<")
end_index = response_text.rfind(">")
if start_index != -1 and end_index != -1 and end_index > start_index:
return response_text[start_index : end_index + 1]
return None

View file

@ -87,14 +87,13 @@ async def generate_ppt_outline(
if not is_google_selected():
client = get_llm_client()
async with client.beta.chat.completions.stream(
async for response in await client.chat.completions.create(
model=model,
messages=get_prompt_template(prompt, n_slides, language, content),
stream=True,
response_format=response_model,
) as stream:
async for event in stream:
if isinstance(event, ContentDeltaEvent):
yield event.delta
):
yield response.choices[0].delta
else:
client = get_google_llm_client()

View file

@ -12,7 +12,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
import { getStaticFileUrl } from "../utils/others";
import { toast } from "sonner";
interface IconsEditorProps {
icon_prompt?: string[] | null;
onClose?: () => void;
@ -53,8 +53,9 @@ const IconsEditor = ({
limit: 40,
});
setIcons(data);
} catch (error) {
} catch (error: any) {
console.error("Error fetching icons:", error);
toast.error(error.message || "Failed to fetch icons. Please try again.");
setIcons([]);
} finally {
setLoading(false);

View file

@ -98,10 +98,10 @@ const ImageEditor = ({
try {
const response = await PresentationGenerationApi.getPreviousGeneratedImages();
setPreviousGeneratedImages(response);
} catch (error) {
} catch (error: any) {
toast.error("Failed to get previous generated images. Please try again.");
console.error("error in getting previous generated images", error);
setError("Failed to get previous generated images. Please try again.");
setError(error.message || "Failed to get previous generated images. Please try again.");
}
}
@ -218,9 +218,9 @@ const ImageEditor = ({
});
setPreviewImages(response);
} catch (err) {
} catch (err: any) {
console.error("Error in image generation", err);
setError("Failed to generate image. Please try again.");
setError(err.message || "Failed to generate image. Please try again.");
} finally {
setIsGenerating(false);
}

View file

@ -16,7 +16,7 @@ export interface LayoutInfo {
export interface GroupSetting {
description: string;
ordered: boolean;
isDefault?: boolean;
default?: boolean;
}
export interface GroupedLayoutsResponse {
@ -87,7 +87,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const settings = groupData.settings || {
description: `${groupData.groupName} presentation layouts`,
ordered: false,
isDefault: false
default: false
};
groupSettingsMap.set(groupData.groupName, settings);

View file

@ -149,11 +149,13 @@ const DocumentsPreviewPage: React.FC = () => {
dispatch(setPresentationId(createResponse.id));
router.push("/outline");
} catch (error) {
console.error("Error in presentation creation:", error);
toast.error("Error in presentation creation. Please try again.");
} catch (error: any) {
console.error("Error in radar presentation creation:", error);
toast.error('Error', {
description: error.message || "Error in radar presentation creation.",
});
setShowLoading({
message: "Error in presentation creation.",
message: "Error in radar presentation creation.",
show: true,
duration: 10,
progress: false,

View file

@ -32,14 +32,14 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
name: groupName,
description: settings?.description || `${groupName} presentation layouts`,
ordered: settings?.ordered || false,
isDefault: settings?.isDefault || false,
default: settings?.default || false,
};
});
// Sort groups to put default first, then by name
return Groups.sort((a, b) => {
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [getAllGroups, getLayoutsByGroup, getGroupSetting]);
@ -47,7 +47,7 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
// Auto-select first group when groups are loaded
useEffect(() => {
if (layoutGroups.length > 0 && !selectedLayoutGroup) {
const defaultGroup = layoutGroups.find(g => g.isDefault) || layoutGroups[0];
const defaultGroup = layoutGroups.find(g => g.default) || layoutGroups[0];
const slides = getLayoutsByGroup(defaultGroup.id);
onSelectLayoutGroup({

View file

@ -86,10 +86,10 @@ export const usePresentationGeneration = (
dispatch(clearPresentationData());
router.push(`/presentation?id=${presentationId}&stream=true`);
}
} catch (error) {
console.error("Error in data generation", error);
} catch (error: any) {
console.error('Error In Presentation Generation(prepare).', error);
toast.error("Generation Error", {
description: "Failed to generate presentation. Please try again.",
description: error.message || "Error In Presentation Generation(prepare).",
});
} finally {
setLoadingState(DEFAULT_LOADING_STATE);

View file

@ -3,7 +3,7 @@ export interface LayoutGroup {
name: string;
description: string;
ordered: boolean;
isDefault?: boolean;
default?: boolean;
slides?: any
}

View file

@ -60,9 +60,11 @@ const SlideContent = ({
dispatch(updateSlide({ index: slide.index, slide: response }));
toast.success("Slide updated successfully");
}
} catch (error) {
console.error("Error updating slide:", error);
toast.error("Failed to update slide. Please try again.");
} catch (error: any) {
console.error("Error in slide editing:", error);
toast.error("Error in slide editing.", {
description: error.message || "Error in slide editing.",
});
} finally {
setIsUpdating(false);
}
@ -70,8 +72,11 @@ const SlideContent = ({
const onDeleteSlide = async () => {
try {
dispatch(deletePresentationSlide(slide.index));
} catch (error) {
} catch (error: any) {
console.error("Error deleting slide:", error);
toast.error("Error deleting slide.", {
description: error.message || "Error deleting slide.",
});
}
};
// Scroll to the new slide when streaming and new slides are being generated

View file

@ -53,6 +53,7 @@ export const useAutoSave = ({
} catch (error) {
console.error('❌ Auto-save failed:', error);
} finally {
setIsSaving(false);
}

View file

@ -0,0 +1,121 @@
// API Error Response Interface
interface ApiErrorResponse {
detail?: string;
message?: string;
error?: string;
}
// API Response Handler Utility
export class ApiResponseHandler {
static async handleResponse(response: Response, defaultErrorMessage: string): Promise<any> {
// Handle successful responses
if (response.ok) {
// Handle 204 No Content responses
if (response.status === 204) {
return true;
}
// Try to parse JSON response
try {
return await response.json();
} catch (error) {
// If JSON parsing fails but response is ok, return empty object
return {};
}
}
// Handle error responses
let errorMessage = defaultErrorMessage;
try {
const errorData: ApiErrorResponse = await response.json();
// Extract error message in order of preference
if (errorData.detail) {
errorMessage = errorData.detail;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.error) {
errorMessage = errorData.error;
}
} catch (parseError) {
// If JSON parsing fails, use status-based messages
errorMessage = this.getStatusBasedErrorMessage(response.status, defaultErrorMessage);
}
// Throw error with appropriate message
throw new Error(errorMessage);
}
static async handleResponseWithResult(response: Response, defaultErrorMessage: string): Promise<{success: boolean, message?: string}> {
try {
// Handle successful responses
if (response.ok) {
return { success: true };
}
// Handle error responses
let errorMessage = defaultErrorMessage;
try {
const errorData: ApiErrorResponse = await response.json();
// Extract error message in order of preference
if (errorData.detail) {
errorMessage = errorData.detail;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.error) {
errorMessage = errorData.error;
}
} catch (parseError) {
// If JSON parsing fails, use status-based messages
errorMessage = this.getStatusBasedErrorMessage(response.status, defaultErrorMessage);
}
return {
success: false,
message: errorMessage,
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : defaultErrorMessage,
};
}
}
private static getStatusBasedErrorMessage(status: number, defaultMessage: string): string {
switch (status) {
case 400:
return "Bad request. Please check your input and try again.";
case 401:
return "Unauthorized. Please log in and try again.";
case 403:
return "Access forbidden. You don't have permission to perform this action.";
case 404:
return "Resource not found. The requested item may have been deleted or moved.";
case 409:
return "Conflict. The resource already exists or there's a conflict with the current state.";
case 422:
return "Validation error. Please check your input and try again.";
case 429:
return "Too many requests. Please wait a moment and try again.";
case 500:
return "Internal server error. Please try again later.";
case 502:
return "Bad gateway. The server is temporarily unavailable.";
case 503:
return "Service unavailable. Please try again later.";
case 504:
return "Gateway timeout. The request took too long to process.";
default:
return defaultMessage;
}
}
}
export type { ApiErrorResponse };

View file

@ -1,5 +1,7 @@
import { getHeader, getHeaderForFormData } from "./header";
import { IconSearch, ImageGenerate, ImageSearch, PreviousGeneratedImagesResponse } from "./params";
import { ApiResponseHandler } from "./api-error-handler";
export class PresentationGenerationApi {
static async uploadDoc(documents: File[]) {
const formData = new FormData();
@ -19,20 +21,13 @@ export class PresentationGenerationApi {
}
);
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
return data;
return await ApiResponseHandler.handleResponse(response, "Failed to upload documents");
} catch (error) {
console.error("Upload error:", error);
throw error;
}
}
static async decomposeDocuments(documentKeys: string[]) {
try {
const response = await fetch(
@ -46,28 +41,53 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.status === 200) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to decompose files: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to decompose documents");
} catch (error) {
console.error("Error in Decompose Files", error);
throw error;
}
}
static async createPresentation({
prompt,
n_slides,
file_paths,
language,
}: {
prompt: string;
n_slides: number | null;
file_paths?: string[];
language: string | null;
}) {
try {
const response = await fetch(
`/api/v1/ppt/presentation/create`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify({
prompt,
n_slides,
file_paths,
language,
}),
cache: "no-cache",
}
);
return await ApiResponseHandler.handleResponse(response, "Failed to create presentation");
} catch (error) {
console.error("error in presentation creation", error);
throw error;
}
}
static async editSlide(
slide_id: string,
prompt: string
) {
try {
const response = await fetch(
`/api/v1/ppt/slide/edit`,
{
@ -76,18 +96,12 @@ export class PresentationGenerationApi {
body: JSON.stringify({
id: slide_id,
prompt,
}),
cache: "no-cache",
}
);
if (!response.ok) {
throw new Error("Failed to update slides");
}
const data = await response.json();
return data;
return await ApiResponseHandler.handleResponse(response, "Failed to update slide");
} catch (error) {
console.error("error in slide update", error);
throw error;
@ -105,15 +119,8 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(
`Failed to update presentation content: ${response.statusText}`
);
}
return await ApiResponseHandler.handleResponse(response, "Failed to update presentation content");
} catch (error) {
console.error("error in presentation content update", error);
throw error;
@ -131,41 +138,17 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to generate data: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to prepare presentation");
} catch (error) {
console.error("error in data generation", error);
throw error;
}
}
// IMAGE AND ICON SEARCH
static async imageSearch(imageSearch: ImageSearch) {
try {
const response = await fetch(
`/api/v1/ppt/image/search`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify(imageSearch),
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to search images: ${response.statusText}`);
}
} catch (error) {
console.error("error in image search", error);
throw error;
}
}
static async generateImage(imageGenerate: ImageGenerate) {
try {
const response = await fetch(
@ -176,20 +159,15 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to generate images: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to generate image");
} catch (error) {
console.error("error in image generation", error);
throw error;
}
}
static getPreviousGeneratedImages = async():Promise<PreviousGeneratedImagesResponse[]>=>{
static getPreviousGeneratedImages = async (): Promise<PreviousGeneratedImagesResponse[]> => {
try {
const response = await fetch(
`/api/v1/ppt/images/generated`,
@ -198,17 +176,14 @@ export class PresentationGenerationApi {
headers: getHeader(),
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to get previous generated images: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to get previous generated images");
} catch (error) {
console.error("error in getting previous generated images", error);
throw error;
}
}
static async searchIcons(iconSearch: IconSearch) {
try {
const response = await fetch(
@ -219,41 +194,15 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to search icons: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to search icons");
} catch (error) {
console.error("error in icon search", error);
throw error;
}
}
static async updateDocuments(body: any) {
try {
const response = await fetch(
`/api/v1/ppt/document/update`,
{
method: "POST",
headers: getHeaderForFormData(),
body: body,
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to update documents: ${response.statusText}`);
}
} catch (error) {
console.error("error in document update", error);
throw error;
}
}
// EXPORT PRESENTATION
static async exportAsPPTX(presentationData: any) {
@ -267,100 +216,13 @@ export class PresentationGenerationApi {
cache: "no-cache",
}
);
if (response.ok) {
return await response.json();
} else {
throw new Error(`Failed to export as pptx: ${response.statusText}`);
}
return await ApiResponseHandler.handleResponse(response, "Failed to export as PowerPoint");
} catch (error) {
console.error("error in pptx export", error);
throw error;
}
}
static async exportAsPDF(presentationData: any) {
try {
const response = await fetch(
`/api/v1/ppt/presentation/export_as_pdf`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify(presentationData),
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to export as pdf: ${response.statusText}`);
}
} catch (error) {
console.error("error in pdf export", error);
throw error;
}
}
static async deleteSlide(presentation_id: string, slide_id: string) {
try {
const response = await fetch(
`/api/v1/ppt/slide/delete?presentation_id=${presentation_id}&slide_id=${slide_id}`,
{
method: "DELETE",
headers: getHeader(),
cache: "no-cache",
}
);
if (response.status === 204) {
return true;
} else {
throw new Error(`Failed to delete slide: ${response.statusText}`);
}
} catch (error) {
console.error("error in slide deletion", error);
throw error;
}
}
static async createPresentation({
prompt,
n_slides,
file_paths,
language,
}: {
prompt: string;
n_slides: number | null;
file_paths?: string[];
language: string | null;
}) {
try {
const response = await fetch(
`/api/v1/ppt/presentation/create`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify({
prompt,
n_slides,
file_paths,
language,
}),
cache: "no-cache",
}
);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Failed to get questions: ${response.statusText}`);
}
} catch (error) {
console.error("error in question generation", error);
throw error;
}
}
}
}

View file

@ -162,7 +162,7 @@ const UploadPage = () => {
* Handles errors during presentation generation
*/
const handleGenerationError = (error: any) => {
console.error("Error in presentation generation:", error);
console.error("Error in upload page", error);
setLoadingState({
isLoading: false,
message: "",
@ -170,7 +170,7 @@ const UploadPage = () => {
showProgress: false,
});
toast.error("Error", {
description: "Failed to generate presentation. Please try again.",
description: error.message || "Error in upload page.",
});
};

View file

@ -0,0 +1,77 @@
import { NextResponse } from "next/server";
import puppeteer from "puppeteer";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const groupName = searchParams.get("group");
console.log("API called with group:", groupName);
if (!groupName) {
console.warn("No group name provided in query params");
return NextResponse.json({ error: "Missing group name" }, { status: 400 });
}
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(groupName)}`;
console.log("Fetching client page:", schemaPageUrl);
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-web-security"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
await page.goto(schemaPageUrl, {
waitUntil: "networkidle0",
timeout: 80000,
});
await page.waitForSelector("[data-layouts]", { timeout: 10000 });
// Extract both data-layouts and data-group-settings attributes
const { dataLayouts, dataGroupSettings } = await page.$eval(
"[data-layouts]",
(el) => ({
dataLayouts: el.getAttribute("data-layouts"),
dataGroupSettings: el.getAttribute("data-group-settings"),
}),
);
let slides, groupSettings;
try {
slides = JSON.parse(dataLayouts || "[]");
} catch (e) {
console.error("Failed to parse data-layouts JSON:", e);
slides = [];
}
try {
groupSettings = JSON.parse(dataGroupSettings || "null");
} catch (e) {
console.error("Failed to parse data-group-settings JSON:", e);
groupSettings = null;
}
// Compose the response to match PresentationLayoutModel
const response = {
name: groupName,
ordered: groupSettings?.ordered ?? false,
slides: slides.map((slide) => ({
id: slide.id,
name: slide.name,
description: slide.description,
json_schema: slide.json_schema,
})),
};
return NextResponse.json(response);
} catch (err) {
console.error("Error fetching or parsing client page:", err);
return NextResponse.json(
{ error: "Failed to fetch or parse client page" },
{ status: 500 },
);
} finally {
if (browser) await browser.close();
}
}

View file

@ -46,7 +46,7 @@ export async function GET() {
settings = {
description: `${groupName} presentation layouts`,
ordered: false,
isDefault: false
default: false
}
}

View file

@ -1,8 +1,7 @@
import {
getHeader,
} from "@/app/(presentation-generator)/services/api/header";
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
export interface PresentationResponse {
id: string;
@ -33,38 +32,36 @@ export class DashboardApi {
method: "GET",
}
);
if (response.status === 200) {
const data = await response.json();
return data;
} else if (response.status === 404) {
// Handle the special case where 404 means "no presentations found"
if (response.status === 404) {
console.log("No presentations found");
return [];
}
return [];
return await ApiResponseHandler.handleResponse(response, "Failed to fetch presentations");
} catch (error) {
console.error("Error fetching presentations:", error);
throw error;
}
}
static async getPresentation(id: string) {
try {
const response = await fetch(
`/api/v1/ppt/presentation/?id=${id}`,
{
method: "GET",
}
);
if (response.status === 200) {
const data = await response.json();
return data;
}
throw new Error("Presentation not found");
return await ApiResponseHandler.handleResponse(response, "Presentation not found");
} catch (error) {
console.error("Error fetching presentations:", error);
console.error("Error fetching presentation:", error);
throw error;
}
}
static async deletePresentation(presentation_id: string) {
try {
const response = await fetch(
@ -74,17 +71,8 @@ export class DashboardApi {
headers: getHeader(),
}
);
const data = await response.json();
if (response.status === 204) {
return {
success: true,
};
}
return {
success: false,
message: data.detail || "Failed to delete presentation",
};
return await ApiResponseHandler.handleResponseWithResult(response, "Failed to delete presentation");
} catch (error) {
console.error("Error deleting presentation:", error);
return {
@ -93,5 +81,4 @@ export class DashboardApi {
};
}
}
}

View file

@ -100,7 +100,7 @@ const GroupLayoutPreview = () => {
</div>
{/* Layout Content */}
<div className="bg-gray-50">
<div className="bg-gray-50 aspect-video max-w-[1280px] w-full">
<LayoutComponent data={sampleData} />
</div>
</Card>

View file

@ -65,7 +65,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
const groupSettings: GroupSetting = targetGroupData.settings ? targetGroupData.settings : {
description: `${targetGroupData.groupName} presentation layouts`,
ordered: false,
isDefault: false
default: false
}
for (const fileName of targetGroupData.files) {
try {

View file

@ -40,7 +40,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
const groupSettings: GroupSetting = groupData.settings ? groupData.settings : {
description: `${groupData.groupName} presentation layouts`,
ordered: false,
isDefault: false
default: false
}
for (const fileName of groupData.files) {

View file

@ -68,7 +68,7 @@ const LayoutPreview = () => {
<span className="text-xs text-gray-500">
{group.layouts.length} layout{group.layouts.length !== 1 ? 's' : ''}
</span>
{group.settings.isDefault && (
{group.settings.default && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Default
</span>

View file

@ -12,7 +12,7 @@ export interface LayoutInfo {
export interface GroupSetting {
description: string;
ordered: boolean;
isDefault?: boolean;
default?: boolean;
}
export interface LayoutGroup {

View file

@ -1,26 +1,32 @@
'use client'
import React from 'react'
import { useSearchParams } from 'next/navigation'
import { useLayout } from '../(presentation-generator)/context/LayoutContext'
"use client";
import React from "react";
import { useSearchParams } from "next/navigation";
import { useLayout } from "../(presentation-generator)/context/LayoutContext";
const page = () => {
const searchParams = useSearchParams()
const group = searchParams.get('group')
const { getLayoutsByGroup, loading } = useLayout()
if (!group) {
return <div>No group provided</div>
}
const layouts = getLayoutsByGroup(group)
return (
const searchParams = useSearchParams();
const group = searchParams.get("group");
const { getLayoutsByGroup, getGroupSetting, loading } = useLayout();
if (!group) {
return <div>No group provided</div>;
}
const layouts = getLayoutsByGroup(group);
const settings = getGroupSetting(group);
return (
<div>
{loading ? (
<div>Loading...</div>
) : (
<div>
{loading ? (
<div>Loading...</div>
) : (
<div data-layouts={JSON.stringify(layouts)}>
<pre>{JSON.stringify(layouts, null, 2)}</pre>
</div>
)}
<div data-layouts={JSON.stringify(layouts)}>
<pre>{JSON.stringify(layouts, null, 2)}</pre>\
</div>
<div data-settings={JSON.stringify(settings)}>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</div>
</div>
)
}
)}
</div>
);
};
export default page
export default page;

View file

@ -47,7 +47,7 @@ export const Schema = z.object({
type SchemaType = z.infer<typeof Schema>;
export default function ExampleComponent({ data }: { data: SchemaType }) {
export default function ExampleSlideLayout({ data }: { data: SchemaType }) {
const { title, subtitle, metrics, chartImage, trendIcon } = data;
return (
<div className="aspect-video max-w-[1280px] w-full bg-white">

View file

@ -0,0 +1,32 @@
import * as z from "zod";
// Note:
// If you want to use images and icons, you must use ImageSchema and IconSchema
// Images and icons are the only media types supported for PDF and PPTX exports
import { ImageSchema, IconSchema } from "./defaultSchemes";
// Schema definition
export const Schema = z.object({
// Notes:
// Schema fields
// Each field must have a default value (this is important for Layout Preview)
// Each field must have a meta description
// Each field must have a minimum and maximum length
// Each array field must have a minimum and maximum number of items
})
// Type inference
type SchemaType = z.infer<typeof Schema>;
// Component definition
const SlideComponent = ({ data }: { data: Partial<SchemaType> }) => {
// Notes:
// Must have consistent aspect ratio (16:9) and max-width of 1280px.
// Validate each data field before rendering using && operator or optional chaining.
// These layouts are exported as PDF and PPTX so they must be optimized for both formats.
// Content must properly fit in the container, specify min and max constraints in the schema.
// You can check out ExampleComponent.tsx for more details.
};
export default SlideComponent;

View file

@ -1,36 +0,0 @@
import * as z from "zod";
// Note:
// If you want to use Image and Icon Must Use these Schemas.
// Image and icons are only media support for PDF and PPTX.
import { ImageSchema, IconSchema } from "./defaultSchemes";
// Schema definition
export const Schema = z.object({
// Note:
// Schema fields
// Each fields must have a default value (this is important for Layout-preview)
// Each fields must have a meta description
// Each fields must have a min and max length, length must support the layout design.
// Each Array fields must have a min and max length
})
// Type inference
type SchemaType = z.infer<typeof Schema>;
// Component definition
const SlideStructure = ({ data }: { data: Partial<SchemaType> }) => {
// Note:
// Data is already parse so not need to parse again.
// Must have consistent aspect ratio (16:9) and max-width 1280px.
// Validate each data before rendering using && operator or optional chaining
// These layout are exported as PDF and PPTX so must be optimized for both.
// Component design, layout, styles, etc.
// Content must be properly fit in the container, if need adjust the lenght of the content and items in Schema.
// See ExampleComponent.tsx for more details.
// Font Size must fit the desing and and the content must be properly fit in the container.
};
export default SlideStructure;

View file

@ -1,5 +1,5 @@
{
"description": "Default layout for presentations",
"ordered": false,
"isDefault": true
"default": true
}

View file

@ -1,5 +1,5 @@
{
"description": "General purpose layouts for common presentation elements",
"ordered": false,
"isDefault": false
"default": false
}

View file

@ -4,13 +4,13 @@ import * as z from "zod";
export const layoutId = "intro-pitchdeck-slide";
export const layoutName = "Intro Pitch Deck Slide";
export const layoutDescription =
"A visually appealing introduction slide for a pitch deck, featuring a large title, company name, date, and contact information with a modern design.";
"A visually appealing introduction slide for a pitch deck, featuring a large title, company name, date, and contact information with a modern design. This Slide is always the first slide in a pitch deck, setting the tone for the presentation with a clean and professional look.";
const introPitchDeckSchema = z.object({
title: z.string().min(2).max(15).default("Pitch Deck and badu").meta({
title: z.string().min(2).max(15).default("Pitch Deck").meta({
description: "Main title of the slide",
}),
description: z.string().default("").meta({
description: "Empty description as per the design",
description: "Description as per the design",
}),
contactNumber: z.string().default("+123-456-7890").meta({
description: "Contact phone number displayed in footer",
@ -42,7 +42,15 @@ interface IntroSlideLayoutProps {
const IntroPitchDeckSlide: React.FC<IntroSlideLayoutProps> = ({
data: slideData,
}) => {
const { title, description, contactNumber, contactAddress, contactWebsite, companyName, date } = slideData;
const {
title,
description,
contactNumber,
contactAddress,
contactWebsite,
companyName,
date,
} = slideData;
return (
<>
{/* Montserrat Font */}
@ -72,43 +80,53 @@ const IntroPitchDeckSlide: React.FC<IntroSlideLayoutProps> = ({
transform: "translateY(-50%)",
}}
>
{title && <div className="relative inline-block">
<h1
className="text-7xl font-bold text-[#1E4CD9] leading-none"
id="pitchdeck-title"
>
{title}
</h1>
{/* Blue underline */}
<span
className="block bg-[#1E4CD9] h-[4px] absolute left-0"
style={{
width: "100%",
bottom: "-0.5em",
transition: "width 0.3s",
}}
/>
</div>}
{title && (
<div className="relative inline-block">
<h1
className="text-7xl font-bold text-[#1E4CD9] leading-none"
id="pitchdeck-title"
>
{title}
</h1>
{/* Blue underline */}
<span
className="block bg-[#1E4CD9] h-[4px] absolute left-0"
style={{
width: "50%",
bottom: "-0.5em",
transition: "width 0.3s",
}}
/>
</div>
)}
</div>
{/* Bottom Contact Row */}
<div className="absolute bottom-8 left-10 right-10 flex flex-wrap items-center gap-10 text-[#1E4CD9] text-sm font-medium">
{contactNumber && <div className="flex items-center gap-2">
<span className="text-lg">📞</span>
<span>{contactNumber}</span>
</div>}
{contactAddress && <div className="flex items-center gap-2">
<span className="text-lg">📍</span>
<span>{contactAddress}</span>
</div>}
{contactWebsite && <div className="flex items-center gap-2">
<span className="text-lg">🌐</span>
<span>{contactWebsite}</span>
</div>}
{description && <div className="flex items-center gap-2">
<span className="text-lg">💬</span>
<span>{description}</span>
</div>}
{contactNumber && (
<div className="flex items-center gap-2">
<span className="text-lg">📞</span>
<span>{contactNumber}</span>
</div>
)}
{contactAddress && (
<div className="flex items-center gap-2">
<span className="text-lg">📍</span>
<span>{contactAddress}</span>
</div>
)}
{contactWebsite && (
<div className="flex items-center gap-2">
<span className="text-lg">🌐</span>
<span>{contactWebsite}</span>
</div>
)}
{description && (
<div className="flex items-center gap-2">
<span className="text-lg">💬</span>
<span>{description}</span>
</div>
)}
</div>
</div>
</>

View file

@ -13,8 +13,8 @@ const aboutCompanySlideSchema = z.object({
}),
content: z
.string()
.min(50)
.max(500)
.min(25)
.max(400)
.default(
"In the presentation session, the background/introduction can be filled with information that is arranged systematically and effectively with respect to an interesting topic to be used as material for discussion at the opening of the presentation session. The introduction can provide a general overview for those who are listening to your presentation so that the key words on the topic of discussion are emphasized during this background/introductory presentation session.",
)
@ -25,7 +25,7 @@ const aboutCompanySlideSchema = z.object({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
description: "Today Date displayed in header",
}),
image: ImageSchema.optional().meta({
description:
@ -53,7 +53,7 @@ const AboutCompanySlideLayout: React.FC<AboutCompanySlideLayoutProps> = ({
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
className="w-full rounded-sm max-w-[1280px] shadow-lg aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "Montserrat, sans-serif",
}}

View file

@ -83,11 +83,11 @@ const problemStatementSlideSchema = z.object({
description:
"List of problem categories with titles, descriptions, and optional icons",
}),
companyName: z.string().min(2).max(50).default("Rimberio").meta({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
description: "Today Date displayed in header",
}),
});

View file

@ -11,8 +11,8 @@ const solutionSlideSchema = z.object({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date displayed in header",
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
title: z.string().min(3).max(25).default("Businesses struggle").meta({
description: "Main title of the slide",
@ -33,7 +33,7 @@ const solutionSlideSchema = z.object({
title: z.string().min(3).max(30).meta({
description: "Section title",
}),
description: z.string().min(5).max(80).meta({
description: z.string().min(5).max(70).meta({
description: "Section description",
}),
icon: IconSchema.optional().meta({
@ -46,7 +46,8 @@ const solutionSlideSchema = z.object({
.default([
{
title: "Market",
description: "Innovative and widely accepted.",
description:
"Innovative and widely accepted. Innovative and widely accepted. Innovative and widely accepted.",
icon: {
__icon_query__: "market innovation",
__icon_url__:
@ -109,7 +110,7 @@ const SolutionSlideLayout: React.FC<SolutionSlideLayoutProps> = ({
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden border-2 border-gray-800"
className="w-full rounded-sm max-w-[1280px] shadow-lg aspect-video bg-white relative z-20 mx-auto overflow-hidden border-2 border-gray-800"
style={{
fontFamily: "Montserrat, sans-serif",
}}
@ -132,11 +133,11 @@ const SolutionSlideLayout: React.FC<SolutionSlideLayoutProps> = ({
</p>
</div>
{/* Four Small Boxes in a Row */}
<div className="grid grid-cols-2 gap-4 w-full max-w-5xl">
<div className="grid grid-cols-2 gap-4 w-full ">
{sections.map((section, idx) => (
<div
key={idx}
className="flex flex-col items-center text-center bg-[#F5F8FE] rounded-lg shadow px-3 py-4 min-h-[140px] max-h-[160px]"
className="flex flex-col items-center text-center bg-[#F5F8FE] rounded-lg shadow px-3 py-4 "
>
<div className="mb-2">
{section?.icon?.__icon_url__ && (

View file

@ -12,7 +12,7 @@ const productOverviewSlideSchema = z.object({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date displayed in header",
description: "Today Date displayed in header",
}),
title: z.string().min(3).max(40).default("Product Overview").meta({
description: "Main title of the slide",

View file

@ -11,11 +11,11 @@ const marketSizeSlideSchema = z.object({
title: z.string().min(3).max(15).default("Market Size").meta({
description: "Main slide title",
}),
companyName: z.string().min(3).max(30).default("Rimberio").meta({
description: "Presenter's name",
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().default("June 13, 2038").meta({
description: "Presentation date",
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
mapImage: ImageSchema.default({
__image_url__:
@ -51,7 +51,11 @@ const marketSizeSlideSchema = z.object({
description:
"The SOM is a smaller fraction of the SAM that is the target of a serviceable and realistically achievable market in the short to medium term.",
},
]),
])
.meta({
description:
"Market statistics including TAM, SAM, and SOM with labels, values, and descriptions.",
}),
description: z
.string()
.default(

View file

@ -14,10 +14,10 @@ export const layoutDescription =
// Make the schema generic: allow any label/value pairs for comparison
const marketValidationSchema = z.object({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name in header",
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Date in header",
description: "Today Date displayed in header",
}),
title: z.string().min(3).max(20).default("Market Validation").meta({
description: "Title of the slide",
@ -58,7 +58,8 @@ const marketValidationSchema = z.object({
{ label: "Liceria & Co.", metricLabel: "Revenue ($K)", value: 1010 },
])
.meta({
description: "Market benchmark data (generic metric)",
description:
"Comparison data for market validation, allowing flexible labels and values",
}),
image: ImageSchema.optional().meta({
description: "Optional decorative image",

View file

@ -16,24 +16,38 @@ export const layoutName = "Company Traction Slide";
export const layoutDescription =
"A slide layout designed to present company traction data, including growth statistics over the years, a chart visualization, and key metrics in a visually appealing format.";
const growthStatsSchema = z.object({
year: z.string(),
})
.catchall(z.number());
const growthStatsSchema = z
.object({
year: z.string(),
})
.catchall(z.number())
.meta({
description:
"Growth statistics for a specific year, with any number of metrics as key-value pairs where keys are metric names and values are numbers.",
});
// growthStats: list of dicts, each dict is { year: string, <metric1>: number, <metric2>: number, ... }
const tractionSchema = z.object({
companyName: z.string().default("presention"),
date: z.string().default("June 13, 2038"),
title: z.string().default("Company Traction"),
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
title: z.string().default("Company Traction").meta({
description: "Main title of the slide",
}),
description: z
.string()
.min(3)
.max(200)
.default(
"Traction is a period where the company is feeling momentum during its development period. If traction momentum is not harnessed, sales figures can decline and the customer base can shrink. In general, companies will judge success by the amount of revenue and new customers they receive.",
),
)
.meta({
description:
"Main content text describing the company's traction and growth momentum.",
}),
// growthStats is a list of objects, each with a 'year' and any number of metric keys (all numbers)
growthStats: z
.array(growthStatsSchema)
@ -88,7 +102,11 @@ const tractionSchema = z.object({
internetOfThings: 65,
others: 52,
}),
]),
])
.meta({
description:
"Growth statistics for the company, used for chart visualization. Each entry is an object representing a specific year, with the 'year' key as a string (e.g., '2020'), and additional keys for each metric (such as 'artificialIntelligence', 'internetOfThings', 'others'), where the values are numbers representing the metric's value for that year. Example:\n\n[\n { year: '2020', artificialIntelligence: 5, internetOfThings: 10, others: 8 },\n { year: '2021', artificialIntelligence: 10, internetOfThings: 20, others: 15 },\n ...\n]\nThis structure allows the chart to dynamically render multiple series over time, with each metric visualized as a separate line.",
}),
});
export const Schema = tractionSchema;

View file

@ -17,14 +17,22 @@ export const layoutDescription =
"A business model presentation slide displaying CAC metrics and monetization strategy.";
const businessModelSchema = z.object({
companyName: z.string().default("presenton"),
date: z.string().default("June 13, 2038"),
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
title: z.string().min(3).max(20).default("Business Model"),
description: z
.string()
.default(
"Describe how you monetize, who your customers are, your distribution channels or fee structure. The goal is to give an idea of how this business will sustain your product or service and explain how your company will make money and achieve its goals. This can be shown with graphs, statistics, or charts. Use the Lifetime Value (LTV) and Customer Acquisition Cost (CAC) metrics to provide a clearer picture.",
),
)
.meta({
description:
"Description of the business model, monetization strategy, and customer acquisition costs.",
}),
cacChart: z
.array(
z.object({
@ -32,10 +40,19 @@ const businessModelSchema = z.object({
percentage: z.number().min(0).max(100),
}),
)
.min(2)
.max(5)
.default([
{ label: "Internet of Things", percentage: 70 },
{ label: "Artificial Intelligence", percentage: 60 },
]),
{ label: "Blockchain", percentage: 50 },
{ label: "Cloud Computing", percentage: 40 },
{ label: "Cybersecurity", percentage: 30 },
])
.meta({
description:
"Array of objects representing Customer Acquisition Cost (CAC) metrics for different business segments or channels. Each object should include a 'label' (the name of the segment or channel) and a 'percentage' (the CAC as a percentage value, from 0 to 100). This data is visualized in the bar chart to illustrate the distribution of CAC across various categories.",
}),
});
export const Schema = businessModelSchema;
@ -50,9 +67,9 @@ const BusinessModelSlide: React.FC<Props> = ({ data }) => {
data?.cacChart && Array.isArray(data.cacChart) && data.cacChart.length > 0
? data.cacChart
: [
{ label: "Internet of Things", percentage: 70 },
{ label: "Artificial Intelligence", percentage: 60 },
];
{ label: "Internet of Things", percentage: 70 },
{ label: "Artificial Intelligence", percentage: 60 },
];
return (
<>

View file

@ -1,5 +1,5 @@
{
"description": "Modern white and blue business pitch deck layouts with clean, professional design",
"ordered": false,
"isDefault": false
"default": false
}

View file

@ -24,7 +24,7 @@ const teamMemberSchema = z.object({
});
const modernTeamSlideSchema = z.object({
title: z.string().min(3).max(20).default("Our Team").meta({
title: z.string().min(3).max(15).default("Our Team").meta({
description: "Main title of the slide",
}),
subtitle: z.string().min(10).max(120).optional().meta({
@ -83,11 +83,11 @@ const modernTeamSlideSchema = z.object({
.meta({
description: "List of team members with their information",
}),
companyName: z.string().default("presenton").meta({
description: "Company name to display in the header",
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().default("June 13, 2038").meta({
description: "Date to display in the header",
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
});

View file

@ -4,20 +4,20 @@ import * as z from "zod";
export const layoutId = "thank-you-slide";
export const layoutName = "Thank You Slide";
export const layoutDescription =
"A simple, plain thank you slide for closing presentations.";
"A simple, plain thank you slide for closing presentations. This is always the last slide in a presentation, providing a clean and professional closing message with company contact information.";
const thankYouSlideSchema = z.object({
title: z.string().min(3).max(40).default("Thank You!").meta({
title: z.string().min(3).max(30).default("Thank You!").meta({
description: "Main thank you message",
}),
subtitle: z.string().min(0).max(100).default("").meta({
description: "Optional subtitle or closing remark",
}),
companyName: z.string().min(2).max(30).default("Rimberio").meta({
companyName: z.string().min(2).max(50).default("presenton").meta({
description: "Company name displayed in header",
}),
date: z.string().min(5).max(30).default("June 13, 2038").meta({
description: "Date displayed in header",
date: z.string().min(5).max(50).default("June 13, 2038").meta({
description: "Today Date displayed in header",
}),
address: z
.string()
@ -72,10 +72,7 @@ const ThankYouSlideLayout: React.FC<ThankYouSlideLayoutProps> = ({ data }) => {
<div className="flex flex-1 flex-col px-16 pb-16 justify-between">
{/* Thank You and description */}
<div className="flex flex-col items-start w-full pt-16">
<h1
className="font-bold text-8xl text-white mb-6 mt-8 text-left w-full"
>
<h1 className="font-bold text-8xl text-white mb-6 mt-8 text-left w-full">
{data?.title || "Thank You!"}
</h1>
{data?.subtitle && (
@ -83,7 +80,6 @@ const ThankYouSlideLayout: React.FC<ThankYouSlideLayoutProps> = ({ data }) => {
{data.subtitle}
</div>
)}
</div>
{/* Footer area */}

View file

@ -1,5 +1,5 @@
{
"description": "Professional presentation layouts with clean design and flexible content fields. Suitable for business pitches, organizational overviews, product presentations, and various corporate communications.",
"ordered": false,
"isDefault": false
"default": false
}