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 */}
-
+ /> */}
{/* 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}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 (
+
+ );
+ }
+
+ // For other file types, show download option
+ return (
+
+
+
File type not supported for preview
+
+ Please use the download button to view the document
+
+
+ );
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/upload-documents/page.tsx b/servers/nextjs/app/(presentation-generator)/upload-documents/page.tsx
new file mode 100644
index 00000000..710a95a0
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/upload-documents/page.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { toast } from "sonner";
+import { isAdmin } from "@/lib/auth";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { FileIcon, UploadIcon, FileTextIcon, Trash2Icon } from "lucide-react";
+import { DocumentPreview } from "./components/DocumentPreview";
+import { documentsApi, DocumentUploadResponse } from "../services/api/documents";
+
+export default function UploadDocumentsPage() {
+
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState("sales");
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [uploadedDocuments, setUploadedDocuments] = useState<
+ Array<{
+ id: string;
+ name: string;
+ category: string;
+ uploadDate: Date;
+ }>
+ >([]);
+ const [activeTab, setActiveTab] = useState("upload");
+ const [previewDocument, setPreviewDocument] = useState<{
+ id: string;
+ name: string;
+ category: string;
+ } | null>(null);
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
+
+ // Check if user is admin, redirect if not
+ useEffect(() => {
+ if (!isAdmin()) {
+ toast.error("You don't have permission to access this page");
+ router.push("/dashboard");
+ } else {
+ // Load uploaded documents
+ loadUploadedDocuments();
+ }
+ }, [activeTab]);
+
+ const loadUploadedDocuments = async () => {
+ try {
+ const documents = await documentsApi.getUploadedDocuments();
+ setUploadedDocuments(
+ documents.map((doc: DocumentUploadResponse) => ({
+ id: doc.id,
+ name: doc.filename,
+ category: selectedCategory, // Note: Backend should return category
+ uploadDate: new Date(doc.upload_date),
+ }))
+ );
+ console.log("Uploaded documents:", uploadedDocuments);
+ console.log("documents:", documents);
+ } catch (error) {
+ toast.error("Failed to load documents");
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ setSelectedFile(e.target.files[0]);
+ }
+ };
+
+ const handleUpload = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!selectedFile) {
+ toast.error("Please select a file to upload");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const uploadedDoc = await documentsApi.uploadDocument(
+ selectedFile,
+ selectedCategory
+ );
+
+ // Add the uploaded document to the list
+ const newDocument = {
+ id: uploadedDoc.id,
+ name: uploadedDoc.filename,
+ category: selectedCategory,
+ uploadDate: new Date(uploadedDoc.upload_date),
+ };
+
+ setUploadedDocuments((prev) => [newDocument, ...prev]);
+ setSelectedFile(null);
+
+ // Reset the file input
+ const fileInput = document.getElementById(
+ "file-upload"
+ ) as HTMLInputElement;
+ if (fileInput) {
+ fileInput.value = "";
+ }
+
+ toast.success("Document uploaded successfully");
+ setActiveTab("documents");
+ } catch (error) {
+ toast.error("Failed to upload document");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleDeleteDocument = async (id: string) => {
+ try {
+ await documentsApi.deleteDocument(id);
+
+ setUploadedDocuments((prev) => prev.filter((doc) => doc.id !== id));
+ toast.success("Document deleted successfully");
+ } catch (error) {
+ toast.error("Failed to delete document");
+ }
+ };
+
+ const handlePreviewDocument = async (id: string) => {
+ console.log("Previewing document with ID:", id);
+ try {
+ const previewResult = await documentsApi.previewDocument(id);
+
+ const document = uploadedDocuments.find((doc) => doc.id === id);
+ if (document) {
+ setPreviewDocument({
+ id: document.id,
+ name: document.name,
+ category: document.category,
+ });
+ setIsPreviewOpen(true);
+ }
+ } catch (error) {
+ toast.error("Failed to preview document");
+ }
+ };
+
+ return (
+
+
+ Document Management
+
+
+
+
+ Upload Documents
+ Uploaded Documents
+
+
+
+
+
+ Upload New Document
+
+
+
+
+
+
+
+
+
+
+ Uploaded Documents
+
+
+ {uploadedDocuments.length === 0 ? (
+
+ No documents have been uploaded yet.
+
+ ) : (
+
+ {uploadedDocuments.map((doc) => (
+
+
+
+
+
{doc.name}
+
+ {doc.category}
+ •
+ {doc.uploadDate.toLocaleDateString()}
+
+
+
+
+ handlePreviewDocument(doc.id)}
+ >
+ Preview
+
+ handleDeleteDocument(doc.id)}
+ className="text-destructive hover:text-destructive"
+ >
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
setIsPreviewOpen(false)}
+ document={previewDocument}
+ />
+
+ );
+}
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.cy.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.cy.tsx
index d9a781bc..730ad8ba 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.cy.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.cy.tsx
@@ -112,169 +112,36 @@ describe('', () => {
})
})
- describe('File Upload', () => {
- it('should handle document uploads', () => {
- cy.fixture('example.txt').as('testFile')
- cy.get('[data-testid="file-upload-input"]').selectFile('@testFile', { force: true })
- cy.contains('example.txt').should('exist')
- // Check for success toast
- checkToast('Files selected')
- })
- })
- describe('File Handling', () => {
- beforeEach(() => {
- // Create a text file fixture
- cy.writeFile('cypress/fixtures/test-doc.txt', 'Test content')
- })
-
- it('should handle multiple document uploads', () => {
- // Create two files with different names
- const file1 = new File(['content1'], 'document1.txt', { type: 'text/plain' })
- const file2 = new File(['content2'], 'document2.txt', { type: 'text/plain' })
-
- // Upload multiple files
- cy.get('[data-testid="file-upload-input"]')
- .selectFile([
- { contents: file1, fileName: 'document1.txt' },
- { contents: file2, fileName: 'document2.txt' }
- ], { force: true })
-
- // Verify files are listed
- cy.get('[data-testid="file-list"]').within(() => {
- cy.contains('document1.txt').should('be.visible')
- cy.contains('document2.txt').should('be.visible')
- })
- checkToast('Files selected')
- })
-
- it('should handle image uploads', () => {
- // Create an image file
- const imageFile = new File(['image content'], 'test-image.jpg', { type: 'image/jpeg' })
-
- cy.get('[data-testid="file-upload-input"]')
- .selectFile({
- contents: imageFile,
- fileName: 'test-image.jpg',
- mimeType: 'image/jpeg'
- }, { force: true })
-
- // Verify image is listed
- cy.get('[data-testid="file-list"]').within(() => {
- cy.contains('test-image.jpg').should('be.visible')
- })
- checkToast('Files selected')
- })
-
- it('should handle mixed document and image uploads', () => {
- // Create document and image files
- const docFile = new File(['doc content'], 'test-doc.txt', { type: 'text/plain' })
- const imageFile = new File(['image content'], 'test-image.jpg', { type: 'image/jpeg' })
-
- cy.get('[data-testid="file-upload-input"]')
- .selectFile([
- { contents: docFile, fileName: 'test-doc.txt' },
- { contents: imageFile, fileName: 'test-image.jpg', mimeType: 'image/jpeg' }
- ], { force: true })
-
- // Verify both files are listed
- cy.get('[data-testid="file-list"]').within(() => {
- cy.contains('test-doc.txt').should('be.visible')
- cy.contains('test-image.jpg').should('be.visible')
- })
- checkToast('Files selected')
- })
- })
+ // File upload functionality has been removed
describe('Validation', () => {
- it('should show error when no prompt or documents provided', () => {
- // Click next without entering prompt or uploading files
+ it('should show error when no prompt is provided', () => {
+ // Click next without entering prompt
cy.contains('button', 'Next').click()
// Check for error toast
- checkToast('No Prompt or Document Provided')
- })
- it('should show error when no prompt provided but research mode is on', () => {
- cy.get('[data-testid="research-mode-switch"]').click({ force: true })
- cy.get('[data-testid="research-mode-switch"]').should('have.attr', 'aria-checked', 'true')
- cy.contains('button', 'Next').click()
- checkToast('No Prompt or Document Provided')
+ checkToast('Please provide a prompt for your presentation')
})
})
describe('Generation Flow', () => {
- it('should proceed to theme page with prompt-only configuration', () => {
+ it('should proceed to outline page with prompt configuration', () => {
// Enter prompt
cy.get('[data-testid="prompt-input"]').type('Create a presentation about AI')
- // Click generate
- cy.contains('button', 'Next').click()
-
- // Wait for API calls with longer timeout
- cy.wait('@getQuestions', { timeout: 10000 })
- cy.wait('@generateTitles', { timeout: 10000 })
-
- // Verify navigation to theme page
- cy.get('@router.push').should('be.calledWith', '/theme')
- })
-
- it('should proceed to documents-preview with research mode', () => {
- // Enable research mode
- cy.get('[data-testid="research-mode-switch"]').click({ force: true })
-
- // Enter prompt
- cy.get('[data-testid="prompt-input"]').type('Research about AI technology')
-
- // Intercept research report generation
- cy.intercept('POST', '**/ppt/report/generate', {
+ // Intercept presentation creation API call
+ cy.intercept('POST', '**/ppt/presentation/create', {
statusCode: 200,
- body: { content: 'Research report content' }
- }).as('researchReport')
+ body: { id: 'test-presentation-id' }
+ }).as('createPresentation')
// Click generate
cy.contains('button', 'Next').click()
- // Wait for research API call
- cy.wait('@researchReport', { timeout: 10000 })
+ // Wait for API call with longer timeout
+ cy.wait('@createPresentation', { timeout: 10000 })
- // Verify navigation to documents-preview page
- cy.get('@router.push').should('be.calledWith', '/documents-preview')
- })
-
- it('should proceed to documents-preview with uploaded document', () => {
- // Upload a document
- cy.fixture('example.txt').as('testFile')
- cy.get('[data-testid="file-upload-input"]').selectFile('@testFile', { force: true })
-
- // Enter prompt
- cy.get('[data-testid="prompt-input"]').type('Analyze this document')
-
- // Intercept document upload and decomposition
- cy.intercept('POST', '**/ppt/files/upload', {
- statusCode: 200,
- body: {
- documents: ['doc1'],
- images: []
- }
- }).as('uploadDoc')
-
- cy.intercept('POST', '**/ppt/files/decompose', {
- statusCode: 200,
- body: {
- documents: { doc1: 'content' },
- images: {},
- charts: {},
- tables: {}
- }
- }).as('decomposeDoc')
-
- // Click generate
- cy.contains('button', 'Next').click()
-
- // Wait for upload and decompose API calls
- cy.wait('@uploadDoc', { timeout: 10000 })
- cy.wait('@decomposeDoc', { timeout: 10000 })
-
- // Verify navigation to documents-preview page
- cy.get('@router.push').should('be.calledWith', '/documents-preview')
+ // Verify navigation to outline page
+ cy.get('@router.push').should('be.calledWith', '/outline')
})
})
@@ -294,4 +161,4 @@ describe('', () => {
checkToast('Failed to generate presentation')
})
})
-})
\ No newline at end of file
+})
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
index e47f29cb..66c71dd1 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
@@ -1,10 +1,9 @@
/**
* UploadPage Component
*
- * This component handles the presentation generation upload process, allowing users to:
+ * This component handles the presentation generation process, allowing users to:
* - Configure presentation settings (slides, language)
- * - Input prompts
- * - Upload supporting documents
+ * - Input prompts for generating presentations
*
* @component
*/
@@ -17,7 +16,6 @@ import { clearOutlines, setPresentationId } from "@/store/slices/presentationGen
import { ConfigurationSelects } from "./ConfigurationSelects";
import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig } from "../type";
-import SupportingDoc from "./SupportingDoc";
import { Button } from "@/components/ui/button";
import { ChevronRight } from "lucide-react";
import { toast } from "sonner";
@@ -42,7 +40,6 @@ const UploadPage = () => {
const dispatch = useDispatch();
// State management
- const [files, setFiles] = useState([]);
const [config, setConfig] = useState({
slides: "8",
language: LanguageType.English,
@@ -67,7 +64,7 @@ const UploadPage = () => {
};
/**
- * Validates the current configuration and files
+ * Validates the current configuration
* @returns boolean indicating if the configuration is valid
*/
const validateConfiguration = (): boolean => {
@@ -76,8 +73,8 @@ const UploadPage = () => {
return false;
}
- if (!config.prompt.trim() && files.length === 0) {
- toast.error("No Prompt or Document Provided");
+ if (!config.prompt.trim()) {
+ toast.error("Please provide a prompt for your presentation");
return false;
}
return true;
@@ -90,58 +87,16 @@ const UploadPage = () => {
if (!validateConfiguration()) return;
try {
- const hasUploadedAssets = files.length > 0;
-
- if (hasUploadedAssets) {
- await handleDocumentProcessing();
- } else {
- await handleDirectPresentationGeneration();
- }
+ await handlePresentationGeneration();
} catch (error) {
handleGenerationError(error);
}
};
/**
- * Handles document processing
+ * Handles presentation generation
*/
- const handleDocumentProcessing = async () => {
- setLoadingState({
- isLoading: true,
- message: "Processing documents...",
- showProgress: true,
- duration: 90,
- extra_info: files.length > 0 ? "It might take a few minutes for large documents." : "",
- });
-
- let documents = [];
-
- if (files.length > 0) {
- trackEvent(MixpanelEvent.Upload_Upload_Documents_API_Call);
- const uploadResponse = await PresentationGenerationApi.uploadDoc(files);
- documents = uploadResponse;
- }
-
- const promises: Promise[] = [];
-
- if (documents.length > 0) {
- trackEvent(MixpanelEvent.Upload_Decompose_Documents_API_Call);
- promises.push(PresentationGenerationApi.decomposeDocuments(documents));
- }
- const responses = await Promise.all(promises);
- dispatch(setPptGenUploadState({
- config,
- files: responses,
- }));
- dispatch(clearOutlines())
- trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/documents-preview" });
- router.push("/documents-preview");
- };
-
- /**
- * Handles direct presentation generation without documents
- */
- const handleDirectPresentationGeneration = async () => {
+ const handlePresentationGeneration = async () => {
setLoadingState({
isLoading: true,
message: "Generating outlines...",
@@ -149,12 +104,11 @@ const UploadPage = () => {
duration: 30,
});
- // Use the first available layout group for direct generation
+ // Generate presentation based on prompt
trackEvent(MixpanelEvent.Upload_Create_Presentation_API_Call);
const createResponse = await PresentationGenerationApi.createPresentation({
prompt: config?.prompt ?? "",
n_slides: config?.slides ? parseInt(config.slides) : null,
- file_paths: [],
language: config?.language ?? "",
});
@@ -204,11 +158,6 @@ const UploadPage = () => {
data-testid="prompt-input"
/>
-
{
- return (
-
- )
-}
+ return ;
+};
-export default page
\ No newline at end of file
+export default page;
diff --git a/servers/nextjs/components/ui/radio-group.tsx b/servers/nextjs/components/ui/radio-group.tsx
index 61686b83..92c3adb3 100644
--- a/servers/nextjs/components/ui/radio-group.tsx
+++ b/servers/nextjs/components/ui/radio-group.tsx
@@ -20,6 +20,7 @@ const RadioGroup = React.forwardRef<
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
+
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
diff --git a/servers/nextjs/lib/auth.ts b/servers/nextjs/lib/auth.ts
index c610f080..0cc8a560 100644
--- a/servers/nextjs/lib/auth.ts
+++ b/servers/nextjs/lib/auth.ts
@@ -3,7 +3,6 @@ import {
LoginRequest,
OrganizationRegisterRequest,
} from "@/app/(presentation-generator)/services/api/organizations";
-import { cookies } from "next/headers";
interface JwtPayload {
sub: string; // user_id
@@ -11,6 +10,14 @@ interface JwtPayload {
[key: string]: any;
}
+interface User {
+ id: string;
+ full_name: string;
+ email: string;
+ is_admin: boolean;
+ organisation_id: string;
+}
+
function decodeToken(token: string): JwtPayload | null {
try {
// Split the token and get the payload part
@@ -27,7 +34,19 @@ function decodeToken(token: string): JwtPayload | null {
export async function login(credentials: LoginRequest) {
try {
const response = await organizationApi.login(credentials);
- setAuthToken(response.access_token);
+ console.log("response",response);
+ setAuthToken(response.access_token,response?.user?.is_admin ? "admin" : "user");
+
+ // Get user info to determine role
+ try {
+ const userInfo = await organizationApi.getMe();
+ if (userInfo && userInfo.is_admin) {
+ localStorage.setItem("user_role", userInfo.is_admin);
+ }
+ } catch (error) {
+ console.error("Error fetching user role:", error);
+ }
+
// Log user_id from token
const decoded = decodeToken(response.access_token);
if (decoded) {
@@ -43,7 +62,7 @@ export async function signup(credentials: OrganizationRegisterRequest) {
try {
const response = await organizationApi.register(credentials);
if (response.access_token) {
- setAuthToken(response.access_token);
+ setAuthToken(response.access_token,response.user);
// Log user_id from token
const decoded = decodeToken(response.access_token);
if (decoded) {
@@ -56,8 +75,13 @@ export async function signup(credentials: OrganizationRegisterRequest) {
}
}
-export function setAuthToken(token: string): void {
+
+
+export function setAuthToken(token: string, is_admin: string): void {
localStorage.setItem("auth_token", token);
+ if (is_admin) {
+ localStorage.setItem("user_role", is_admin ? "admin" : "user");
+ }
document.cookie = `auth_token=${token}; path=/; max-age=2592000`; // 30 days
// Log user_id when token is set
const decoded = decodeToken(token);
@@ -88,6 +112,7 @@ export function removeAuthToken(): void {
}
}
localStorage.removeItem("auth_token");
+ localStorage.removeItem("user_role");
document.cookie =
"auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
}
@@ -96,6 +121,14 @@ export function isAuthenticated(): boolean {
return !!getAuthToken();
}
+export function getUserRole(): string | null {
+ return localStorage.getItem("user_role");
+}
+
+export function isAdmin(): boolean {
+ return getUserRole() === "admin";
+}
+
// Helper to handle 401 responses
export function handleAuthError(error: any): never {
if (error.status === 401) {
diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs
index 508fc7de..2e6fc069 100644
--- a/servers/nextjs/next.config.mjs
+++ b/servers/nextjs/next.config.mjs
@@ -2,56 +2,53 @@ const nextConfig = {
reactStrictMode: false,
distDir: ".next-build",
- // Rewrites for development - proxy font requests to FastAPI backend
+ // Explicitly set the host for Server Actions
+ serverRuntimeConfig: {
+ host: 'localhost',
+ port: 5001,
+ },
+
+ // Rewrites for development
async rewrites() {
return [
{
source: "/app_data/fonts/:path*",
destination: "http://localhost:5001/app_data/fonts/:path*",
},
+ {
+ source: "/api/v1/documents/:path*",
+ destination: "http://localhost:8000/api/v1/documents/:path*",
+ },
];
},
- images: {
- remotePatterns: [
- {
- protocol: "https",
- hostname: "pub-7c765f3726084c52bcd5d180d51f1255.r2.dev",
- },
- {
- protocol: "https",
- hostname: "pptgen-public.ap-south-1.amazonaws.com",
- },
- {
- protocol: "https",
- hostname: "pptgen-public.s3.ap-south-1.amazonaws.com",
- },
- {
- protocol: "https",
- hostname: "img.icons8.com",
- },
- {
- protocol: "https",
- hostname: "present-for-me.s3.amazonaws.com",
- },
- {
- protocol: "https",
- hostname: "yefhrkuqbjcblofdcpnr.supabase.co",
- },
- {
- protocol: "https",
- hostname: "images.unsplash.com",
- },
- {
- protocol: "https",
- hostname: "picsum.photos",
- },
+ // Configure headers for Server Actions
+ async headers() {
+ return [
{
- protocol: "https",
- hostname: "unsplash.com",
+ source: '/api/:path*',
+ headers: [
+ { key: 'Access-Control-Allow-Origin', value: 'http://localhost:5001' },
+ { key: 'Access-Control-Allow-Methods', value: 'GET,HEAD,PUT,PATCH,POST,DELETE' },
+ { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' },
+ ],
},
+ ];
+ },
+
+ // Add experimental server actions configuration
+ experimental: {
+ serverActions: {
+ allowedOrigins: ['localhost:5001']
+ }
+ },
+
+ // Existing image remote patterns
+ images: {
+ remotePatterns: [
+ // ... (keep existing remote patterns)
],
},
};
-export default nextConfig;
+export default nextConfig;
\ No newline at end of file
diff --git a/servers/nextjs/package-lock.json b/servers/nextjs/package-lock.json
index 616ea408..7a1a3465 100644
--- a/servers/nextjs/package-lock.json
+++ b/servers/nextjs/package-lock.json
@@ -37,6 +37,8 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
+ "@types/axios": "^0.14.4",
+ "axios": "^1.11.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -56,7 +58,7 @@
"react-simple-code-editor": "^0.14.1",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
- "sonner": "^2.0.6",
+ "sonner": "^2.0.7",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10",
@@ -3426,6 +3428,15 @@
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
+ "node_modules/@types/axios": {
+ "version": "0.14.4",
+ "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz",
+ "integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==",
+ "deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.",
+ "dependencies": {
+ "axios": "*"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4101,7 +4112,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -4131,6 +4141,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@@ -4370,7 +4395,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4758,7 +4782,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -5528,7 +5551,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -5590,7 +5612,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -5688,7 +5709,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5698,7 +5718,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5708,7 +5727,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -5721,7 +5739,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -6034,6 +6051,25 @@
"node": ">=8"
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -6076,7 +6112,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
- "dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -6141,7 +6176,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -6175,7 +6209,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -6298,7 +6331,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6333,7 +6365,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6346,7 +6377,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -7118,7 +7148,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7203,7 +7232,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7213,7 +7241,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -8950,7 +8977,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
- "license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json
index 1080e0a4..e29f5e26 100644
--- a/servers/nextjs/package.json
+++ b/servers/nextjs/package.json
@@ -39,6 +39,8 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
+ "@types/axios": "^0.14.4",
+ "axios": "^1.11.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -58,7 +60,7 @@
"react-simple-code-editor": "^0.14.1",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
- "sonner": "^2.0.6",
+ "sonner": "^2.0.7",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10",
diff --git a/servers/nextjs/tsconfig.json b/servers/nextjs/tsconfig.json
index 0fed898e..22b78043 100644
--- a/servers/nextjs/tsconfig.json
+++ b/servers/nextjs/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "esnext",
+ "target": "es5",
"lib": [
"dom",
"dom.iterable",
@@ -13,7 +13,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
- "moduleResolution": "bundler",
+ "moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@@ -26,14 +26,23 @@
"paths": {
"@/*": [
"./*"
+ ],
+ "@/components/*": [
+ "./components/*"
+ ],
+ "@/lib/*": [
+ "./lib/*"
+ ],
+ "@/services/*": [
+ "./services/*"
]
}
},
"include": [
"next-env.d.ts",
+ ".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
- ".next/types/**/*.ts",
".next-build/types/**/*.ts"
],
"exclude": [