feat: implement PDF/PPTX export functionality with dedicated routes and components

- Added new middleware to handle session authentication for presentation retrieval.
- Introduced new layout and page components for PDF export, ensuring no loading state is shown during headless rendering.
- Enhanced ConfigurationInitializer to manage loading state based on route.
- Updated DashboardApi to include credentials in requests for presentation data.
- Refactored hasValidLLMConfig function for cleaner validation logic.
This commit is contained in:
sudipnext 2026-04-24 10:30:51 +05:45
parent 11904c6cb0
commit 2e7e31db8c
7 changed files with 39 additions and 19 deletions

View file

@ -1,3 +1,5 @@
import re
from fastapi import Request
from starlette.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
@ -23,6 +25,12 @@ class SessionAuthMiddleware(BaseHTTPMiddleware):
_EXEMPT_PREFIXES = (
"/api/v1/auth/",
)
# PPTX/PDF export loads /pdf-maker in a headless browser with no session cookie; it
# only needs a single-deck read by id. (UUID is not a secret; this matches prior behavior
# when auth middleware did not protect these routes during export.)
_PRESENTATION_GET_BY_ID = re.compile(
r"^/api/v1/ppt/presentation/[0-9a-fA-F-]{36}/?$"
)
_PROTECTED_NON_API_PATHS = {
"/docs",
"/openapi.json",
@ -32,6 +40,11 @@ class SessionAuthMiddleware(BaseHTTPMiddleware):
def _is_exempt(self, path: str) -> bool:
return any(path.startswith(prefix) for prefix in self._EXEMPT_PREFIXES)
def _is_presentation_get_by_id(self, request: Request, path: str) -> bool:
if request.method != "GET":
return False
return bool(self._PRESENTATION_GET_BY_ID.match(path))
def _requires_auth(self, path: str) -> bool:
if path.startswith("/api/"):
return True
@ -46,6 +59,7 @@ class SessionAuthMiddleware(BaseHTTPMiddleware):
request.method == "OPTIONS"
or not self._requires_auth(path)
or self._is_exempt(path)
or self._is_presentation_get_by_id(request, path)
):
return await call_next(request)

View file

@ -0,0 +1,10 @@
import React from "react";
/**
* Do not wrap with ConfigurationInitializer: it always mounts with isLoading=true
* and only clears after useEffect, so headless PDF/PPTX export captures the
* "Initializing Application" screen. Export only needs the slide renderer; no LLM check.
*/
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View file

@ -2,7 +2,7 @@
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import "../utils/prism-languages";
import "@/app/(presentation-generator)/utils/prism-languages";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@ -10,13 +10,12 @@ import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { AlertCircle } from "lucide-react";
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { DashboardApi } from "../services/api/dashboard";
import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard";
import { setupImageUrlConverter } from "@/utils/image-url-converter";
import { V1ContentRender } from "../components/V1ContentRender";
import { useFontLoader } from "../hooks/useFontLoad";
import { Theme } from "../services/api/types";
import { V1ContentRender } from "@/app/(presentation-generator)/components/V1ContentRender";
import { useFontLoader } from "@/app/(presentation-generator)/hooks/useFontLoad";
import { Theme } from "@/app/(presentation-generator)/services/api/types";

View file

@ -53,6 +53,7 @@ export class DashboardApi {
getApiUrl(`/api/v1/ppt/presentation/${id}`),
{
method: "GET",
credentials: "include",
}
);

View file

@ -12,9 +12,11 @@ import { getApiUrl } from '@/utils/api';
export function ConfigurationInitializer({ children }: { children: React.ReactNode }) {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const route = usePathname();
const [isLoading, setIsLoading] = useState(
() => !route?.startsWith("/pdf-maker")
);
const router = useRouter();
// Fetch user config state
useEffect(() => {
@ -31,13 +33,13 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
}
const fetchUserConfigState = async () => {
setIsLoading(true);
if (route.startsWith('/pdf-maker')) {
if (route.startsWith("/pdf-maker")) {
setIsLoading(false);
return;
}
setIsLoading(true);
let canChangeKeys = false;
try {
const res = await fetch('/api/can-change-keys');
@ -64,7 +66,6 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
dispatch(setLLMConfig(llmConfig));
const isValid = hasValidLLMConfig(llmConfig);
console.log('isValid', isValid);
if (route.startsWith('/pdf-maker')) {
setIsLoading(false);
return;

View file

@ -128,10 +128,5 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
store.dispatch(setLLMConfig(llmConfig));
};
export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
console.log('llmConfig', llmConfig);
const validationError = getLLMConfigValidationError(llmConfig);
console.log('validationError', validationError);
return validationError === null;
}
export const hasValidLLMConfig = (llmConfig: LLMConfig) =>
getLLMConfigValidationError(llmConfig) === null;