feat(fastapi): adds slide assets generation, adds dict and schema processors, fix(nextjs): changes image and icon schema fields to __xxxxx__
This commit is contained in:
parent
ede81ab9db
commit
03b2b06ff0
44 changed files with 331 additions and 316 deletions
|
|
@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y \
|
|||
WORKDIR /app
|
||||
|
||||
# Set environment variables
|
||||
ENV APP_DATA_DIRECTORY=/app/user_data
|
||||
ENV APP_DATA_DIRECTORY=/app_data
|
||||
ENV TEMP_DIRECTORY=/tmp/presenton
|
||||
|
||||
# Install ollama
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ WORKDIR /app
|
|||
RUN ls -a
|
||||
|
||||
# Set environment variables
|
||||
ENV APP_DATA_DIRECTORY=/app/user_data
|
||||
ENV APP_DATA_DIRECTORY=/app_data
|
||||
ENV TEMP_DIRECTORY=/tmp/presenton
|
||||
|
||||
# Install ollama
|
||||
|
|
|
|||
BIN
app_data/fastapi.db
Normal file
BIN
app_data/fastapi.db
Normal file
Binary file not shown.
BIN
app_data/images/15b080fb-5b93-4e83-a501-17eef22e8f9c.jpg
Normal file
BIN
app_data/images/15b080fb-5b93-4e83-a501-17eef22e8f9c.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 873 KiB |
BIN
app_data/images/3628f28d-7d7b-430d-bd57-13b51c28d55e.jpg
Normal file
BIN
app_data/images/3628f28d-7d7b-430d-bd57-13b51c28d55e.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 MiB |
BIN
app_data/images/8b200426-6974-47d7-93be-626c4b5b122b.jpg
Normal file
BIN
app_data/images/8b200426-6974-47d7-93be-626c4b5b122b.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
1
app_data/settings.json
Normal file
1
app_data/settings.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
1
app_data/userConfig.json
Normal file
1
app_data/userConfig.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"LLM":"google","GOOGLE_API_KEY":"AIzaSyAFY4MWd8aE7L4qWkFdrqpDAMgO2M63Cc4","OLLAMA_URL":"http://localhost:11434","USE_CUSTOM_URL":false}
|
||||
|
|
@ -8,7 +8,7 @@ services:
|
|||
# You can replace 5000 with any other port number of your choice to run Presenton on a different port number.
|
||||
- "5000:80"
|
||||
volumes:
|
||||
- ./user_data:/app/user_data
|
||||
- ./app_data:/app_data
|
||||
environment:
|
||||
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
|
||||
- LLM=${LLM}
|
||||
|
|
@ -38,7 +38,7 @@ services:
|
|||
# You can replace 5000 with any other port number of your choice to run Presenton on a different port number.
|
||||
- "5000:80"
|
||||
volumes:
|
||||
- ./user_data:/app/user_data
|
||||
- ./app_data:/app_data
|
||||
environment:
|
||||
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
|
||||
- LLM=${LLM}
|
||||
|
|
@ -62,6 +62,7 @@ services:
|
|||
- "8000:8000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./app_data:/app_data
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
|
||||
|
|
@ -93,6 +94,7 @@ services:
|
|||
- "8000:8000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./app_data:/app_data
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
|
||||
|
|
|
|||
12
nginx.conf
12
nginx.conf
|
|
@ -25,5 +25,17 @@ http {
|
|||
proxy_read_timeout 30m;
|
||||
proxy_connect_timeout 30m;
|
||||
}
|
||||
|
||||
location /static {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_read_timeout 30m;
|
||||
proxy_connect_timeout 30m;
|
||||
}
|
||||
|
||||
location /app_data {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_read_timeout 30m;
|
||||
proxy_connect_timeout 30m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ from fastapi.staticfiles import StaticFiles
|
|||
from api.lifespan import app_lifespan
|
||||
from api.middlewares import UserConfigEnvUpdateMiddleware
|
||||
from api.v1.ppt.router import API_V1_PPT_ROUTER
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
|
||||
|
||||
app = FastAPI(lifespan=app_lifespan)
|
||||
|
|
@ -14,7 +15,11 @@ app.include_router(API_V1_PPT_ROUTER)
|
|||
|
||||
# Static files
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
# APP.mount("/static/app-data", StaticFiles(directory=get_app_data_directory_env()))
|
||||
app.mount(
|
||||
"/app_data/images",
|
||||
StaticFiles(directory=get_images_directory()),
|
||||
name="app_data/images",
|
||||
)
|
||||
|
||||
|
||||
# Middlewares
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import os
|
||||
from fastapi import APIRouter, Body
|
||||
from fastapi import APIRouter
|
||||
|
||||
from models.image_prompt import ImagePrompt
|
||||
from services.image_generation_service import ImageGenerationService
|
||||
from utils.get_env import get_app_data_directory_env
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
|
||||
IMAGES_ROUTER = APIRouter(prefix="/images", tags=["Images"])
|
||||
|
||||
|
||||
@IMAGES_ROUTER.get("/generate")
|
||||
async def generate_image(prompt: str):
|
||||
images_directory = os.path.join(get_app_data_directory_env(), "images")
|
||||
images_directory = get_images_directory()
|
||||
image_prompt = ImagePrompt(prompt=prompt)
|
||||
image_generation_service = ImageGenerationService(images_directory)
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,8 @@ async def stream_presentation(presentation_id: str):
|
|||
layout = presentation.get_layout()
|
||||
outline = presentation.get_presentation_outline()
|
||||
|
||||
asyncio_tasks = []
|
||||
# These tasks will be gathered and awaited after all slides are generated
|
||||
async_assets_generation_tasks = []
|
||||
|
||||
slides: List[SlideModel] = []
|
||||
yield SSEResponse(
|
||||
|
|
@ -206,7 +207,7 @@ async def stream_presentation(presentation_id: str):
|
|||
content=slide_content,
|
||||
)
|
||||
slides.append(slide)
|
||||
asyncio_tasks.append(process_slide_and_fetch_assets(slide))
|
||||
async_assets_generation_tasks.append(process_slide_and_fetch_assets(slide))
|
||||
yield SSEResponse(
|
||||
event="response",
|
||||
data=json.dumps({"type": "chunk", "chunk": slide.model_dump_json()}),
|
||||
|
|
@ -217,11 +218,15 @@ async def stream_presentation(presentation_id: str):
|
|||
data=json.dumps({"type": "chunk", "chunk": " ] }"}),
|
||||
).to_string()
|
||||
|
||||
await asyncio.gather(*asyncio_tasks)
|
||||
generated_assets_lists = await asyncio.gather(*async_assets_generation_tasks)
|
||||
generated_assets = []
|
||||
for assets_list in generated_assets_lists:
|
||||
generated_assets.extend(assets_list)
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
sql_session.add_all(slides)
|
||||
sql_session.add_all(generated_assets)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
for each_slide in slides:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from pydantic import BaseModel, Field, HttpUrl, EmailStr
|
|||
|
||||
from models.presentation_layout import PresentationLayoutModel, SlideLayoutModel
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
from services.schema_processor import SchemaProcessor
|
||||
from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key
|
||||
from utils.schema_utils import remove_fields_from_schema
|
||||
|
||||
|
||||
class ContactInfoModel(BaseModel):
|
||||
|
|
@ -16,9 +17,8 @@ class ContactInfoModel(BaseModel):
|
|||
|
||||
|
||||
class ImageModel(BaseModel):
|
||||
url: str = Field(description="Image URL")
|
||||
__image_type__: Literal["image"] = "image"
|
||||
prompt: str = Field(description="Image prompt")
|
||||
image_url__: str = Field(description="Image URL")
|
||||
image_prompt__: str = Field(description="Image prompt")
|
||||
|
||||
|
||||
# First Slide Layout
|
||||
|
|
@ -417,14 +417,9 @@ presentation_layout = PresentationLayoutModel(
|
|||
|
||||
# print(json.dumps(FirstSlideModel.model_json_schema()))
|
||||
|
||||
# slide_schema = FirstSlideModel.model_json_schema()
|
||||
|
||||
# schema_processor = SchemaProcessor()
|
||||
# print(
|
||||
# json.dumps(
|
||||
# schema_processor.remove_image_url_fields(FirstSlideModel.model_json_schema())
|
||||
# )
|
||||
# )
|
||||
slide_schema = FirstSlideModel.model_json_schema()
|
||||
|
||||
slide_schema = remove_fields_from_schema(slide_schema, ["image_url__"])
|
||||
print(slide_schema)
|
||||
|
||||
# print(PresentationOutlineModel.model_json_schema())
|
||||
|
|
|
|||
14
servers/fastapi/models/json_path_guide.py
Normal file
14
servers/fastapi/models/json_path_guide.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DictGuide(BaseModel):
|
||||
key: str
|
||||
|
||||
|
||||
class ListGuide(BaseModel):
|
||||
index: int
|
||||
|
||||
|
||||
class JsonPathGuide(BaseModel):
|
||||
guides: List[DictGuide | ListGuide]
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
from utils.randomizers import get_random_uuid
|
||||
|
||||
|
||||
class ImageAsset(SQLModel, table=True):
|
||||
id: str = Field(default=get_random_uuid, primary_key=True)
|
||||
prompt: Optional[str] = Field(default=None)
|
||||
path: str
|
||||
id: str = Field(default_factory=get_random_uuid, primary_key=True)
|
||||
created_at: datetime = Field(default=datetime.now())
|
||||
path: str
|
||||
extras: Optional[dict] = Field(sa_column=Column(JSON), default=None)
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
from services.schema_processor import SchemaProcessor
|
||||
from services.temp_file_service import TempFileService
|
||||
from services.database import sql_engine
|
||||
|
||||
|
||||
SCHEMA_PROCESSOR = SchemaProcessor()
|
||||
TEMP_FILE_SERVICE = TempFileService()
|
||||
SQL_ENGINE = sql_engine
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import aiohttp
|
|||
from google import genai
|
||||
from google.genai.types import GenerateContentConfig
|
||||
from models.image_prompt import ImagePrompt
|
||||
from models.sql.image_asset import ImageAsset
|
||||
from utils.download_helpers import download_file
|
||||
from utils.get_env import get_pexels_api_key_env
|
||||
from utils.llm_provider import (
|
||||
|
|
@ -18,7 +19,6 @@ class ImageGenerationService:
|
|||
|
||||
def __init__(self, output_directory: str):
|
||||
self.output_directory = output_directory
|
||||
os.makedirs(output_directory, exist_ok=True)
|
||||
|
||||
self.use_pexels = False
|
||||
if get_pexels_api_key_env():
|
||||
|
|
@ -35,7 +35,7 @@ class ImageGenerationService:
|
|||
return self.generate_image_openai
|
||||
return None
|
||||
|
||||
async def generate_image(self, prompt: ImagePrompt) -> str:
|
||||
async def generate_image(self, prompt: ImagePrompt) -> str | ImageAsset:
|
||||
if not self.image_gen_func:
|
||||
print("No image generation function found. Using placeholder image.")
|
||||
return "/static/images/placeholder.jpg"
|
||||
|
|
@ -45,8 +45,17 @@ class ImageGenerationService:
|
|||
|
||||
try:
|
||||
image_path = await self.image_gen_func(image_prompt, self.output_directory)
|
||||
if image_path and os.path.exists(image_path):
|
||||
return image_path
|
||||
if image_path:
|
||||
if image_path.startswith("http"):
|
||||
return image_path
|
||||
elif os.path.exists(image_path):
|
||||
return ImageAsset(
|
||||
path=image_path,
|
||||
extras={
|
||||
"prompt": prompt.prompt,
|
||||
"theme_prompt": prompt.theme_prompt,
|
||||
},
|
||||
)
|
||||
raise Exception(f"Image not found at {image_path}")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -84,7 +93,7 @@ class ImageGenerationService:
|
|||
|
||||
return image_path
|
||||
|
||||
async def get_image_from_pexels(self, prompt: str, output_directory: str) -> str:
|
||||
async def get_image_from_pexels(self, prompt: str) -> str:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await session.get(
|
||||
f"https://api.pexels.com/v1/search?query={prompt}&per_page=1",
|
||||
|
|
@ -92,4 +101,4 @@ class ImageGenerationService:
|
|||
)
|
||||
data = await response.json()
|
||||
image_url = data["photos"][0]["src"]["large"]
|
||||
return await download_file(image_url, output_directory)
|
||||
return image_url
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
from __future__ import annotations
|
||||
from copy import deepcopy
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class SchemaProcessor:
|
||||
|
||||
def resolve_refs(self, schema, defs):
|
||||
if isinstance(schema, dict):
|
||||
if "$ref" in schema:
|
||||
ref_path = schema["$ref"]
|
||||
if ref_path.startswith("#/$defs/"):
|
||||
def_key = ref_path.replace("#/$defs/", "")
|
||||
return self.resolve_refs(defs[def_key], defs)
|
||||
else:
|
||||
raise ValueError(f"Unsupported $ref path: {ref_path}")
|
||||
else:
|
||||
return {k: self.resolve_refs(v, defs) for k, v in schema.items()}
|
||||
elif isinstance(schema, list):
|
||||
return [self.resolve_refs(item, defs) for item in schema]
|
||||
else:
|
||||
return schema
|
||||
|
||||
def flatten_schema(self, schema):
|
||||
schema = deepcopy(schema)
|
||||
defs = schema.pop("$defs", {})
|
||||
return self.resolve_refs(schema, defs)
|
||||
|
||||
def find_dict_with_key(
|
||||
self, data: dict, target_key: str, current_path: Optional[List[str]] = None
|
||||
) -> List[List[str]]:
|
||||
if current_path is None:
|
||||
current_path = []
|
||||
paths = []
|
||||
if target_key in data:
|
||||
paths.append(current_path.copy())
|
||||
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
new_path = current_path + [key]
|
||||
paths.extend(self.find_dict_with_key(value, target_key, new_path))
|
||||
elif isinstance(value, list):
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, dict):
|
||||
new_path = current_path + [key, str(i)]
|
||||
paths.extend(
|
||||
self.find_dict_with_key(item, target_key, new_path)
|
||||
)
|
||||
return paths
|
||||
|
||||
def get_dict_at_path(self, data: dict, path: List[str]) -> dict:
|
||||
current = data
|
||||
|
||||
for part in path:
|
||||
if part.isdigit():
|
||||
current = current[int(part)]
|
||||
else:
|
||||
current = current[part]
|
||||
|
||||
return current
|
||||
|
||||
def set_dict_at_path(self, data: dict, path: List[str], value) -> None:
|
||||
if not path:
|
||||
raise ValueError("Path cannot be empty")
|
||||
|
||||
current = data
|
||||
|
||||
# Navigate to the parent of the target location
|
||||
for part in path[:-1]:
|
||||
if part.isdigit():
|
||||
index = int(part)
|
||||
if index >= len(current):
|
||||
# Extend list if needed
|
||||
current.extend([{}] * (index - len(current) + 1))
|
||||
current = current[index]
|
||||
else:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
|
||||
# Set the value at the final path component
|
||||
final_part = path[-1]
|
||||
if final_part.isdigit():
|
||||
index = int(final_part)
|
||||
if index >= len(current):
|
||||
# Extend list if needed
|
||||
current.extend([None] * (index - len(current) + 1))
|
||||
current[index] = value
|
||||
else:
|
||||
current[final_part] = value
|
||||
|
||||
def remove_image_url_fields(self, data: dict) -> dict:
|
||||
copied_data = data.copy()
|
||||
|
||||
image_type_paths = self.find_dict_with_key(copied_data, "__image_type__")
|
||||
|
||||
for path in image_type_paths:
|
||||
dict_at_path = self.get_dict_at_path(copied_data, path)
|
||||
if "properties" in dict_at_path:
|
||||
del dict_at_path["properties"]["url"]
|
||||
dict_at_parent_path = self.get_dict_at_path(copied_data, path[:-1])
|
||||
if "required" in dict_at_parent_path:
|
||||
dict_at_parent_path["required"].remove("url")
|
||||
|
||||
return copied_data
|
||||
|
|
@ -67,4 +67,4 @@ def test_gemini_schema_support():
|
|||
response_schema=TwoColumnSlideModel.model_json_schema(),
|
||||
),
|
||||
)
|
||||
print(response.parsed)
|
||||
print(response.text)
|
||||
|
|
|
|||
8
servers/fastapi/utils/asset_directory_utils.py
Normal file
8
servers/fastapi/utils/asset_directory_utils.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import os
|
||||
from utils.get_env import get_app_data_directory_env
|
||||
|
||||
|
||||
def get_images_directory():
|
||||
images_directory = os.path.join(get_app_data_directory_env(), "images")
|
||||
os.makedirs(images_directory, exist_ok=True)
|
||||
return images_directory
|
||||
48
servers/fastapi/utils/dict_utils.py
Normal file
48
servers/fastapi/utils/dict_utils.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from typing import List
|
||||
|
||||
from models.json_path_guide import JsonPathGuide, DictGuide, ListGuide
|
||||
|
||||
|
||||
def get_dict_paths_with_key(data: dict, key: str) -> List[JsonPathGuide]:
|
||||
result = []
|
||||
|
||||
def _find_paths(obj, current_path: List[DictGuide | ListGuide]):
|
||||
if isinstance(obj, dict):
|
||||
if key in obj:
|
||||
result.append(JsonPathGuide(guides=current_path.copy()))
|
||||
for k, v in obj.items():
|
||||
new_path = current_path + [DictGuide(key=k)]
|
||||
_find_paths(v, new_path)
|
||||
elif isinstance(obj, list):
|
||||
for i, item in enumerate(obj):
|
||||
new_path = current_path + [ListGuide(index=i)]
|
||||
_find_paths(item, new_path)
|
||||
|
||||
_find_paths(data, [])
|
||||
return result
|
||||
|
||||
|
||||
def get_dict_at_path(data: dict, path: JsonPathGuide) -> dict:
|
||||
current = data
|
||||
for guide in path.guides:
|
||||
if isinstance(guide, DictGuide):
|
||||
current = current[guide.key]
|
||||
elif isinstance(guide, ListGuide):
|
||||
current = current[guide.index]
|
||||
return current
|
||||
|
||||
|
||||
def set_dict_at_path(data: dict, path: JsonPathGuide, value: dict):
|
||||
current = data
|
||||
for guide in path.guides[:-1]:
|
||||
if isinstance(guide, DictGuide):
|
||||
current = current[guide.key]
|
||||
elif isinstance(guide, ListGuide):
|
||||
current = current[guide.index]
|
||||
|
||||
if path.guides:
|
||||
final_guide = path.guides[-1]
|
||||
if isinstance(final_guide, DictGuide):
|
||||
current[final_guide.key] = value
|
||||
elif isinstance(final_guide, ListGuide):
|
||||
current[final_guide.index] = value
|
||||
|
|
@ -3,14 +3,13 @@ import json
|
|||
from google.genai.types import GenerateContentConfig
|
||||
from models.presentation_layout import SlideLayoutModel
|
||||
from models.presentation_outline_model import SlideOutlineModel
|
||||
from services import SCHEMA_PROCESSOR
|
||||
from services.schema_processor import SchemaProcessor
|
||||
from utils.llm_provider import (
|
||||
get_google_llm_client,
|
||||
get_llm_client,
|
||||
get_small_model,
|
||||
is_google_selected,
|
||||
)
|
||||
from utils.schema_utils import remove_fields_from_schema
|
||||
|
||||
system_prompt = """
|
||||
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
|
||||
|
|
@ -66,7 +65,9 @@ async def get_slide_content_from_type_and_outline(
|
|||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "SlideContent",
|
||||
"schema": slide_layout.json_schema,
|
||||
"schema": remove_fields_from_schema(
|
||||
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,60 @@
|
|||
import os
|
||||
import asyncio
|
||||
from typing import List, Tuple
|
||||
from models.presentation_layout import SlideLayoutModel
|
||||
from models.sql.asset import ImageAsset
|
||||
from models.image_prompt import ImagePrompt
|
||||
from models.sql.image_asset import ImageAsset
|
||||
from models.sql.slide import SlideModel
|
||||
from services import SCHEMA_PROCESSOR
|
||||
from services.icon_finder_service import IconFinderService
|
||||
from services.image_generation_service import ImageGenerationService
|
||||
from utils.get_env import get_app_data_directory_env
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key, set_dict_at_path
|
||||
|
||||
|
||||
async def process_slide_and_fetch_assets(
|
||||
slide: SlideModel, layout: SlideLayoutModel
|
||||
) -> SlideModel:
|
||||
image_directory = os.path.join(get_app_data_directory_env(), "images")
|
||||
slide: SlideModel,
|
||||
) -> List[ImageAsset]:
|
||||
image_directory = get_images_directory()
|
||||
|
||||
image_generation_service = ImageGenerationService(image_directory)
|
||||
icon_finder_service = IconFinderService()
|
||||
|
||||
image_type_paths = SCHEMA_PROCESSOR.find_dict_with_key(
|
||||
slide.content, "__image_type__"
|
||||
)
|
||||
for path in image_type_paths:
|
||||
image_dict = SCHEMA_PROCESSOR.get_dict_at_path(slide.content, path)
|
||||
image_prompt = image_dict["prompt"]
|
||||
if image_dict["__image_type__"] == "image":
|
||||
image_path = await image_generation_service.generate_image(image_prompt)
|
||||
image_dict["url"] = image_path
|
||||
else:
|
||||
icon_path = await icon_finder_service.search_icons(image_prompt)
|
||||
image_dict["url"] = icon_path[0]
|
||||
async_tasks = []
|
||||
|
||||
SCHEMA_PROCESSOR.set_dict_at_path(slide.content, path, image_dict)
|
||||
image_paths = get_dict_paths_with_key(slide.content, "__image_prompt__")
|
||||
icon_paths = get_dict_paths_with_key(slide.content, "__icon_query__")
|
||||
|
||||
for image_path in image_paths:
|
||||
image_prompt_parent = get_dict_at_path(slide.content, image_path)
|
||||
async_tasks.append(
|
||||
image_generation_service.generate_image(
|
||||
ImagePrompt(
|
||||
prompt=image_prompt_parent["__image_prompt__"],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for icon_path in icon_paths:
|
||||
icon_query_parent = get_dict_at_path(slide.content, icon_path)
|
||||
async_tasks.append(
|
||||
icon_finder_service.search_icons(icon_query_parent["__icon_query__"])
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*async_tasks)
|
||||
results.reverse()
|
||||
|
||||
return_assets = []
|
||||
for image_path in image_paths:
|
||||
image_dict = get_dict_at_path(slide.content, image_path)
|
||||
result = results.pop()
|
||||
if isinstance(result, ImageAsset):
|
||||
return_assets.append(result)
|
||||
image_dict["__image_url__"] = result.path
|
||||
else:
|
||||
image_dict["__image_url__"] = result
|
||||
set_dict_at_path(slide.content, image_path, image_dict)
|
||||
|
||||
for icon_path in icon_paths:
|
||||
icon_dict = get_dict_at_path(slide.content, icon_path)
|
||||
icon_dict["__icon_url__"] = results.pop()[0]
|
||||
set_dict_at_path(slide.content, icon_path, icon_dict)
|
||||
|
||||
return return_assets
|
||||
|
|
|
|||
50
servers/fastapi/utils/schema_utils.py
Normal file
50
servers/fastapi/utils/schema_utils.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from copy import deepcopy
|
||||
from typing import List
|
||||
|
||||
from utils.dict_utils import get_dict_paths_with_key, get_dict_at_path, set_dict_at_path
|
||||
|
||||
|
||||
def resolve_refs(schema, defs):
|
||||
if isinstance(schema, dict):
|
||||
if "$ref" in schema:
|
||||
ref_path = schema["$ref"]
|
||||
if ref_path.startswith("#/$defs/"):
|
||||
def_key = ref_path.replace("#/$defs/", "")
|
||||
return resolve_refs(defs[def_key], defs)
|
||||
else:
|
||||
raise ValueError(f"Unsupported $ref path: {ref_path}")
|
||||
else:
|
||||
return {k: resolve_refs(v, defs) for k, v in schema.items()}
|
||||
elif isinstance(schema, list):
|
||||
return [resolve_refs(item, defs) for item in schema]
|
||||
else:
|
||||
return schema
|
||||
|
||||
|
||||
def flatten_schema(schema):
|
||||
schema = deepcopy(schema)
|
||||
defs = schema.pop("$defs", {})
|
||||
return resolve_refs(schema, defs)
|
||||
|
||||
|
||||
def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):
|
||||
schema = deepcopy(schema)
|
||||
properties_paths = get_dict_paths_with_key(schema, "properties")
|
||||
for path in properties_paths:
|
||||
parent_obj = get_dict_at_path(schema, path)
|
||||
if "properties" in parent_obj and isinstance(parent_obj["properties"], dict):
|
||||
for field in fields_to_remove:
|
||||
if field in parent_obj["properties"]:
|
||||
del parent_obj["properties"][field]
|
||||
|
||||
required_paths = get_dict_paths_with_key(schema, "required")
|
||||
for path in required_paths:
|
||||
parent_obj = get_dict_at_path(schema, path)
|
||||
if "required" in parent_obj and isinstance(parent_obj["required"], list):
|
||||
parent_obj["required"] = [
|
||||
field
|
||||
for field in parent_obj["required"]
|
||||
if field not in fields_to_remove
|
||||
]
|
||||
|
||||
return schema
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { filepath: string[] } },
|
||||
) {
|
||||
const BASE_DIR = "/app";
|
||||
|
||||
const filepath = params.filepath.join("/");
|
||||
|
||||
if (!params.filepath) {
|
||||
return new NextResponse('No file specified', { status: 400 });
|
||||
}
|
||||
|
||||
const filePath = path.join(BASE_DIR, filepath);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return new NextResponse('File not found', { status: 404 });
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
return new NextResponse('Access to directories is forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Disposition', `inline; filename="${path.basename(filePath)}"`);
|
||||
headers.set('Content-Type', getMimeType(filePath));
|
||||
|
||||
return new NextResponse(fileStream as any, { headers });
|
||||
}
|
||||
|
||||
function getMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.txt': return 'text/plain';
|
||||
case '.json': return 'application/json';
|
||||
case '.jpg':
|
||||
case '.jpeg': return 'image/jpeg';
|
||||
case '.png': return 'image/png';
|
||||
case '.pdf': return 'application/pdf';
|
||||
default: return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ const BulletPointSlideLayout: React.FC<BulletPointSlideLayoutProps> = ({ data: s
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ const cardSlideSchema = z.object({
|
|||
}),
|
||||
cards: z.array(z.object({
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Default card icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Default card icon'
|
||||
}).meta({
|
||||
description: "Icon for the card",
|
||||
}),
|
||||
|
|
@ -29,24 +29,24 @@ const cardSlideSchema = z.object({
|
|||
})).min(2).max(6).default([
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Lightning fast icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Lightning fast icon'
|
||||
},
|
||||
title: 'Lightning Fast',
|
||||
description: 'Optimized performance for quick results and seamless user experience'
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'Secure and safe icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__icon_query__: 'Secure and safe icon'
|
||||
},
|
||||
title: 'Secure & Safe',
|
||||
description: 'Enterprise-grade security with advanced encryption and protection'
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Precise targeting icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__icon_query__: 'Precise targeting icon'
|
||||
},
|
||||
title: 'Precise Targeting',
|
||||
description: 'Advanced analytics to reach your exact audience with precision'
|
||||
|
|
@ -73,7 +73,7 @@ const CardSlideLayout: React.FC<CardSlideLayoutProps> = ({ data: slideData }) =>
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.url})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
@ -108,8 +108,8 @@ const CardSlideLayout: React.FC<CardSlideLayoutProps> = ({ data: slideData }) =>
|
|||
<div className="mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<div className="w-16 h-16 mx-auto bg-blue-100 rounded-xl flex items-center justify-center overflow-hidden print:w-12 print:h-12">
|
||||
<img
|
||||
src={card.icon?.url || ''}
|
||||
alt={card.icon?.prompt || card.title}
|
||||
src={card.icon?.__icon_url__ || ''}
|
||||
alt={card.icon?.__icon_query__ || card.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const ComparisonSlideLayout: React.FC<ComparisonSlideLayoutProps> = ({ data: sli
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.url})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const ContentSlideLayout: React.FC<ContentSlideLayoutProps> = ({ data: slideData
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData?.backgroundImage})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData?.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const FirstSlideLayout: React.FC<FirstSlideLayoutProps> = ({ data: slideData })
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ const iconSlideSchema = z.object({
|
|||
description: "Optional subtitle or description",
|
||||
}),
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'A beautiful road in the mountains'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'A beautiful road in the mountains'
|
||||
}).meta({
|
||||
description: "Main slide icon",
|
||||
}),
|
||||
|
|
@ -55,8 +55,8 @@ const IconSlideLayout: React.FC<IconSlideLayoutProps> = ({ data: slideData }) =>
|
|||
<div className="relative mb-8 p-8 rounded-3xl bg-white border-2 shadow-xl print:shadow-md">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-600 to-blue-800 opacity-5 rounded-3xl"></div>
|
||||
<img
|
||||
src={slideData?.icon?.url || ''}
|
||||
alt={slideData?.icon?.prompt || ''}
|
||||
src={slideData?.icon?.__icon_url__ || ''}
|
||||
alt={slideData?.icon?.__icon_query__ || ''}
|
||||
className="w-24 h-24 object-contain relative z-10 print:w-20 print:h-20"
|
||||
/>
|
||||
<div className="absolute -bottom-2 -right-2 w-6 h-6 bg-gradient-to-br from-blue-600 to-blue-800 rounded-full opacity-80"></div>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ const imageSlideSchema = z.object({
|
|||
description: "Main description text",
|
||||
}),
|
||||
image: ImageSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'A beautiful road in the mountains'
|
||||
__image_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__image_prompt__: 'A beautiful road in the mountains'
|
||||
}).meta({
|
||||
description: "Main slide image",
|
||||
}),
|
||||
|
|
@ -47,8 +47,8 @@ const ImageSlideLayout: React.FC<ImageSlideLayoutProps> = ({ data: slideData })
|
|||
{/* Left panel - Image */}
|
||||
<div className="flex-1 relative">
|
||||
<img
|
||||
src={slideData?.image?.url || ''}
|
||||
alt={slideData?.image?.prompt || ''}
|
||||
src={slideData?.image?.__image_url__ || ''}
|
||||
alt={slideData?.image?.__image_prompt__ || ''}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Overlay gradient */}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ const processSlideSchema = z.object({
|
|||
}),
|
||||
steps: z.array(z.object({
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Default step icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Default step icon'
|
||||
}).meta({
|
||||
description: "Icon for the step",
|
||||
}),
|
||||
|
|
@ -29,24 +29,24 @@ const processSlideSchema = z.object({
|
|||
})).min(2).max(6).default([
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Plan and strategy icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Plan and strategy icon'
|
||||
},
|
||||
title: 'Plan & Strategy',
|
||||
description: 'Define objectives, analyze requirements, and create a comprehensive roadmap'
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'Execute and build icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__icon_query__: 'Execute and build icon'
|
||||
},
|
||||
title: 'Execute & Build',
|
||||
description: 'Implement solutions with precision using cutting-edge technology and best practices'
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Launch and optimize icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__icon_query__: 'Launch and optimize icon'
|
||||
},
|
||||
title: 'Launch & Optimize',
|
||||
description: 'Deploy the solution and continuously improve based on performance metrics'
|
||||
|
|
@ -73,7 +73,7 @@ const ProcessSlideLayout: React.FC<ProcessSlideLayoutProps> = ({ data: slideData
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.url})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
@ -110,8 +110,8 @@ const ProcessSlideLayout: React.FC<ProcessSlideLayoutProps> = ({ data: slideData
|
|||
</div>
|
||||
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-white rounded-full flex items-center justify-center shadow-lg print:w-6 print:h-6">
|
||||
<img
|
||||
src={step.icon?.url || ''}
|
||||
alt={step.icon?.prompt || step.title}
|
||||
src={step.icon?.__icon_url__ || ''}
|
||||
alt={step.icon?.__icon_query__ || step.title}
|
||||
className="w-6 h-6 object-cover rounded-full print:w-4 print:h-4"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData?.backgroundImage})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData?.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const StatisticsSlideLayout: React.FC<StatisticsSlideLayoutProps> = ({ data: sli
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `url("${slideData.backgroundImage.url}")`,
|
||||
backgroundImage: `url("${slideData.backgroundImage.__image_url__}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `url("${slideData.backgroundImage.url}")`,
|
||||
backgroundImage: `url("${slideData.backgroundImage.__image_url__}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
|
|
@ -99,7 +99,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
|
|||
<div className="relative z-10 flex flex-col h-full px-8 py-8">
|
||||
{/* Professional Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData?.backgroundImage
|
||||
<h1 className={`text-4xl md:text-5xl font-bold mb-3 tracking-tight leading-tight break-words ${slideData?.backgroundImage?.__image_prompt__
|
||||
? 'text-white drop-shadow-lg'
|
||||
: 'text-slate-900'
|
||||
}`}>
|
||||
|
|
@ -109,7 +109,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
|
|||
</h1>
|
||||
|
||||
{slideData?.subtitle && (
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData?.backgroundImage
|
||||
<p className={`text-xl font-light leading-relaxed break-words ${slideData?.backgroundImage?.__image_prompt__
|
||||
? 'text-slate-200 drop-shadow-md'
|
||||
: 'text-slate-600'
|
||||
}`}>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ const timelineSlideSchema = z.object({
|
|||
description: "Event description",
|
||||
}),
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Default event icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Default event icon'
|
||||
}).meta({
|
||||
description: "Icon for the event",
|
||||
})
|
||||
|
|
@ -35,8 +35,8 @@ const timelineSlideSchema = z.object({
|
|||
title: 'Foundation',
|
||||
description: 'Company founded with a vision to transform digital experiences',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Foundation icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Foundation icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -44,8 +44,8 @@ const timelineSlideSchema = z.object({
|
|||
title: 'First Success',
|
||||
description: 'Launched first product and gained initial market traction',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'First success icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__icon_query__: 'First success icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -53,8 +53,8 @@ const timelineSlideSchema = z.object({
|
|||
title: 'Expansion',
|
||||
description: 'Expanded team and entered new markets with innovative solutions',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Expansion icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__icon_query__: 'Expansion icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -62,8 +62,8 @@ const timelineSlideSchema = z.object({
|
|||
title: 'Innovation',
|
||||
description: 'Introduced breakthrough technology and achieved industry recognition',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Innovation icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Innovation icon'
|
||||
}
|
||||
}
|
||||
]).meta({
|
||||
|
|
@ -88,7 +88,7 @@ const TimelineSlideLayout: React.FC<TimelineSlideLayoutProps> = ({ data: slideDa
|
|||
<div
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.url})`,
|
||||
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
} : {}}
|
||||
|
|
@ -137,8 +137,8 @@ const TimelineSlideLayout: React.FC<TimelineSlideLayoutProps> = ({ data: slideDa
|
|||
<h3 className="text-xl font-bold text-gray-800 mb-3 leading-tight flex items-center gap-2 print:text-lg">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center overflow-hidden flex-shrink-0 print:w-6 print:h-6">
|
||||
<img
|
||||
src={event.icon?.url || ''}
|
||||
alt={event.icon?.prompt || event.title}
|
||||
src={event.icon?.__icon_url__ || ''}
|
||||
alt={event.icon?.__icon_query__ || event.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ const type1SlideSchema = z.object({
|
|||
description: "Main description text",
|
||||
}),
|
||||
image: ImageSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'A beautiful road in the mountains'
|
||||
__image_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__image_prompt__: 'A beautiful road in the mountains'
|
||||
}).meta({
|
||||
description: "Main slide image",
|
||||
})
|
||||
|
|
@ -52,8 +52,8 @@ const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData })
|
|||
{/* Image */}
|
||||
<div className="w-full h-full min-h-[200px] lg:min-h-[300px]">
|
||||
<img
|
||||
src={slideData?.image?.url || ''}
|
||||
alt={slideData?.image?.prompt || slideData?.title || ''}
|
||||
src={slideData?.image?.__image_url__ || ''}
|
||||
alt={slideData?.image?.__image_prompt__ || slideData?.title || ''}
|
||||
className="w-full h-full object-cover rounded-lg shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,24 +25,24 @@ const type3SlideSchema = z.object({
|
|||
heading: 'First Feature',
|
||||
description: 'Description for the first featured item with detailed information',
|
||||
image: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'A beautiful road in the mountains'
|
||||
__image_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__image_prompt__: 'A beautiful road in the mountains'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Second Feature',
|
||||
description: 'Description for the second featured item with relevant details',
|
||||
image: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'Modern office workspace'
|
||||
__image_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__image_prompt__: 'Modern office workspace'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Third Feature',
|
||||
description: 'Description for the third featured item with important points',
|
||||
image: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Laptop with code on screen'
|
||||
__image_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__image_prompt__: 'Laptop with code on screen'
|
||||
}
|
||||
}
|
||||
]).meta({
|
||||
|
|
@ -94,8 +94,8 @@ const Type3SlideLayout: React.FC<Type3SlideLayoutProps> = ({ data: slideData })
|
|||
{/* Image */}
|
||||
<div className="max-md:h-[140px] max-lg:h-[180px] h-48 w-full">
|
||||
<img
|
||||
src={item.image?.url || ''}
|
||||
alt={item.image?.prompt || item.heading}
|
||||
src={item.image?.__image_url__ || ''}
|
||||
alt={item.image?.__image_prompt__ || item.heading}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const type7SlideSchema = z.object({
|
|||
description: "Item description",
|
||||
}),
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Default icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Default icon'
|
||||
}).meta({
|
||||
description: "Icon for the item",
|
||||
})
|
||||
|
|
@ -28,32 +28,32 @@ const type7SlideSchema = z.object({
|
|||
heading: 'Professional Service',
|
||||
description: 'High-quality professional services tailored to your specific needs and requirements',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Professional service icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Professional service icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Expert Consultation',
|
||||
description: 'Expert advice and consultation from experienced professionals in the field',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'Expert consultation icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__icon_query__: 'Expert consultation icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Quality Assurance',
|
||||
description: 'Comprehensive quality assurance processes to ensure excellent results',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Quality assurance icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__icon_query__: 'Quality assurance icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Customer Support',
|
||||
description: 'Dedicated customer support available to assist you throughout the process',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Customer support icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Customer support icon'
|
||||
}
|
||||
}
|
||||
]).meta({
|
||||
|
|
@ -100,8 +100,8 @@ const Type7SlideLayout: React.FC<Type7SlideLayoutProps> = ({ data: slideData })
|
|||
<div className="flex-shrink-0 lg:w-16">
|
||||
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-blue-600 rounded-lg flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
src={item.icon?.url || ''}
|
||||
alt={item.icon?.prompt || item.heading}
|
||||
src={item.icon?.__icon_url__ || ''}
|
||||
alt={item.icon?.__icon_query__ || item.heading}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -135,8 +135,8 @@ const Type7SlideLayout: React.FC<Type7SlideLayoutProps> = ({ data: slideData })
|
|||
<div className="text-center mb-4">
|
||||
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center mx-auto mb-4 overflow-hidden">
|
||||
<img
|
||||
src={item.icon?.url || ''}
|
||||
alt={item.icon?.prompt || item.heading}
|
||||
src={item.icon?.__icon_url__ || ''}
|
||||
alt={item.icon?.__icon_query__ || item.heading}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const type8SlideSchema = z.object({
|
|||
description: "Item description",
|
||||
}),
|
||||
icon: IconSchema.default({
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Default icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Default icon'
|
||||
}).meta({
|
||||
description: "Icon for the item",
|
||||
})
|
||||
|
|
@ -31,24 +31,24 @@ const type8SlideSchema = z.object({
|
|||
heading: 'Advanced Features',
|
||||
description: 'Cutting-edge functionality designed to enhance productivity and user experience',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
prompt: 'Advanced features icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
|
||||
__icon_query__: 'Advanced features icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Reliable Performance',
|
||||
description: 'Consistent and dependable performance across all platforms and devices',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
prompt: 'Reliable performance icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
|
||||
__icon_query__: 'Reliable performance icon'
|
||||
}
|
||||
},
|
||||
{
|
||||
heading: 'Secure Environment',
|
||||
description: 'Enterprise-grade security measures to protect your data and privacy',
|
||||
icon: {
|
||||
url: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
prompt: 'Secure environment icon'
|
||||
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
|
||||
__icon_query__: 'Secure environment icon'
|
||||
}
|
||||
}
|
||||
]).meta({
|
||||
|
|
@ -83,8 +83,8 @@ const Type8SlideLayout: React.FC<Type8SlideLayoutProps> = ({ data: slideData })
|
|||
<div className="text-center mb-4">
|
||||
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center mx-auto mb-4 overflow-hidden">
|
||||
<img
|
||||
src={item.icon?.url || ''}
|
||||
alt={item.icon?.prompt || item.heading}
|
||||
src={item.icon?.__icon_url__ || ''}
|
||||
alt={item.icon?.__icon_query__ || item.heading}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -117,8 +117,8 @@ const Type8SlideLayout: React.FC<Type8SlideLayoutProps> = ({ data: slideData })
|
|||
<div className="w-[32px] md:w-[64px] h-[32px] md:h-[64px]">
|
||||
<div className="w-full h-full bg-blue-600 rounded-lg flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
src={item.icon?.url || ''}
|
||||
alt={item.icon?.prompt || item.heading}
|
||||
src={item.icon?.__icon_url__ || ''}
|
||||
alt={item.icon?.__icon_query__ || item.heading}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const ImageSchema = z.object({
|
||||
url: z.url().meta({
|
||||
__image_url__: z.url().meta({
|
||||
description: "URL to image",
|
||||
}),
|
||||
prompt: z.string().meta({
|
||||
__image_prompt__: z.string().meta({
|
||||
description: "Prompt used to generate the image",
|
||||
}),
|
||||
__image_type__:z.literal('image')
|
||||
})
|
||||
|
||||
export const IconSchema = z.object({
|
||||
url: z.string().meta({
|
||||
__icon_url__: z.string().meta({
|
||||
description: "URL to icon",
|
||||
}),
|
||||
prompt: z.string().meta({
|
||||
description: "Prompt used to generate the icon",
|
||||
__icon_query__: z.string().meta({
|
||||
description: "Query used to search the icon",
|
||||
}),
|
||||
__image_type__:z.literal('icon')
|
||||
})
|
||||
|
|
@ -42,12 +42,6 @@ const nextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
rewrites: async () => [
|
||||
{
|
||||
source: "/static/:path*",
|
||||
destination: "/api/static/:path*",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue