diff --git a/servers/fastapi/api/document_router.py b/servers/fastapi/api/document_router.py new file mode 100644 index 00000000..eeb23e57 --- /dev/null +++ b/servers/fastapi/api/document_router.py @@ -0,0 +1,143 @@ +from fastapi import APIRouter, File, UploadFile, HTTPException, BackgroundTasks +from fastapi.responses import FileResponse +from typing import List +import os +import uuid +import mimetypes +from datetime import datetime + +router = APIRouter(prefix="/api/v1/documents", tags=["documents"]) + +# Ensure upload directory exists +UPLOAD_DIRECTORY = "/app/uploads" +os.makedirs(UPLOAD_DIRECTORY, exist_ok=True) + +@router.get("/list") +async def list_documents(): + """ + List all uploaded documents + """ + documents = [] + try: + for filename in os.listdir(UPLOAD_DIRECTORY): + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + if os.path.isfile(file_path): + # Extract metadata from the filename + parts = filename.split('__') + original_filename = parts[0] if len(parts) > 1 else filename + file_id = parts[-1].split('.')[0] if len(parts) > 1 else filename.split('.')[0] + + documents.append({ + "id": file_id, + "filename": original_filename, + "size": os.path.getsize(file_path), + "upload_date": str(datetime.fromtimestamp(os.path.getctime(file_path))), + "content_type": mimetypes.guess_type(filename)[0] or 'application/octet-stream' + }) + return documents + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list documents: {str(e)}") + +@router.post("/upload") +async def upload_document(file: UploadFile = File(...)): + """ + Upload a new document + """ + try: + # Generate unique filename + file_id = str(uuid.uuid4()) + file_extension = file.filename.split('.')[-1] if '.' in file.filename else '' + + # Create a filename that preserves the original name and includes a unique ID + unique_filename = f"{file.filename}__{file_id}.{file_extension}" + file_path = os.path.join(UPLOAD_DIRECTORY, unique_filename) + + # Save the file + with open(file_path, "wb") as buffer: + buffer.write(await file.read()) + + # Create metadata + return { + "id": file_id, + "filename": file.filename, + "size": os.path.getsize(file_path), + "upload_date": str(datetime.now()), + "content_type": file.content_type or 'application/octet-stream' + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") + +@router.delete("/{document_id}") +async def delete_document(document_id: str): + """ + Delete a specific document + """ + try: + for filename in os.listdir(UPLOAD_DIRECTORY): + # Check if the filename contains the document_id + if document_id in filename: + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + os.remove(file_path) + return {"success": True, "message": "Document deleted successfully"} + + raise HTTPException(status_code=404, detail="Document not found") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Delete failed: {str(e)}") + +@router.get("/preview/{document_id}") +async def preview_document(document_id: str): + """ + Preview a specific document + """ + try: + for filename in os.listdir(UPLOAD_DIRECTORY): + # Check if the filename contains the document_id + if document_id in filename: + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + + # Extract original filename + original_filename = filename.split('__')[0] + + # Determine MIME type + mime_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream' + + return FileResponse( + file_path, + media_type=mime_type, + filename=original_filename + ) + + raise HTTPException(status_code=404, detail="Document not found") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Preview failed: {str(e)}") + + +@router.get("/download/{document_id}") +async def download_document(document_id: str): + """ + Download a specific document + """ + try: + for filename in os.listdir(UPLOAD_DIRECTORY): + # Check if the filename contains the document_id + if document_id in filename: + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + + # Extract original filename + original_filename = filename.split('__')[0] + + # Determine MIME type + mime_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream' + + return FileResponse( + file_path, + media_type=mime_type, + filename=original_filename, + headers={ + "Content-Disposition": f"attachment; filename*=UTF-8''{original_filename}" + } + ) + + raise HTTPException(status_code=404, detail="Document not found") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}") \ No newline at end of file diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index 2930cdbd..47f1ae78 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -1,26 +1,39 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from api.lifespan import app_lifespan -from api.middlewares import UserConfigEnvUpdateMiddleware +from api.middlewares import UserConfigEnvUpdateMiddleware, ServerActionsValidationMiddleware from api.v1.ppt.router import API_V1_PPT_ROUTER from api.v1.router import API_V1_ROUTER - +from api.document_router import router as document_router # Add this import app = FastAPI(lifespan=app_lifespan) - # Routers app.include_router(API_V1_PPT_ROUTER) app.include_router(API_V1_ROUTER) +app.include_router(document_router) # Add the document router # Middlewares -origins = ["*"] +origins = [ + "http://localhost:3000", # Next.js development server + "http://localhost:5001", # FastAPI server +] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], - allow_headers=["*"], + allow_headers=[ + "Content-Type", + "Authorization", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Origin", + "X-Requested-With", + "X-Forwarded-Host", + "Origin", + "*" + ], ) app.add_middleware(UserConfigEnvUpdateMiddleware) +app.add_middleware(ServerActionsValidationMiddleware) \ No newline at end of file diff --git a/servers/fastapi/api/middlewares.py b/servers/fastapi/api/middlewares.py index 6c4d83d9..fe737a36 100644 --- a/servers/fastapi/api/middlewares.py +++ b/servers/fastapi/api/middlewares.py @@ -1,4 +1,4 @@ -from fastapi import Request +from fastapi import Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response @@ -11,3 +11,25 @@ async def dispatch(self, request: Request, call_next): if get_can_change_keys_env() != "false": update_env_with_user_config() return await call_next(request) + + +class ServerActionsValidationMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # Validate Server Actions request headers + forwarded_host = request.headers.get('x-forwarded-host', '') + origin = request.headers.get('origin', '') + + # Allowed hosts for Server Actions + allowed_hosts = ['localhost:3000', 'localhost:5001'] + + # Check if the headers match or are in the allowed hosts + if forwarded_host and origin: + if forwarded_host not in allowed_hosts or origin not in allowed_hosts: + # Detailed error logging + print(f"Header mismatch: x-forwarded-host={forwarded_host}, origin={origin}") + raise HTTPException( + status_code=403, + detail="Invalid Server Actions request. Host and origin headers do not match." + ) + + return await call_next(request) diff --git a/servers/fastapi/api/v1/documents.py b/servers/fastapi/api/v1/documents.py new file mode 100644 index 00000000..3f1ec0ac --- /dev/null +++ b/servers/fastapi/api/v1/documents.py @@ -0,0 +1,111 @@ +import os +from typing import List, Dict, Any +from fastapi import APIRouter, File, UploadFile, Depends, HTTPException +from fastapi.responses import FileResponse + +from services.document_service import DocumentService +from dependencies.auth import get_current_user +from models.document_upload import ( + DocumentUploadRequest, + DocumentUploadResponse, + DocumentPreviewRequest, + DocumentPreviewResponse, + DocumentDownloadRequest +) + +router = APIRouter(prefix="/documents", tags=["documents"]) + +@router.post("/upload", response_model=DocumentUploadResponse) +async def upload_document( + file: UploadFile = File(...), + current_user: dict = Depends(get_current_user) +): + """ + Upload a document + + Args: + file (UploadFile): The file to upload + current_user (dict): Authenticated user details + + Returns: + DocumentUploadResponse: Details of the uploaded document + """ + try: + upload_result = DocumentService.upload_document(file, current_user['id']) + return upload_result + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") + +@router.post("/preview", response_model=DocumentPreviewResponse) +async def preview_document( + request: DocumentPreviewRequest, + current_user: dict = Depends(get_current_user) +): + """ + Generate a preview for a document + + Args: + request (DocumentPreviewRequest): Document preview request + current_user (dict): Authenticated user details + + Returns: + DocumentPreviewResponse: Document preview information + """ + try: + preview_result = DocumentService.preview_document( + request.document_id, + current_user['id'] + ) + return preview_result + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Preview failed: {str(e)}") + +@router.post("/download") +async def download_document( + request: DocumentDownloadRequest, + current_user: dict = Depends(get_current_user) +): + """ + Download a document + + Args: + request (DocumentDownloadRequest): Document download request + current_user (dict): Authenticated user details + + Returns: + FileResponse: The file to be downloaded + """ + try: + return DocumentService.download_document( + request.document_id, + current_user['id'] + ) + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}") + +@router.get("/", response_model=List[Dict[str, Any]]) +async def list_documents( + current_user: dict = Depends(get_current_user) +): + """ + List all documents for the current user + + Args: + current_user (dict): Authenticated user details + + Returns: + List[Dict[str, Any]]: List of documents + """ + try: + documents = DocumentService.list_documents(current_user['id']) + return documents + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to list documents: {str(e)}") diff --git a/servers/fastapi/models/document_upload.py b/servers/fastapi/models/document_upload.py new file mode 100644 index 00000000..44daadaa --- /dev/null +++ b/servers/fastapi/models/document_upload.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + +class DocumentUploadRequest(BaseModel): + file_name: str + file_type: str + file_size: int + organization_id: Optional[int] = None + +class DocumentUploadResponse(BaseModel): + document_id: int + file_name: str + file_path: str + uploaded_at: datetime + file_type: str + file_size: int + +class DocumentPreviewRequest(BaseModel): + document_id: int + +class DocumentPreviewResponse(BaseModel): + document_id: int + file_name: str + file_type: str + preview_url: Optional[str] = None + preview_text: Optional[str] = None + +class DocumentDownloadRequest(BaseModel): + document_id: int diff --git a/servers/fastapi/services/document_service.py b/servers/fastapi/services/document_service.py new file mode 100644 index 00000000..1c8a753c --- /dev/null +++ b/servers/fastapi/services/document_service.py @@ -0,0 +1,88 @@ +import os +import uuid +from typing import List, Optional +from fastapi import UploadFile, HTTPException, FileResponse +from pydantic import BaseModel +from datetime import datetime +import mimetypes + +class DocumentMetadata(BaseModel): + id: str + filename: str + size: int + upload_date: str + content_type: str + +class DocumentService: + UPLOAD_DIRECTORY = "/app/uploads" + + def __init__(self): + # Ensure upload directory exists + os.makedirs(self.UPLOAD_DIRECTORY, exist_ok=True) + + def upload_document(self, file: UploadFile) -> DocumentMetadata: + """ + Upload a document and save it to the server + """ + try: + # Generate unique filename + file_id = str(uuid.uuid4()) + file_extension = file.filename.split('.')[-1] if '.' in file.filename else '' + unique_filename = f"{file_id}.{file_extension}" + file_path = os.path.join(self.UPLOAD_DIRECTORY, unique_filename) + + # Save the file + with open(file_path, "wb") as buffer: + buffer.write(file.file.read()) + + # Create metadata + return DocumentMetadata( + id=file_id, + filename=file.filename, + size=os.path.getsize(file_path), + upload_date=str(datetime.now()), + content_type=file.content_type or 'application/octet-stream' + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") + + def list_documents(self) -> List[DocumentMetadata]: + """ + List all uploaded documents + """ + documents = [] + for filename in os.listdir(self.UPLOAD_DIRECTORY): + file_path = os.path.join(self.UPLOAD_DIRECTORY, filename) + if os.path.isfile(file_path): + file_id = filename.split('.')[0] + documents.append(DocumentMetadata( + id=file_id, + filename=filename, + size=os.path.getsize(file_path), + upload_date=str(datetime.fromtimestamp(os.path.getctime(file_path))), + content_type=mimetypes.guess_type(filename)[0] or 'application/octet-stream' + )) + return documents + + def get_document(self, document_id: str) -> Optional[str]: + """ + Get the file path for a specific document + """ + for filename in os.listdir(self.UPLOAD_DIRECTORY): + if filename.startswith(document_id): + return os.path.join(self.UPLOAD_DIRECTORY, filename) + raise HTTPException(status_code=404, detail="Document not found") + + def delete_document(self, document_id: str) -> bool: + """ + Delete a specific document + """ + file_path = self.get_document(document_id) + try: + os.remove(file_path) + return True + except Exception as e: + raise HTTPException(status_code=500, detail=f"Delete failed: {str(e)}") + +# Instantiate the service +document_service = DocumentService() \ No newline at end of file diff --git a/servers/nextjs/app/(auth)/layout.tsx b/servers/nextjs/app/(auth)/layout.tsx index 4cab7100..fd39f24b 100644 --- a/servers/nextjs/app/(auth)/layout.tsx +++ b/servers/nextjs/app/(auth)/layout.tsx @@ -8,13 +8,13 @@ export default function AuthLayout({ return (
{/* Background Image */} - Presentation workspace + /> */} {/* Dark overlay */}
diff --git a/servers/nextjs/app/(auth)/login/page.tsx b/servers/nextjs/app/(auth)/login/page.tsx index 3be379b6..59021c43 100644 --- a/servers/nextjs/app/(auth)/login/page.tsx +++ b/servers/nextjs/app/(auth)/login/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; -import { login, isAuthenticated } from "@/lib/auth"; +import { login, isAuthenticated, setAuthToken } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -36,11 +36,13 @@ export default function LoginPage() { setIsLoading(true); try { - await login({ email, password }); + const resp=await login({ email, password }); toast.success("Login successful!"); - + + console.log("Login response:", resp); // Redirect to the page they came from or dashboard const from = searchParams.get("from") || "/dashboard"; + setAuthToken(resp?.access_token, resp.user?.is_admin); router.replace(from); } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to login"); @@ -50,8 +52,8 @@ export default function LoginPage() { }; return ( -
- +
+ Login diff --git a/servers/nextjs/app/(auth)/signup/page.tsx b/servers/nextjs/app/(auth)/signup/page.tsx index bd9afaa9..7460b0b1 100644 --- a/servers/nextjs/app/(auth)/signup/page.tsx +++ b/servers/nextjs/app/(auth)/signup/page.tsx @@ -6,6 +6,8 @@ import Link from "next/link"; import { signup, setAuthToken } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; import { Card, CardHeader, @@ -22,19 +24,38 @@ export default function SignupPage() { const [organizationName, setOrganizationName] = useState(""); const [userName, setUserName] = useState(""); const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [role, setRole] = useState("user"); + const [passwordError, setPasswordError] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); + + // Validate passwords match + if (password !== confirmPassword) { + setPasswordError("Passwords do not match"); + setIsLoading(false); + return; + } + + // Validate password strength + if (password.length < 8) { + setPasswordError("Password must be at least 8 characters long"); + setIsLoading(false); + return; + } try { const response = await signup({ organisation_name: organizationName, admin_full_name: userName, admin_email: email, - admin_password: "Test@123", + admin_password: password, + is_admin: role, }); - setAuthToken(response.token); + setAuthToken(response.token, role); toast.success("Account created successfully!"); router.push("/dashboard"); } catch (error) { @@ -47,7 +68,7 @@ export default function SignupPage() { }; return ( -
+
@@ -99,6 +120,60 @@ export default function SignupPage() { disabled={isLoading} />
+
+ + { + setPassword(e.target.value); + setPasswordError(""); + }} + required + disabled={isLoading} + /> +
+
+ + { + setConfirmPassword(e.target.value); + setPasswordError(""); + }} + required + disabled={isLoading} + /> + {passwordError && ( +

{passwordError}

+ )} +
+
+ + +
+ + +
+
+ + +
+
+
- - My Account - + + {user && ( + <> + +
+ {user.full_name} + + {user.role || 'User'} + +
+
+ + + )} Log out diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx index 41fabd4c..29e0fa38 100644 --- a/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import Wrapper from "@/components/Wrapper"; -import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; +import { dashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; import { PresentationGrid } from "@/app/(presentation-generator)/dashboard/components/PresentationGrid"; import Header from "@/app/(presentation-generator)/dashboard/components/Header"; @@ -24,7 +24,7 @@ const DashboardPage: React.FC = () => { try { setIsLoading(true); setError(null); - const data = await DashboardApi.getPresentations(); + const data = await dashboardApi.getPresentations(); data.sort( (a: any, b: any) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx index 8e838693..41e75412 100644 --- a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx @@ -16,7 +16,9 @@ const Header = () => {
{pathname !== "/upload" && pathname !== "/dashboard" && }
- +
+ +
diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx index 7abebac8..84e1c2f1 100644 --- a/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { Card } from "@/components/ui/card"; -import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; +import { dashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; import { DotsVerticalIcon, TrashIcon } from "@radix-ui/react-icons"; import { Popover, @@ -37,7 +37,7 @@ export const PresentationCard = ({ e.preventDefault(); e.stopPropagation(); - const response = await DashboardApi.deletePresentation(id); + const response = await dashboardApi.deletePresentation(id); if (response) { toast.success("Presentation deleted", { diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx index 945cd3fd..663a141a 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx @@ -56,6 +56,10 @@ const GroupLayouts: React.FC = ({ layoutId, groupName, } = layout; + + // Check if the layout component is an error component + const isErrorComponent = LayoutComponent?.displayName === "CustomTemplateErrorSlide"; + return (
= ({ >
- + {isErrorComponent ? ( +
+ Layout Preview +
+ ) : ( + + )}
); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index 5e0c4639..df0d4e87 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -49,12 +49,6 @@ const LayoutSelection: React.FC = ({ if (groups.length === 0) return []; const Groups: LayoutGroup[] = groups - .filter(groupName => { - // Filter out groups that contain any errored layouts (from custom templates compile/parse errors) - const fullData = getFullDataByGroup(groupName); - const hasErroredLayouts = fullData.some(fd => (fd as any)?.component?.displayName === "CustomTemplateErrorSlide"); - return !hasErroredLayouts; - }) .map(groupName => { const settings = getGroupSetting(groupName); const customMeta = summaryMap[groupName]; @@ -89,15 +83,44 @@ const LayoutSelection: React.FC = ({ // Auto-select first group when groups are loaded useEffect(() => { if (layoutGroups.length > 0 && !selectedLayoutGroup) { - const defaultGroup = layoutGroups.find(g => g.default) || layoutGroups[0]; - const slides = getLayoutsByGroup(defaultGroup.id); - - onSelectLayoutGroup({ - ...defaultGroup, - slides: slides, - }); + // Try to find a default group with valid slides + let validGroupFound = false; + + // First try the default group + const defaultGroup = layoutGroups.find(g => g.default); + if (defaultGroup) { + const slides = getLayoutsByGroup(defaultGroup.id); + if (slides && slides.length > 0) { + onSelectLayoutGroup({ + ...defaultGroup, + slides: slides, + }); + validGroupFound = true; + } + } + + // If no valid default group, try each group until we find one with valid slides + if (!validGroupFound) { + for (const group of layoutGroups) { + const slides = getLayoutsByGroup(group.id); + if (slides && slides.length > 0) { + onSelectLayoutGroup({ + ...group, + slides: slides, + }); + validGroupFound = true; + break; + } + } + } + + // If still no valid group found, show an error + if (!validGroupFound && layoutGroups.length > 0) { + console.error("No valid templates found in any group"); + alert("No valid templates found. Please try refreshing the page or contact support."); + } } - }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]); + }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup, getLayoutsByGroup]); useEffect(() => { if (loading) { return; @@ -152,10 +175,18 @@ const LayoutSelection: React.FC = ({ const handleLayoutGroupSelection = (group: LayoutGroup) => { const slides = getLayoutsByGroup(group.id); - onSelectLayoutGroup({ - ...group, - slides: slides, - }); + + // Make sure we have at least one valid slide + if (slides && slides.length > 0) { + onSelectLayoutGroup({ + ...group, + slides: slides, + }); + } else { + // If no slides are found, show an error message + console.error(`No valid slides found for group: ${group.id}`); + alert("No valid templates found in this group. Please select another template group."); + } } return ( @@ -201,4 +232,4 @@ const LayoutSelection: React.FC = ({ ); }; -export default LayoutSelection; \ No newline at end of file +export default LayoutSelection; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx index 54ba9ae4..844f0634 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx @@ -22,7 +22,6 @@ import Link from "next/link"; import { RootState } from "@/store/store"; import { toast } from "sonner"; - import Announcement from "@/components/Announcement"; import { PptxPresentationModel } from "@/types/pptx_models"; import HeaderNav from "../../components/HeaderNab"; @@ -30,12 +29,13 @@ import PDFIMAGE from "@/public/pdf.svg"; import PPTXIMAGE from "@/public/pptx.svg"; import Image from "next/image"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; +import { UserProfile } from "../../components/UserProfile"; const Header = ({ presentation_id, currentSlide, }: { - presentation_id: string; + presentation_id?: string; currentSlide?: number; }) => { const [open, setOpen] = useState(false); @@ -217,10 +217,7 @@ const Header = ({ showProgress={true} duration={40} /> -
- +
@@ -237,18 +234,17 @@ const Header = ({ )} - +
{/* Mobile Menu */}
- +
-
); diff --git a/servers/nextjs/app/(presentation-generator)/services/api/documents.ts b/servers/nextjs/app/(presentation-generator)/services/api/documents.ts new file mode 100644 index 00000000..a0514b4d --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/services/api/documents.ts @@ -0,0 +1,84 @@ +import axios from 'axios'; + +const DOCUMENT_API_BASE_URL = '/api/v1/documents'; + +export interface DocumentUploadResponse { + id: string; + filename: string; + size: number; + upload_date: string; + content_type: string; +} + +export const documentsApi = { + async getUploadedDocuments(): Promise { + try { + const response = await axios.get(`${DOCUMENT_API_BASE_URL}/list`); + return response.data.map((doc: DocumentUploadResponse) => ({ + id: doc.id, + filename: doc.filename, + upload_date: doc.upload_date, + size: doc.size, + content_type: doc.content_type + })); + } catch (error) { + console.error('Failed to fetch documents:', error); + return []; + } + }, + + async uploadDocument(file: File, category: string): Promise { + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await axios.post(`${DOCUMENT_API_BASE_URL}/upload`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + + return { + id: response.data.id, + filename: response.data.filename, + upload_date: response.data.upload_date, + size: response.data.size, + content_type: response.data.content_type + }; + } catch (error) { + console.error('Document upload failed:', error); + throw error; + } + }, + + async deleteDocument(documentId: string): Promise { + try { + await axios.delete(`${DOCUMENT_API_BASE_URL}/${documentId}`); + } catch (error) { + console.error('Document deletion failed:', error); + throw error; + } + }, + + async previewDocument(documentId: string): Promise { + try { + const response = await axios.get(`${DOCUMENT_API_BASE_URL}/preview/${documentId}`, { + responseType: 'blob' + }); + return response.data; + } catch (error) { + console.error('Document preview failed:', error); + throw error; + } + }, + + async downloadDocument(documentId: string): Promise { + try { + const response = await axios.get(`${DOCUMENT_API_BASE_URL}/download/${documentId}`, { + responseType: 'blob' + }); + return response.data; + } catch (error) { + console.error('Document download failed:', error); + throw error; + } + } +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/services/api/header.ts b/servers/nextjs/app/(presentation-generator)/services/api/header.ts index d84251d5..580e78f6 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/header.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/header.ts @@ -1,17 +1,31 @@ export const getHeader = () => { + // Get the auth token from localStorage if available + let authToken = ''; + if (typeof window !== 'undefined') { + authToken = localStorage.getItem('auth_token') || ''; + } + return { "Content-Type": "application/json", Accept: "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Authorization": authToken ? `Bearer ${authToken}` : 'default_user', }; }; export const getHeaderForFormData = () => { + // Get the auth token from localStorage if available + let authToken = ''; + if (typeof window !== 'undefined') { + authToken = localStorage.getItem('auth_token') || ''; + } + return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Authorization", + "Authorization": authToken ? `Bearer ${authToken}` : 'default_user', }; }; diff --git a/servers/nextjs/app/(presentation-generator)/services/api/organizations.ts b/servers/nextjs/app/(presentation-generator)/services/api/organizations.ts index f3c0ef8a..3d17897c 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/organizations.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/organizations.ts @@ -11,13 +11,15 @@ export interface LoginRequest { export interface LoginResponse { access_token: string; token_type: string; + user: UserInfo; } export interface OrganizationRegisterRequest { - email: string; - password: string; + admin_email: string; + admin_password: string; organisation_name: string; - full_name: string; + admin_full_name: string; + is_admin?: string; } export interface UserInfo { @@ -25,6 +27,7 @@ export interface UserInfo { email: string; organisation_name?: string; full_name?: string; + is_admin?: string; } export const organizationApi = { diff --git a/servers/nextjs/app/(presentation-generator)/services/api/params.ts b/servers/nextjs/app/(presentation-generator)/services/api/params.ts index bc50ef8b..022a2d1a 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/params.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/params.ts @@ -6,25 +6,20 @@ export interface ImageSearch { } export interface ImageGenerate { - - prompt: string; } export interface IconSearch { - - query: string; limit: number; } export interface PreviousGeneratedImagesResponse { - - extras: { - prompt: string; - theme_prompt: string | null; - }, - created_at: string; - id: string; - path: string; -} \ No newline at end of file + extras: { + prompt: string; + theme_prompt: string | null; + }; + created_at: string; + id: string; + path: string; +} diff --git a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts index ec27d9e3..f4ab7878 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts @@ -1,75 +1,28 @@ -import { getHeader, getHeaderForFormData } from "./header"; +import { getHeader } from "./header"; import { IconSearch, ImageGenerate, ImageSearch, PreviousGeneratedImagesResponse } from "./params"; import { ApiResponseHandler } from "./api-error-handler"; export class PresentationGenerationApi { - static async uploadDoc(documents: File[]) { - const formData = new FormData(); - - documents.forEach((document) => { - formData.append("files", document); - }); - - try { - const response = await fetch( - `/api/v1/ppt/files/upload`, - { - method: "POST", - headers: getHeaderForFormData(), - body: formData, - cache: "no-cache", - } - ); - - return await ApiResponseHandler.handleResponse(response, "Failed to upload documents"); - } catch (error) { - console.error("Upload error:", error); - throw error; - } - } - - static async decomposeDocuments(documentKeys: string[]) { - try { - const response = await fetch( - `/api/v1/ppt/files/decompose`, - { - method: "POST", - headers: getHeader(), - body: JSON.stringify({ - file_paths: documentKeys, - }), - cache: "no-cache", - } - ); - - return await ApiResponseHandler.handleResponse(response, "Failed to decompose documents"); - } catch (error) { - console.error("Error in Decompose Files", error); - throw error; - } - } static async createPresentation({ prompt, n_slides, - file_paths, language, }: { prompt: string; n_slides: number | null; - file_paths?: string[]; language: string | null; }) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/presentation/create`, + `${baseUrl}/api/v1/ppt/presentation/create`, { method: "POST", headers: getHeader(), body: JSON.stringify({ prompt, n_slides, - file_paths, language, }), cache: "no-cache", @@ -88,8 +41,9 @@ export class PresentationGenerationApi { prompt: string ) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/slide/edit`, + `${baseUrl}/api/v1/ppt/slide/edit`, { method: "POST", headers: getHeader(), @@ -110,8 +64,10 @@ export class PresentationGenerationApi { static async updatePresentationContent(body: any) { try { + // Use the correct API endpoint URL with the base URL + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/presentation/update`, + `${baseUrl}/api/v1/ppt/presentation/update`, { method: "PUT", headers: getHeader(), @@ -129,8 +85,9 @@ export class PresentationGenerationApi { static async presentationPrepare(presentationData: any) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/presentation/prepare`, + `${baseUrl}/api/v1/ppt/presentation/prepare`, { method: "POST", headers: getHeader(), @@ -151,8 +108,9 @@ export class PresentationGenerationApi { static async generateImage(imageGenerate: ImageGenerate) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/images/generate?prompt=${imageGenerate.prompt}`, + `${baseUrl}/api/v1/ppt/images/generate?prompt=${imageGenerate.prompt}`, { method: "GET", headers: getHeader(), @@ -169,8 +127,9 @@ export class PresentationGenerationApi { static getPreviousGeneratedImages = async (): Promise => { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/images/generated`, + `${baseUrl}/api/v1/ppt/images/generated`, { method: "GET", headers: getHeader(), @@ -186,8 +145,9 @@ export class PresentationGenerationApi { static async searchIcons(iconSearch: IconSearch) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/icons/search?query=${iconSearch.query}&limit=${iconSearch.limit}`, + `${baseUrl}/api/v1/ppt/icons/search?query=${iconSearch.query}&limit=${iconSearch.limit}`, { method: "GET", headers: getHeader(), @@ -207,8 +167,9 @@ export class PresentationGenerationApi { // EXPORT PRESENTATION static async exportAsPPTX(presentationData: any) { try { + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; const response = await fetch( - `/api/v1/ppt/presentation/export/pptx`, + `${baseUrl}/api/v1/ppt/presentation/export/pptx`, { method: "POST", headers: getHeader(), @@ -225,4 +186,4 @@ export class PresentationGenerationApi { -} \ No newline at end of file +} diff --git a/servers/nextjs/app/(presentation-generator)/upload-documents/components/DocumentPreview.tsx b/servers/nextjs/app/(presentation-generator)/upload-documents/components/DocumentPreview.tsx new file mode 100644 index 00000000..28db4e82 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/upload-documents/components/DocumentPreview.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { DownloadIcon, FileIcon } from 'lucide-react'; +import { toast } from 'sonner'; + +// Define the prop types +interface DocumentPreviewProps { + isOpen: boolean; + onClose: () => void; + document: { + id: string; + name: string; + category: string; + } | null; +} + +// Main component function +export function DocumentPreview({ + isOpen, + onClose, + document +}: DocumentPreviewProps) { + // State and effect hooks + const [fileBlob, setFileBlob] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + // Fetch document preview + React.useEffect(() => { + const fetchPreview = async () => { + if (!document || !isOpen) return; + + setIsLoading(true); + try { + // Dynamically import the API to avoid circular dependencies + const { documentsApi } = await import('../../services/api/documents'); + const blob = await documentsApi.previewDocument(document.id); + setFileBlob(blob); + } catch (error) { + console.error('Preview failed:', error); + toast.error('Failed to load document preview'); + } finally { + setIsLoading(false); + } + }; + + fetchPreview(); + }, [document, isOpen]); + + // Download handler + const handleDownload = async () => { + if (!document) return; + + try { + const { documentsApi } = await import('../../services/api/documents'); + + const blob = await documentsApi.downloadDocument(document.id); + + // Ensure blob is valid + if (!(blob instanceof Blob)) { + throw new Error('Invalid download response'); + } + + // Use modern download method + const url = URL.createObjectURL(blob); + const link = window.document.createElement('a'); + link.href = url; + link.download = document.name; + window.document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast.success('Document downloaded successfully'); + } catch (error) { + console.error('Download failed:', error); + toast.error('Failed to download document'); + } + }; + + // Render preview content + const renderPreviewContent = () => { + if (isLoading) return
Loading...
; + if (!fileBlob) return
No preview available
; + + // Determine file type + const fileType = fileBlob.type; + + // Handle different file types + if (fileType === 'application/pdf') { + return ( +