import os import base64 from datetime import datetime from typing import Optional, List, Dict from uuid import UUID from fastapi import APIRouter, HTTPException, File, UploadFile, Form, Depends from pydantic import BaseModel from openai import OpenAI from openai import APIError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from services.database import get_async_session from models.sql.presentation_layout_code import PresentationLayoutCodeModel from .prompts import ( GENERATE_HTML_SYSTEM_PROMPT, HTML_TO_REACT_SYSTEM_PROMPT, HTML_EDIT_SYSTEM_PROMPT, ) from models.sql.template import TemplateModel # Create separate routers for each functionality SLIDE_TO_HTML_ROUTER = APIRouter(prefix="/slide-to-html", tags=["slide-to-html"]) HTML_TO_REACT_ROUTER = APIRouter(prefix="/html-to-react", tags=["html-to-react"]) HTML_EDIT_ROUTER = APIRouter(prefix="/html-edit", tags=["html-edit"]) LAYOUT_MANAGEMENT_ROUTER = APIRouter( prefix="/template-management", tags=["template-management"] ) # Request/Response models for slide-to-html endpoint class SlideToHtmlRequest(BaseModel): image: str # Partial path to image file (e.g., "/app_data/images/uuid/slide_1.png") xml: str # OXML content as text fonts: Optional[List[str]] = None # Optional normalized root fonts for this slide class SlideToHtmlResponse(BaseModel): success: bool html: str # Request/Response models for html-edit endpoint class HtmlEditResponse(BaseModel): success: bool edited_html: str message: Optional[str] = None # Request/Response models for html-to-react endpoint class HtmlToReactRequest(BaseModel): html: str # HTML content to convert to React component image: Optional[str] = None # Optional image path to provide visual context class HtmlToReactResponse(BaseModel): success: bool react_component: str message: Optional[str] = None # Request/Response models for layout management endpoints class LayoutData(BaseModel): presentation: UUID # UUID of the presentation layout_id: str # Unique identifier for the layout layout_name: str # Display name of the layout layout_code: str # TSX/React component code for the layout fonts: Optional[List[str]] = None # Optional list of font links class SaveLayoutsRequest(BaseModel): layouts: list[LayoutData] class SaveLayoutsResponse(BaseModel): success: bool saved_count: int message: Optional[str] = None class GetLayoutsResponse(BaseModel): success: bool layouts: list[LayoutData] message: Optional[str] = None template: Optional[dict] = None fonts: Optional[List[str]] = None class PresentationSummary(BaseModel): presentation_id: UUID layout_count: int last_updated_at: Optional[datetime] = None template: Optional[dict] = None class GetPresentationSummaryResponse(BaseModel): success: bool presentations: List[PresentationSummary] total_presentations: int total_layouts: int message: Optional[str] = None class ErrorResponse(BaseModel): success: bool = False detail: str error_code: Optional[str] = None class TemplateCreateRequest(BaseModel): id: UUID name: str description: Optional[str] = None class TemplateCreateResponse(BaseModel): success: bool template: dict message: Optional[str] = None class TemplateInfo(BaseModel): id: UUID name: Optional[str] = None description: Optional[str] = None created_at: Optional[datetime] = None async def generate_html_from_slide( base64_image: str, media_type: str, xml_content: str, api_key: str, fonts: Optional[List[str]] = None, ) -> str: """ Generate HTML content from slide image and XML using OpenAI GPT-5 Responses API. Args: base64_image: Base64 encoded image data media_type: MIME type of the image (e.g., 'image/png') xml_content: OXML content as text api_key: OpenAI API key fonts: Optional list of normalized root font families to prefer in output Returns: Generated HTML content as string Raises: HTTPException: If API call fails or no content is generated """ print( f"Generating HTML from slide image and XML using OpenAI GPT-5 Responses API..." ) try: client = OpenAI(api_key=api_key) # Compose input for Responses API. Include system prompt, image (separate), OXML and optional fonts text. data_url = f"data:{media_type};base64,{base64_image}" fonts_text = ( f"\nFONTS (Normalized root families used in this slide, use where it is required): {', '.join(fonts)}" if fonts else "" ) user_text = f"OXML: \n\n{fonts_text}" input_payload = [ {"role": "system", "content": GENERATE_HTML_SYSTEM_PROMPT}, { "role": "user", "content": [ {"type": "input_image", "image_url": data_url}, {"type": "input_text", "text": user_text}, ], }, ] print("Making Responses API request for HTML generation...") response = client.responses.create( model="gpt-5", input=input_payload, reasoning={"effort": "high"}, text={"verbosity": "low"}, ) # Extract the response text html_content = ( getattr(response, "output_text", None) or getattr(response, "text", None) or "" ) print(f"Received HTML content length: {len(html_content)}") if not html_content: raise HTTPException( status_code=500, detail="No HTML content generated by OpenAI GPT-5" ) return html_content except APIError as e: print(f"OpenAI API Error: {e}") raise HTTPException( status_code=500, detail=f"OpenAI API error during HTML generation: {str(e)}" ) except Exception as e: # Handle various API errors error_msg = str(e) print(f"Exception occurred: {error_msg}") print(f"Exception type: {type(e)}") if "timeout" in error_msg.lower(): raise HTTPException( status_code=408, detail=f"OpenAI API timeout during HTML generation: {error_msg}", ) elif "connection" in error_msg.lower(): raise HTTPException( status_code=503, detail=f"OpenAI API connection error during HTML generation: {error_msg}", ) else: raise HTTPException( status_code=500, detail=f"OpenAI API error during HTML generation: {error_msg}", ) async def generate_react_component_from_html( html_content: str, api_key: str, image_base64: Optional[str] = None, media_type: Optional[str] = None, ) -> str: """ Convert HTML content to TSX React component using OpenAI GPT-5 Responses API. Args: html_content: Generated HTML content api_key: OpenAI API key Returns: Generated TSX React component code as string Raises: HTTPException: If API call fails or no content is generated """ try: client = OpenAI(api_key=api_key) print("Making Responses API request for React component generation...") # Build payload with optional image content_parts = [{"type": "input_text", "text": f"HTML INPUT:\n{html_content}"}] if image_base64 and media_type: data_url = f"data:{media_type};base64,{image_base64}" content_parts.insert(0, {"type": "input_image", "image_url": data_url}) input_payload = [ {"role": "system", "content": HTML_TO_REACT_SYSTEM_PROMPT}, {"role": "user", "content": content_parts}, ] response = client.responses.create( model="gpt-5", input=input_payload, reasoning={"effort": "minimal"}, text={"verbosity": "low"}, ) react_content = ( getattr(response, "output_text", None) or getattr(response, "text", None) or "" ) print(f"Received React content length: {len(react_content)}") if not react_content: raise HTTPException( status_code=500, detail="No React component generated by OpenAI GPT-5" ) react_content = ( react_content.replace("```tsx", "") .replace("```", "") .replace("typescript", "") .replace("javascript", "") ) # Filter out lines that start with import or export filtered_lines = [] for line in react_content.split("\n"): stripped_line = line.strip() if not ( stripped_line.startswith("import ") or stripped_line.startswith("export ") ): filtered_lines.append(line) filtered_react_content = "\n".join(filtered_lines) print(f"Filtered React content length: {len(filtered_react_content)}") return filtered_react_content except APIError as e: print(f"OpenAI API Error: {e}") raise HTTPException( status_code=500, detail=f"OpenAI API error during React generation: {str(e)}", ) except Exception as e: # Handle various API errors error_msg = str(e) print(f"Exception occurred: {error_msg}") print(f"Exception type: {type(e)}") if "timeout" in error_msg.lower(): raise HTTPException( status_code=408, detail=f"OpenAI API timeout during React generation: {error_msg}", ) elif "connection" in error_msg.lower(): raise HTTPException( status_code=503, detail=f"OpenAI API connection error during React generation: {error_msg}", ) else: raise HTTPException( status_code=500, detail=f"OpenAI API error during React generation: {error_msg}", ) async def edit_html_with_images( current_ui_base64: str, sketch_base64: Optional[str], media_type: str, html_content: str, prompt: str, api_key: str, ) -> str: """ Edit HTML content based on one or two images and a text prompt using OpenAI GPT-5 Responses API. Args: current_ui_base64: Base64 encoded current UI image data sketch_base64: Base64 encoded sketch/indication image data (optional) media_type: MIME type of the images (e.g., 'image/png') html_content: Current HTML content to edit prompt: Text prompt describing the changes api_key: OpenAI API key Returns: Edited HTML content as string Raises: HTTPException: If API call fails or no content is generated """ try: client = OpenAI(api_key=api_key) print("Making Responses API request for HTML editing...") current_data_url = f"data:{media_type};base64,{current_ui_base64}" sketch_data_url = ( f"data:{media_type};base64,{sketch_base64}" if sketch_base64 else None ) content_parts = [ {"type": "input_image", "image_url": current_data_url}, { "type": "input_text", "text": f"CURRENT HTML TO EDIT:\n{html_content}\n\nTEXT PROMPT FOR CHANGES:\n{prompt}", }, ] if sketch_data_url: # Insert sketch image after current UI image for context content_parts.insert( 1, {"type": "input_image", "image_url": sketch_data_url} ) input_payload = [ {"role": "system", "content": HTML_EDIT_SYSTEM_PROMPT}, {"role": "user", "content": content_parts}, ] response = client.responses.create( model="gpt-5", input=input_payload, reasoning={"effort": "low"}, text={"verbosity": "low"}, ) edited_html = ( getattr(response, "output_text", None) or getattr(response, "text", None) or "" ) print(f"Received edited HTML content length: {len(edited_html)}") if not edited_html: raise HTTPException( status_code=500, detail="No edited HTML content generated by OpenAI GPT-5", ) return edited_html except APIError as e: print(f"OpenAI API Error: {e}") raise HTTPException( status_code=500, detail=f"OpenAI API error during HTML editing: {str(e)}" ) except Exception as e: # Handle various API errors error_msg = str(e) print(f"Exception occurred: {error_msg}") print(f"Exception type: {type(e)}") if "timeout" in error_msg.lower(): raise HTTPException( status_code=408, detail=f"OpenAI API timeout during HTML editing: {error_msg}", ) elif "connection" in error_msg.lower(): raise HTTPException( status_code=503, detail=f"OpenAI API connection error during HTML editing: {error_msg}", ) else: raise HTTPException( status_code=500, detail=f"OpenAI API error during HTML editing: {error_msg}", ) # ENDPOINT 1: Slide to HTML conversion @SLIDE_TO_HTML_ROUTER.post("/", response_model=SlideToHtmlResponse) async def convert_slide_to_html(request: SlideToHtmlRequest): """ Convert a slide image and its OXML data to HTML using Anthropic Claude API. Args: request: JSON request containing image path and XML content Returns: SlideToHtmlResponse with generated HTML """ try: # Get OpenAI API key from environment api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise HTTPException( status_code=500, detail="OPENAI_API_KEY environment variable not set" ) # Resolve image path to actual file system path actual_image_path = resolve_image_path_to_filesystem(request.image) if not actual_image_path: raise HTTPException( status_code=404, detail=f"Image file not found: {request.image}" ) # Read and encode image to base64 with open(actual_image_path, "rb") as image_file: image_content = image_file.read() base64_image = base64.b64encode(image_content).decode("utf-8") # Determine media type from file extension file_extension = os.path.splitext(actual_image_path)[1].lower() media_type_map = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", } media_type = media_type_map.get(file_extension, "image/png") # Generate HTML using the extracted function html_content = await generate_html_from_slide( base64_image=base64_image, media_type=media_type, xml_content=request.xml, api_key=api_key, fonts=request.fonts, ) html_content = html_content.replace("```html", "").replace("```", "") return SlideToHtmlResponse(success=True, html=html_content) except HTTPException: # Re-raise HTTP exceptions as-is raise except Exception as e: # Log the full error for debugging print(f"Unexpected error during slide to HTML processing: {str(e)}") raise HTTPException( status_code=500, detail=f"Error processing slide to HTML: {str(e)}" ) # ENDPOINT 2: HTML to React component conversion @HTML_TO_REACT_ROUTER.post("/", response_model=HtmlToReactResponse) async def convert_html_to_react(request: HtmlToReactRequest): """ Convert HTML content to TSX React component using Anthropic Claude API. Args: request: JSON request containing HTML content Returns: HtmlToReactResponse with generated React component """ try: # Get OpenAI API key from environment api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise HTTPException( status_code=500, detail="OPENAI_API_KEY environment variable not set" ) # Validate HTML content if not request.html or not request.html.strip(): raise HTTPException(status_code=400, detail="HTML content cannot be empty") # Optionally resolve image and encode to base64 image_b64 = None media_type = None if request.image: actual_image_path = resolve_image_path_to_filesystem(request.image) if actual_image_path: with open(actual_image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") ext = os.path.splitext(actual_image_path)[1].lower() media_type = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", }.get(ext, "image/png") # Convert HTML to React component react_component = await generate_react_component_from_html( html_content=request.html, api_key=api_key, image_base64=image_b64, media_type=media_type, ) react_component = react_component.replace("```tsx", "").replace("```", "") return HtmlToReactResponse( success=True, react_component=react_component, message="React component generated successfully", ) except HTTPException: # Re-raise HTTP exceptions as-is raise except Exception as e: # Log the full error for debugging print(f"Unexpected error during HTML to React processing: {str(e)}") raise HTTPException( status_code=500, detail=f"Error processing HTML to React: {str(e)}" ) # ENDPOINT 3: HTML editing with images @HTML_EDIT_ROUTER.post("/", response_model=HtmlEditResponse) async def edit_html_with_images_endpoint( current_ui_image: UploadFile = File(..., description="Current UI image file"), sketch_image: Optional[UploadFile] = File( None, description="Sketch/indication image file (optional)" ), html: str = Form(..., description="Current HTML content to edit"), prompt: str = Form(..., description="Text prompt describing the changes"), ): """ Edit HTML content based on one or two uploaded images and a text prompt using Anthropic Claude API. Args: current_ui_image: Uploaded current UI image file sketch_image: Uploaded sketch/indication image file (optional) html: Current HTML content to edit (form data) prompt: Text prompt describing the changes (form data) Returns: HtmlEditResponse with edited HTML """ try: # Get OpenAI API key from environment api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise HTTPException( status_code=500, detail="OPENAI_API_KEY environment variable not set" ) # Validate inputs if not html or not html.strip(): raise HTTPException(status_code=400, detail="HTML content cannot be empty") if not prompt or not prompt.strip(): raise HTTPException(status_code=400, detail="Text prompt cannot be empty") # Validate current UI image file if ( not current_ui_image.content_type or not current_ui_image.content_type.startswith("image/") ): raise HTTPException( status_code=400, detail="Current UI file must be an image" ) # Validate sketch image file only if provided if sketch_image and ( not sketch_image.content_type or not sketch_image.content_type.startswith("image/") ): raise HTTPException(status_code=400, detail="Sketch file must be an image") # Read and encode current UI image to base64 current_ui_content = await current_ui_image.read() current_ui_base64 = base64.b64encode(current_ui_content).decode("utf-8") # Read and encode sketch image to base64 only if provided sketch_base64 = None if sketch_image: sketch_content = await sketch_image.read() sketch_base64 = base64.b64encode(sketch_content).decode("utf-8") # Use the content type from the uploaded files media_type = current_ui_image.content_type # Edit HTML using the function edited_html = await edit_html_with_images( current_ui_base64=current_ui_base64, sketch_base64=sketch_base64, media_type=media_type, html_content=html, prompt=prompt, api_key=api_key, ) edited_html = edited_html.replace("```html", "").replace("```", "") return HtmlEditResponse( success=True, edited_html=edited_html, message="HTML edited successfully" ) except HTTPException: # Re-raise HTTP exceptions as-is raise except Exception as e: # Log the full error for debugging print(f"Unexpected error during HTML editing: {str(e)}") raise HTTPException( status_code=500, detail=f"Error processing HTML editing: {str(e)}" ) # ENDPOINT 4: Save layouts for a presentation @LAYOUT_MANAGEMENT_ROUTER.post( "/save-templates", response_model=SaveLayoutsResponse, responses={ 400: {"model": ErrorResponse, "description": "Validation error"}, 500: {"model": ErrorResponse, "description": "Internal server error"}, }, ) async def save_layouts( request: SaveLayoutsRequest, session: AsyncSession = Depends(get_async_session) ): """ Save multiple layouts for presentations. Args: request: JSON request containing array of layout data session: Database session Returns: SaveLayoutsResponse with success status and count of saved layouts Raises: HTTPException: 400 for validation errors, 500 for server errors """ try: # Validate request data if not request.layouts: raise HTTPException(status_code=400, detail="Layouts array cannot be empty") if len(request.layouts) > 50: # Reasonable limit raise HTTPException( status_code=400, detail="Cannot save more than 50 layouts at once" ) saved_count = 0 for i, layout_data in enumerate(request.layouts): # Validate individual layout data if ( not layout_data.presentation or not str(layout_data.presentation).strip() ): raise HTTPException( status_code=400, detail=f"Layout {i+1}: presentation_id cannot be empty", ) if not layout_data.layout_id or not layout_data.layout_id.strip(): raise HTTPException( status_code=400, detail=f"Layout {i+1}: layout_id cannot be empty" ) if not layout_data.layout_name or not layout_data.layout_name.strip(): raise HTTPException( status_code=400, detail=f"Layout {i+1}: layout_name cannot be empty" ) if not layout_data.layout_code or not layout_data.layout_code.strip(): raise HTTPException( status_code=400, detail=f"Layout {i+1}: layout_code cannot be empty" ) # Check if layout already exists for this presentation and layout_id stmt = select(PresentationLayoutCodeModel).where( PresentationLayoutCodeModel.presentation == layout_data.presentation, PresentationLayoutCodeModel.layout_id == layout_data.layout_id, ) result = await session.execute(stmt) existing_layout = result.scalar_one_or_none() if existing_layout: # Update existing layout existing_layout.layout_name = layout_data.layout_name existing_layout.layout_code = layout_data.layout_code existing_layout.fonts = layout_data.fonts existing_layout.updated_at = datetime.now() else: # Create new layout new_layout = PresentationLayoutCodeModel( presentation=layout_data.presentation, layout_id=layout_data.layout_id, layout_name=layout_data.layout_name, layout_code=layout_data.layout_code, fonts=layout_data.fonts, ) session.add(new_layout) saved_count += 1 await session.commit() return SaveLayoutsResponse( success=True, saved_count=saved_count, message=f"Successfully saved {saved_count} layout(s)", ) except HTTPException: # Re-raise HTTP exceptions as-is await session.rollback() raise except Exception as e: await session.rollback() print(f"Unexpected error saving layouts: {str(e)}") raise HTTPException( status_code=500, detail=f"Internal server error while saving layouts: {str(e)}", ) # ENDPOINT 5: Get layouts for a presentation @LAYOUT_MANAGEMENT_ROUTER.get( "/get-templates/{presentation}", response_model=GetLayoutsResponse, responses={ 400: {"model": ErrorResponse, "description": "Invalid presentation ID"}, 404: { "model": ErrorResponse, "description": "No layouts found for presentation", }, 500: {"model": ErrorResponse, "description": "Internal server error"}, }, ) async def get_layouts( presentation: UUID, session: AsyncSession = Depends(get_async_session) ): """ Retrieve all layouts for a specific presentation. Args: presentation: UUID of the presentation session: Database session Returns: GetLayoutsResponse with layouts data Raises: HTTPException: 404 if no layouts found, 400 for invalid UUID, 500 for server errors """ try: # Validate presentation_id format (basic UUID check) if not presentation or len(str(presentation).strip()) == 0: raise HTTPException( status_code=400, detail="Presentation ID cannot be empty" ) # Query layouts for the given presentation_id stmt = select(PresentationLayoutCodeModel).where( PresentationLayoutCodeModel.presentation == presentation ) result = await session.execute(stmt) layouts_db = result.scalars().all() # Check if any layouts were found if not layouts_db: raise HTTPException( status_code=404, detail=f"No layouts found for presentation ID: {presentation}", ) # Convert to response format layouts = [ LayoutData( presentation=layout.presentation, layout_id=layout.layout_id, layout_name=layout.layout_name, layout_code=layout.layout_code, fonts=layout.fonts, ) for layout in layouts_db ] # Aggregate unique fonts across all layouts aggregated_fonts: set[str] = set() for layout in layouts_db: if layout.fonts: aggregated_fonts.update([f for f in layout.fonts if isinstance(f, str)]) fonts_list = sorted(list(aggregated_fonts)) if aggregated_fonts else None # Fetch template meta template_meta = await session.get(TemplateModel, presentation) template = None if template_meta: template = { "id": template_meta.id, "name": template_meta.name, "description": template_meta.description, "created_at": template_meta.created_at, } return GetLayoutsResponse( success=True, layouts=layouts, message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation}", template=template, fonts=fonts_list, ) except HTTPException: # Re-raise HTTP exceptions as-is raise except Exception as e: print(f"Error retrieving layouts for presentation {presentation}: {str(e)}") raise HTTPException( status_code=500, detail=f"Internal server error while retrieving layouts: {str(e)}", ) # ENDPOINT: Get all presentations with layout counts @LAYOUT_MANAGEMENT_ROUTER.get( "/summary", response_model=GetPresentationSummaryResponse, summary="Get all presentations with layout counts", description="Retrieve a summary of all presentations and the number of layouts in each", responses={ 200: { "model": GetPresentationSummaryResponse, "description": "Presentations summary retrieved successfully", }, 500: {"model": ErrorResponse, "description": "Internal server error"}, }, ) async def get_presentations_summary( session: AsyncSession = Depends(get_async_session), ): """ Get summary of all presentations with their layout counts. """ try: # Query to get presentation_id, count of layouts, and MAX(updated_at) stmt = select( PresentationLayoutCodeModel.presentation, func.count(PresentationLayoutCodeModel.id).label("layout_count"), func.max(PresentationLayoutCodeModel.updated_at).label("last_updated_at"), ).group_by(PresentationLayoutCodeModel.presentation) result = await session.execute(stmt) presentation_data = result.all() # Convert to response format with template info if available presentations = [] for row in presentation_data: template_meta = await session.get(TemplateModel, row.presentation) template = None if template_meta: template = { "id": template_meta.id, "name": template_meta.name, "description": template_meta.description, "created_at": template_meta.created_at, } presentations.append( PresentationSummary( presentation_id=row.presentation, layout_count=row.layout_count, last_updated_at=row.last_updated_at, template=template, ) ) # Calculate totals total_presentations = len(presentations) total_layouts = sum(p.layout_count for p in presentations) return GetPresentationSummaryResponse( success=True, presentations=presentations, total_presentations=total_presentations, total_layouts=total_layouts, message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)", ) except Exception as e: print(f"Error retrieving presentations summary: {str(e)}") raise HTTPException( status_code=500, detail=f"Internal server error while retrieving presentations summary: {str(e)}", ) @LAYOUT_MANAGEMENT_ROUTER.post( "/templates", response_model=TemplateCreateResponse, responses={ 400: {"model": ErrorResponse, "description": "Validation error"}, 500: {"model": ErrorResponse, "description": "Internal server error"}, }, ) async def create_template( request: TemplateCreateRequest, session: AsyncSession = Depends(get_async_session), ): try: if not request.id or not request.name: raise HTTPException(status_code=400, detail="id and name are required") # Upsert template by id existing = await session.get(TemplateModel, request.id) if existing: existing.name = request.name existing.description = request.description else: session.add( TemplateModel( id=request.id, name=request.name, description=request.description ) ) await session.commit() # Read back template = await session.get(TemplateModel, request.id) return TemplateCreateResponse( success=True, template={ "id": template.id, "name": template.name, "description": template.description, "created_at": template.created_at, }, message="Template saved", ) except HTTPException: await session.rollback() raise except Exception as e: await session.rollback() raise HTTPException( status_code=500, detail=f"Failed to save template: {str(e)}" ) @LAYOUT_MANAGEMENT_ROUTER.delete("/delete-templates/{template_id}", status_code=204) async def delete_template( template_id: UUID, session: AsyncSession = Depends(get_async_session), ): try: await session.execute( delete(TemplateModel).where(TemplateModel.id == template_id) ) await session.execute( delete(PresentationLayoutCodeModel).where( PresentationLayoutCodeModel.presentation == template_id, ) ) await session.commit() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to delete template")