feat(api): implement presentation generation endpoint with layout and export options
This commit is contained in:
parent
7ac578224a
commit
3fe860d431
7 changed files with 576 additions and 59 deletions
|
|
@ -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}",
|
||||
)
|
||||
|
|
|
|||
19
servers/fastapi/models/generate_presentation_api.py
Normal file
19
servers/fastapi/models/generate_presentation_api.py
Normal 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
|
||||
18
servers/fastapi/services/get_layout_by_name.py
Normal file
18
servers/fastapi/services/get_layout_by_name.py
Normal 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)
|
||||
189
servers/fastapi/tests/test_presentation_generation_api.py
Normal file
189
servers/fastapi/tests/test_presentation_generation_api.py
Normal 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
|
||||
77
servers/nextjs/app/api/layout/route.ts
Normal file
77
servers/nextjs/app/api/layout/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue