diff --git a/Dockerfile b/Dockerfile index cb8ae34c..8e375bc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile.dev b/Dockerfile.dev index ad11f17f..04f65834 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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 diff --git a/app_data/fastapi.db b/app_data/fastapi.db new file mode 100644 index 00000000..78572e93 Binary files /dev/null and b/app_data/fastapi.db differ diff --git a/app_data/images/15b080fb-5b93-4e83-a501-17eef22e8f9c.jpg b/app_data/images/15b080fb-5b93-4e83-a501-17eef22e8f9c.jpg new file mode 100644 index 00000000..82583be6 Binary files /dev/null and b/app_data/images/15b080fb-5b93-4e83-a501-17eef22e8f9c.jpg differ diff --git a/app_data/images/3628f28d-7d7b-430d-bd57-13b51c28d55e.jpg b/app_data/images/3628f28d-7d7b-430d-bd57-13b51c28d55e.jpg new file mode 100644 index 00000000..e5f8e9da Binary files /dev/null and b/app_data/images/3628f28d-7d7b-430d-bd57-13b51c28d55e.jpg differ diff --git a/app_data/images/8b200426-6974-47d7-93be-626c4b5b122b.jpg b/app_data/images/8b200426-6974-47d7-93be-626c4b5b122b.jpg new file mode 100644 index 00000000..de324663 Binary files /dev/null and b/app_data/images/8b200426-6974-47d7-93be-626c4b5b122b.jpg differ diff --git a/app_data/settings.json b/app_data/settings.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/app_data/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/app_data/userConfig.json b/app_data/userConfig.json new file mode 100644 index 00000000..691da263 --- /dev/null +++ b/app_data/userConfig.json @@ -0,0 +1 @@ +{"LLM":"google","GOOGLE_API_KEY":"AIzaSyAFY4MWd8aE7L4qWkFdrqpDAMgO2M63Cc4","OLLAMA_URL":"http://localhost:11434","USE_CUSTOM_URL":false} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 38211c6c..a96f4c87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/nginx.conf b/nginx.conf index 4277d977..9d4764aa 100644 --- a/nginx.conf +++ b/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; + } } } \ No newline at end of file diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index b56a50b3..d6be4668 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -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 diff --git a/servers/fastapi/api/v1/ppt/endpoints/images.py b/servers/fastapi/api/v1/ppt/endpoints/images.py index 752f79de..64acbde8 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/images.py +++ b/servers/fastapi/api/v1/ppt/endpoints/images.py @@ -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) diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index fa2ef209..69993e3f 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -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: diff --git a/servers/fastapi/get_test_schema.py b/servers/fastapi/get_test_schema.py index df53343e..4a17e9e7 100644 --- a/servers/fastapi/get_test_schema.py +++ b/servers/fastapi/get_test_schema.py @@ -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()) diff --git a/servers/fastapi/models/json_path_guide.py b/servers/fastapi/models/json_path_guide.py new file mode 100644 index 00000000..58343a1d --- /dev/null +++ b/servers/fastapi/models/json_path_guide.py @@ -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] diff --git a/servers/fastapi/models/sql/asset.py b/servers/fastapi/models/sql/image_asset.py similarity index 58% rename from servers/fastapi/models/sql/asset.py rename to servers/fastapi/models/sql/image_asset.py index 120c053b..9195a495 100644 --- a/servers/fastapi/models/sql/asset.py +++ b/servers/fastapi/models/sql/image_asset.py @@ -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) diff --git a/servers/fastapi/services/__init__.py b/servers/fastapi/services/__init__.py index 7a2f9255..89bac591 100644 --- a/servers/fastapi/services/__init__.py +++ b/servers/fastapi/services/__init__.py @@ -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 diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index 94f06deb..4acff9f1 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -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 diff --git a/servers/fastapi/services/schema_processor.py b/servers/fastapi/services/schema_processor.py deleted file mode 100644 index f0ef5596..00000000 --- a/servers/fastapi/services/schema_processor.py +++ /dev/null @@ -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 diff --git a/servers/fastapi/tests/test_gemini_schema_support.py b/servers/fastapi/tests/test_gemini_schema_support.py index 44844cd2..cd66083e 100644 --- a/servers/fastapi/tests/test_gemini_schema_support.py +++ b/servers/fastapi/tests/test_gemini_schema_support.py @@ -67,4 +67,4 @@ def test_gemini_schema_support(): response_schema=TwoColumnSlideModel.model_json_schema(), ), ) - print(response.parsed) + print(response.text) diff --git a/servers/fastapi/utils/asset_directory_utils.py b/servers/fastapi/utils/asset_directory_utils.py new file mode 100644 index 00000000..1b886880 --- /dev/null +++ b/servers/fastapi/utils/asset_directory_utils.py @@ -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 diff --git a/servers/fastapi/utils/dict_utils.py b/servers/fastapi/utils/dict_utils.py new file mode 100644 index 00000000..51fc3e91 --- /dev/null +++ b/servers/fastapi/utils/dict_utils.py @@ -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 diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index ebd025d0..afca334b 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -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__"] + ), }, }, ) diff --git a/servers/fastapi/utils/process_slides.py b/servers/fastapi/utils/process_slides.py index ca35eef8..ec043fb4 100644 --- a/servers/fastapi/utils/process_slides.py +++ b/servers/fastapi/utils/process_slides.py @@ -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 diff --git a/servers/fastapi/utils/schema_utils.py b/servers/fastapi/utils/schema_utils.py new file mode 100644 index 00000000..d78c8dfa --- /dev/null +++ b/servers/fastapi/utils/schema_utils.py @@ -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 diff --git a/servers/nextjs/app/api/static/[...filepath]/route.ts b/servers/nextjs/app/api/static/[...filepath]/route.ts deleted file mode 100644 index 782d165d..00000000 --- a/servers/nextjs/app/api/static/[...filepath]/route.ts +++ /dev/null @@ -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'; - } -} diff --git a/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx index f61ae195..fa0a0f5e 100644 --- a/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx +++ b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx @@ -46,7 +46,7 @@ const BulletPointSlideLayout: React.FC = ({ data: s
= ({ data: slideData }) =>
= ({ data: slideData }) =>
{card.icon?.prompt
diff --git a/servers/nextjs/components/layouts/ComparisonSlideLayout.tsx b/servers/nextjs/components/layouts/ComparisonSlideLayout.tsx index 72e438cf..906d7060 100644 --- a/servers/nextjs/components/layouts/ComparisonSlideLayout.tsx +++ b/servers/nextjs/components/layouts/ComparisonSlideLayout.tsx @@ -76,7 +76,7 @@ const ComparisonSlideLayout: React.FC = ({ data: sli
= ({ data: slideData
= ({ data: slideData })
= ({ data: slideData }) =>
{slideData?.icon?.prompt
diff --git a/servers/nextjs/components/layouts/ImageSlideLayout.tsx b/servers/nextjs/components/layouts/ImageSlideLayout.tsx index 21289c8a..1accc93f 100644 --- a/servers/nextjs/components/layouts/ImageSlideLayout.tsx +++ b/servers/nextjs/components/layouts/ImageSlideLayout.tsx @@ -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 = ({ data: slideData }) {/* Left panel - Image */}
{slideData?.image?.prompt {/* Overlay gradient */} diff --git a/servers/nextjs/components/layouts/ProcessSlideLayout.tsx b/servers/nextjs/components/layouts/ProcessSlideLayout.tsx index 6698922a..d4b9c237 100644 --- a/servers/nextjs/components/layouts/ProcessSlideLayout.tsx +++ b/servers/nextjs/components/layouts/ProcessSlideLayout.tsx @@ -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 = ({ data: slideData
= ({ data: slideData
{step.icon?.prompt
diff --git a/servers/nextjs/components/layouts/QuoteSlideLayout.tsx b/servers/nextjs/components/layouts/QuoteSlideLayout.tsx index 711bd38c..4c1962ce 100644 --- a/servers/nextjs/components/layouts/QuoteSlideLayout.tsx +++ b/servers/nextjs/components/layouts/QuoteSlideLayout.tsx @@ -48,7 +48,7 @@ const QuoteSlideLayout: React.FC = ({ data: slideData })
= ({ data: sli
= ({ data: slideData }) =>
= ({ data: slideData }) =>
{/* Professional Header */}
-

@@ -109,7 +109,7 @@ const TeamSlideLayout: React.FC = ({ data: slideData }) =>

{slideData?.subtitle && ( -

diff --git a/servers/nextjs/components/layouts/TimelineSlideLayout.tsx b/servers/nextjs/components/layouts/TimelineSlideLayout.tsx index 81a4281b..d0ac0db7 100644 --- a/servers/nextjs/components/layouts/TimelineSlideLayout.tsx +++ b/servers/nextjs/components/layouts/TimelineSlideLayout.tsx @@ -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 = ({ data: slideDa

= ({ data: slideDa

{event.icon?.prompt
diff --git a/servers/nextjs/components/layouts/Type1SlideLayout.tsx b/servers/nextjs/components/layouts/Type1SlideLayout.tsx index 6b587fbb..7f73702a 100644 --- a/servers/nextjs/components/layouts/Type1SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type1SlideLayout.tsx @@ -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 = ({ data: slideData }) {/* Image */}
{slideData?.image?.prompt
diff --git a/servers/nextjs/components/layouts/Type3SlideLayout.tsx b/servers/nextjs/components/layouts/Type3SlideLayout.tsx index 2d5db420..7a949f5f 100644 --- a/servers/nextjs/components/layouts/Type3SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type3SlideLayout.tsx @@ -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 = ({ data: slideData }) {/* Image */}
{item.image?.prompt
diff --git a/servers/nextjs/components/layouts/Type7SlideLayout.tsx b/servers/nextjs/components/layouts/Type7SlideLayout.tsx index 98700163..3afe0125 100644 --- a/servers/nextjs/components/layouts/Type7SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type7SlideLayout.tsx @@ -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 = ({ data: slideData })
{item.icon?.prompt
@@ -135,8 +135,8 @@ const Type7SlideLayout: React.FC = ({ data: slideData })
{item.icon?.prompt
diff --git a/servers/nextjs/components/layouts/Type8SlideLayout.tsx b/servers/nextjs/components/layouts/Type8SlideLayout.tsx index b2b7494a..ad73a37b 100644 --- a/servers/nextjs/components/layouts/Type8SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type8SlideLayout.tsx @@ -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 = ({ data: slideData })
{item.icon?.prompt
@@ -117,8 +117,8 @@ const Type8SlideLayout: React.FC = ({ data: slideData })
{item.icon?.prompt
diff --git a/servers/nextjs/components/layouts/defaultSchemes.ts b/servers/nextjs/components/layouts/defaultSchemes.ts index c783a02b..4f3c7fde 100644 --- a/servers/nextjs/components/layouts/defaultSchemes.ts +++ b/servers/nextjs/components/layouts/defaultSchemes.ts @@ -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') }) \ No newline at end of file diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index c358ea6b..e4cc1ab9 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -42,12 +42,6 @@ const nextConfig = { }, ], }, - rewrites: async () => [ - { - source: "/static/:path*", - destination: "/api/static/:path*", - }, - ], }; export default nextConfig;