diff --git a/src/landppt/api/global_master_template_api.py b/src/landppt/api/global_master_template_api.py new file mode 100644 index 0000000..60a3e3f --- /dev/null +++ b/src/landppt/api/global_master_template_api.py @@ -0,0 +1,360 @@ +""" +Global Master Template API endpoints +""" + +import logging +from typing import List, Optional +from fastapi import APIRouter, HTTPException, Depends, Query +from fastapi.responses import JSONResponse + +from .models import ( + GlobalMasterTemplateCreate, GlobalMasterTemplateUpdate, GlobalMasterTemplateResponse, + GlobalMasterTemplateDetailResponse, GlobalMasterTemplateGenerateRequest, + TemplateSelectionRequest, TemplateSelectionResponse +) +from ..services.global_master_template_service import GlobalMasterTemplateService + +logger = logging.getLogger(__name__) + +# Create router +router = APIRouter(prefix="/api/global-master-templates", tags=["Global Master Templates"]) + +# Service instance +template_service = GlobalMasterTemplateService() + + +@router.post("/", response_model=GlobalMasterTemplateResponse) +async def create_template(template_data: GlobalMasterTemplateCreate): + """Create a new global master template""" + try: + result = await template_service.create_template(template_data.model_dump()) + return GlobalMasterTemplateResponse(**result) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Failed to create template: {e}") + raise HTTPException(status_code=500, detail="Failed to create template") + + +@router.get("/", response_model=dict) +async def get_all_templates( + active_only: bool = Query(True, description="Only return active templates"), + tags: Optional[str] = Query(None, description="Filter by tags (comma-separated)"), + page: int = Query(1, ge=1, description="Page number (1-based)"), + page_size: int = Query(6, ge=1, le=100, description="Number of items per page"), + search: Optional[str] = Query(None, description="Search in template name and description") +): + """Get all global master templates with pagination""" + try: + if tags: + tag_list = [tag.strip() for tag in tags.split(",")] + result = await template_service.get_templates_by_tags_paginated( + tag_list, active_only, page, page_size, search + ) + else: + result = await template_service.get_all_templates_paginated( + active_only, page, page_size, search + ) + + return { + "templates": [GlobalMasterTemplateResponse(**template) for template in result["templates"]], + "pagination": result["pagination"] + } + except Exception as e: + logger.error(f"Failed to get templates: {e}") + raise HTTPException(status_code=500, detail="Failed to get templates") + + +@router.get("/{template_id}", response_model=GlobalMasterTemplateDetailResponse) +async def get_template_by_id(template_id: int): + """Get a global master template by ID""" + try: + template = await template_service.get_template_by_id(template_id) + if not template: + raise HTTPException(status_code=404, detail="Template not found") + + return GlobalMasterTemplateDetailResponse(**template) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to get template") + + +@router.put("/{template_id}", response_model=dict) +async def update_template(template_id: int, update_data: GlobalMasterTemplateUpdate): + """Update a global master template""" + try: + # Filter out None values + update_dict = {k: v for k, v in update_data.model_dump().items() if v is not None} + + if not update_dict: + raise HTTPException(status_code=400, detail="No update data provided") + + success = await template_service.update_template(template_id, update_dict) + if not success: + raise HTTPException(status_code=404, detail="Template not found") + + return {"success": True, "message": "Template updated successfully"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to update template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to update template") + + +@router.delete("/{template_id}", response_model=dict) +async def delete_template(template_id: int): + """Delete a global master template""" + try: + success = await template_service.delete_template(template_id) + if not success: + raise HTTPException(status_code=404, detail="Template not found") + + return {"success": True, "message": "Template deleted successfully"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to delete template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to delete template") + + +@router.post("/{template_id}/set-default", response_model=dict) +async def set_default_template(template_id: int): + """Set a template as the default template""" + try: + success = await template_service.set_default_template(template_id) + if not success: + raise HTTPException(status_code=404, detail="Template not found") + + return {"success": True, "message": "Default template set successfully"} + except Exception as e: + logger.error(f"Failed to set default template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to set default template") + + +@router.get("/default/template", response_model=GlobalMasterTemplateDetailResponse) +async def get_default_template(): + """Get the default global master template""" + try: + template = await template_service.get_default_template() + if not template: + raise HTTPException(status_code=404, detail="No default template found") + + return GlobalMasterTemplateDetailResponse(**template) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get default template: {e}") + raise HTTPException(status_code=500, detail="Failed to get default template") + + +@router.post("/generate") +async def generate_template_with_ai(request: GlobalMasterTemplateGenerateRequest): + """Generate a new template using AI (does not save to database)""" + try: + # 准备参考图片数据 + reference_image_data = None + if request.reference_image: + reference_image_data = { + "filename": request.reference_image.filename, + "data": request.reference_image.data, + "size": request.reference_image.size, + "type": request.reference_image.type + } + + # 使用AI生成服务(不保存到数据库) + result = await template_service.generate_template_with_ai( + prompt=request.prompt, + template_name=request.template_name, + description=request.description, + tags=request.tags, + generation_mode=request.generation_mode, + reference_image=reference_image_data + ) + + return { + "success": True, + "message": "模板生成完成!", + "data": result + } + + except Exception as e: + logger.error(f"Failed to generate template with AI: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/save-generated", response_model=GlobalMasterTemplateResponse) +async def save_generated_template(request: dict): + """Save a generated template after user confirmation""" + try: + # Extract template data from request + template_data = { + 'template_name': request.get('template_name'), + 'description': request.get('description', ''), + 'html_template': request.get('html_template'), + 'tags': request.get('tags', []), + 'created_by': 'AI' + } + + # Add timestamp to avoid name conflicts + import time + timestamp = int(time.time()) + template_data['template_name'] = f"{template_data['template_name']}_{timestamp}" + + result = await template_service.create_template(template_data) + return GlobalMasterTemplateResponse(**result) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Failed to save generated template: {e}") + raise HTTPException(status_code=500, detail="Failed to save template") + + +@router.post("/adjust-template") +async def adjust_template(request: dict): + """Adjust a generated template based on user feedback""" + from fastapi.responses import StreamingResponse + import json + + async def adjust_stream(): + try: + # Get the current template and adjustment request + current_html = request.get('html_template') + adjustment_request = request.get('adjustment_request') + template_name = request.get('template_name', '模板') + + # Send initial status + yield f"data: {json.dumps({'type': 'status', 'message': '正在分析调整需求...'})}\n\n" + + # Use the template service to adjust the template + async for chunk in template_service.adjust_template_with_ai_stream( + current_html=current_html, + adjustment_request=adjustment_request, + template_name=template_name + ): + yield f"data: {json.dumps(chunk)}\n\n" + + except Exception as e: + logger.error(f"Failed to adjust template: {e}") + yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n" + + return StreamingResponse( + adjust_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream" + } + ) + + +@router.post("/select", response_model=TemplateSelectionResponse) +async def select_template_for_project(request: TemplateSelectionRequest): + """Select a template for PPT generation""" + try: + if request.selected_template_id: + # Get the selected template + template = await template_service.get_template_by_id(request.selected_template_id) + if not template: + raise HTTPException(status_code=404, detail="Selected template not found") + + # Increment usage count + await template_service.increment_template_usage(request.selected_template_id) + + return TemplateSelectionResponse( + success=True, + message="Template selected successfully", + selected_template=GlobalMasterTemplateResponse(**template) + ) + else: + # Use default template + template = await template_service.get_default_template() + if not template: + raise HTTPException(status_code=404, detail="No default template found") + + # Increment usage count + await template_service.increment_template_usage(template['id']) + + return TemplateSelectionResponse( + success=True, + message="Default template selected", + selected_template=GlobalMasterTemplateResponse(**template) + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to select template: {e}") + raise HTTPException(status_code=500, detail="Failed to select template") + + +@router.post("/{template_id}/duplicate", response_model=GlobalMasterTemplateResponse) +async def duplicate_template(template_id: int, new_name: str = Query(..., description="New template name")): + """Duplicate an existing template""" + try: + # Get the original template + original = await template_service.get_template_by_id(template_id) + if not original: + raise HTTPException(status_code=404, detail="Template not found") + + # Create duplicate data + duplicate_data = { + 'template_name': new_name, + 'description': f"复制自: {original['template_name']}", + 'html_template': original['html_template'], + 'tags': original['tags'] + ['复制'], + 'created_by': 'duplicate' + } + + result = await template_service.create_template(duplicate_data) + return GlobalMasterTemplateResponse(**result) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to duplicate template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to duplicate template") + + +@router.get("/{template_id}/preview", response_model=dict) +async def get_template_preview(template_id: int): + """Get template preview data""" + try: + template = await template_service.get_template_by_id(template_id) + if not template: + raise HTTPException(status_code=404, detail="Template not found") + + return { + "id": template['id'], + "template_name": template['template_name'], + "preview_image": template['preview_image'], + "html_template": template['html_template'] + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get template preview {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to get template preview") + + +# Add increment usage endpoint for internal use +@router.post("/{template_id}/increment-usage", response_model=dict) +async def increment_template_usage(template_id: int): + """Increment template usage count (internal use)""" + try: + success = await template_service.increment_template_usage(template_id) + if not success: + raise HTTPException(status_code=404, detail="Template not found") + + return {"success": True, "message": "Usage count incremented"} + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to increment usage for template {template_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to increment usage count")