From 3d06644914971021b1193176fa19745ed261a91e Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sat, 18 Apr 2026 20:56:37 +0545 Subject: [PATCH] feat: update placeholder image references and improve asset handling - Replaced all instances of the placeholder image path from "/static/images/placeholder.jpg" to "/static/images/replaceable_template_image.png". - Added a new Nginx location block for serving app data with a long cache expiration. - Enhanced the image generation service to return the new template image when generation fails. - Updated various services and endpoints to ensure consistent handling of asset paths, including resolving backend asset URLs. - Removed Electron-specific checks from several components to streamline API calls and improve compatibility with web deployments. - Improved error handling and logging in the PDF export process. - Adjusted Next.js configuration for API routing to ensure proper asset serving in Docker environments. --- nginx.conf | 6 ++ .../api/v1/ppt/endpoints/pdf_slides.py | 2 +- .../api/v1/ppt/endpoints/pptx_slides.py | 2 +- .../fastapi/services/export_task_service.py | 3 - .../services/image_generation_service.py | 6 +- servers/fastapi/services/liteparse_service.py | 68 ++++++------ .../services/pptx_presentation_creator.py | 4 +- .../images/replaceable_template_image.png | Bin 0 -> 18572 bytes servers/fastapi/templates/preview.py | 2 +- .../fastapi/tests/test_image_generation.py | 6 +- servers/fastapi/tests/test_slide_to_html.py | 4 +- .../fastapi/utils/asset_directory_utils.py | 2 +- servers/fastapi/utils/oauth/openai_codex.py | 2 +- servers/fastapi/utils/ocr_language.py | 2 +- servers/fastapi/utils/path_helpers.py | 2 +- servers/fastapi/utils/process_slides.py | 2 +- .../(dashboard)/settings/PrivacySettings.tsx | 27 ++--- .../outline/components/CustomTemplateCard.tsx | 4 +- .../outline/components/TemplateSelection.tsx | 30 +----- .../pdf-maker/PdfMakerPage.tsx | 32 ++++-- .../components/PresentationHeader.tsx | 24 ----- .../components/TemplatePreviewClient.tsx | 7 ++ .../api/presentation_to_pptx_model/route.ts | 41 +++++++- servers/nextjs/app/hooks/compileLayout.ts | 54 +++++++++- .../nextjs/app/hooks/useCustomTemplates.ts | 22 ++-- .../components/OnBoarding/FinalStep.tsx | 27 ++--- servers/nextjs/lib/run-bundled-pdf-export.ts | 1 - servers/nextjs/next.config.mjs | 2 +- servers/nextjs/types/global.d.ts | 22 ---- servers/nextjs/utils/api.ts | 98 ++++++++++++++---- servers/nextjs/utils/mixpanel.ts | 12 +-- 31 files changed, 295 insertions(+), 221 deletions(-) create mode 100644 servers/fastapi/static/images/replaceable_template_image.png diff --git a/nginx.conf b/nginx.conf index b5ae9172..a61a9c9b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -96,5 +96,11 @@ http { expires 1y; add_header Cache-Control "public, immutable"; } + + location /app_data/pptx-to-html/ { + alias /app_data/pptx-to-html/; + expires 1y; + add_header Cache-Control "public, immutable"; + } } } \ No newline at end of file diff --git a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py index 606cb12f..4906df87 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py +++ b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py @@ -99,7 +99,7 @@ async def process_pdf_slides( ) else: # Fallback if screenshot generation failed or file is empty placeholder - screenshot_url = "/static/images/placeholder.jpg" + screenshot_url = "/static/images/replaceable_template_image.png" slides_data.append( PdfSlideData(slide_number=i, screenshot_url=screenshot_url) diff --git a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py index b4c4acae..6ef33574 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py +++ b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py @@ -378,7 +378,7 @@ async def process_pptx_slides( ) else: # Fallback if screenshot generation failed or file is empty placeholder - screenshot_url = "/static/images/placeholder.jpg" + screenshot_url = "/static/images/replaceable_template_image.png" # Compute normalized fonts for this slide raw_slide_fonts = extract_fonts_from_oxml(xml_content) diff --git a/servers/fastapi/services/export_task_service.py b/servers/fastapi/services/export_task_service.py index 8e9c8ac2..ca31c588 100644 --- a/servers/fastapi/services/export_task_service.py +++ b/servers/fastapi/services/export_task_service.py @@ -90,9 +90,6 @@ class ExportTaskService: def _build_node_env(self) -> Mapping[str, str]: env = os.environ.copy() - binary_name = os.path.basename(self.node_binary).lower() - if binary_name not in {"node", "node.exe"}: - env.setdefault("ELECTRON_RUN_AS_NODE", "1") app_data_directory = get_app_data_directory_env() if not app_data_directory: diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index 6b47d426..8d0ea9cb 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -69,11 +69,11 @@ class ImageGenerationService: """ if self.is_image_generation_disabled: print("Image generation is disabled. Using placeholder image.") - return "/static/images/placeholder.jpg" + return "/static/images/replaceable_template_image.png" if not self.image_gen_func: print("No image generation function found. Using placeholder image.") - return "/static/images/placeholder.jpg" + return "/static/images/replaceable_template_image.png" image_prompt = prompt.get_image_prompt( with_theme=not self.is_stock_provider_selected() @@ -107,7 +107,7 @@ class ImageGenerationService: except Exception as e: print(f"Error generating image: {e}") - return "/static/images/placeholder.jpg" + return "/static/images/replaceable_template_image.png" async def generate_image_openai( self, prompt: str, output_directory: str, model: str, quality: str diff --git a/servers/fastapi/services/liteparse_service.py b/servers/fastapi/services/liteparse_service.py index dca0835d..392a94c5 100644 --- a/servers/fastapi/services/liteparse_service.py +++ b/servers/fastapi/services/liteparse_service.py @@ -45,15 +45,8 @@ class LiteParseService: self._npm_project_root = self._resolve_npm_project_root() def _build_node_env(self) -> Dict[str, str]: - """Build environment for Node subprocesses. - - When the configured runtime binary is not the canonical `node` executable - (for example Electron's app binary), force Node-compatible mode. - """ + """Build environment for Node subprocesses.""" env = os.environ.copy() - binary_name = os.path.basename(self.node_binary).lower() - if binary_name not in {"node", "node.exe"}: - env.setdefault("ELECTRON_RUN_AS_NODE", "1") # LiteParse checks ImageMagick availability with `which magick`. # On macOS app launches, PATH often excludes Homebrew bins, even when @@ -97,36 +90,51 @@ class LiteParseService: return env def _resolve_npm_project_root(self) -> str: - """Directory whose node_modules contains @llamaindex/liteparse (runner dir or Electron app root).""" - local_nm = os.path.join( - self.runner_dir, "node_modules", "@llamaindex", "liteparse" - ) - if os.path.isdir(local_nm): - return self.runner_dir - electron_nm = os.path.abspath( - os.path.join(self.runner_dir, "..", "..", "node_modules", "@llamaindex", "liteparse") - ) - if os.path.isdir(electron_nm): - return os.path.abspath(os.path.join(self.runner_dir, "..", "..")) - return os.path.abspath(os.path.join(self.runner_dir, "..", "..")) + """Directory whose node_modules contains @llamaindex/liteparse.""" + candidates = [ + self.runner_dir, + os.path.abspath(os.path.join(self.runner_dir, "..")), + os.path.abspath(os.path.join(os.getcwd(), "..", "..", "document-extraction-liteparse")), + os.path.abspath(os.path.join(os.getcwd(), "..", "..")), + "/app/document-extraction-liteparse", + "/app", + ] + + fallback = candidates[0] + for candidate in candidates: + if os.path.isdir(candidate): + fallback = candidate + local_nm = os.path.join(candidate, "node_modules", "@llamaindex", "liteparse") + if os.path.isdir(local_nm): + return candidate + + return fallback @staticmethod def _resolve_runner_path() -> str: cwd = os.path.abspath(".") + service_dir = os.path.dirname(__file__) candidates = [ - # electron/servers/fastapi → electron/resources/... - os.path.abspath( - os.path.join( - cwd, "..", "..", "resources", "document-extraction", "liteparse_runner.mjs" - ) - ), - # servers/fastapi (repo root layout) → electron/resources/... + # Dedicated Docker runtime path + "/app/document-extraction-liteparse/liteparse_runner.mjs", + # servers/fastapi (repo root layout) → resources/... os.path.abspath( os.path.join( cwd, "..", "..", - "electron", + "resources", + "document-extraction", + "liteparse_runner.mjs", + ) + ), + # services/liteparse_service.py → resources/... + os.path.abspath( + os.path.join( + service_dir, + "..", + "..", + "..", "resources", "document-extraction", "liteparse_runner.mjs", @@ -138,8 +146,6 @@ class LiteParseService: cwd, "..", "..", "app", "resources", "document-extraction", "liteparse_runner.mjs" ) ), - # Docker / explicit layout - "/app/document-extraction-liteparse/liteparse_runner.mjs", ] for path in candidates: if os.path.isfile(path): @@ -169,7 +175,7 @@ class LiteParseService: if not os.path.isdir(liteparse_dir): return ( False, - f"LiteParse npm package missing at {liteparse_dir}. Run npm install in the Electron app directory.", + f"LiteParse npm package missing at {liteparse_dir}. Install @llamaindex/liteparse in the runtime project root.", ) # @llamaindex/liteparse is ESM-only; require.resolve() fails. Use dynamic import. diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index d3e51a7f..e0f179da 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -220,7 +220,7 @@ class PptxPresentationCreator: each_shape.picture.path = os.path.join("/app_data", relative_path) each_shape.picture.is_network = False return - # Resolve HTTP URLs that contain absolute filesystem paths (Mac/Electron) + # Resolve HTTP URLs that contain absolute filesystem paths. local_path = resolve_image_path_to_filesystem(image_path) if local_path: each_shape.picture.path = local_path @@ -315,7 +315,7 @@ class PptxPresentationCreator: def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path - # Resolve /app_data/... to actual filesystem path (Electron) + # Resolve /app_data/... to actual filesystem path. if image_path.startswith("/app_data/"): app_data_dir = get_app_data_directory_env() if app_data_dir: diff --git a/servers/fastapi/static/images/replaceable_template_image.png b/servers/fastapi/static/images/replaceable_template_image.png new file mode 100644 index 0000000000000000000000000000000000000000..838d7f9d2996f6ad8b0c7a73436779cd68596610 GIT binary patch literal 18572 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 literal 0 HcmV?d00001 diff --git a/servers/fastapi/templates/preview.py b/servers/fastapi/templates/preview.py index 0a045319..06ae0e90 100644 --- a/servers/fastapi/templates/preview.py +++ b/servers/fastapi/templates/preview.py @@ -367,7 +367,7 @@ async def store_slide_images( await asyncio.to_thread(_copy_file, screenshot_path, destination_path) slide_image_urls.append(f"/app_data/images/{session_id}/{file_name}") else: - slide_image_urls.append("/static/images/placeholder.jpg") + slide_image_urls.append("/static/images/replaceable_template_image.png") return slide_image_urls diff --git a/servers/fastapi/tests/test_image_generation.py b/servers/fastapi/tests/test_image_generation.py index 56a602b4..1c82f8d8 100644 --- a/servers/fastapi/tests/test_image_generation.py +++ b/servers/fastapi/tests/test_image_generation.py @@ -195,7 +195,7 @@ class TestImageGenerationService: result = await service.generate_image(sample_image_prompt) # Should return placeholder - assert result == "/static/images/placeholder.jpg" + assert result == "/static/images/replaceable_template_image.png" asyncio.run(run_test()) @@ -221,7 +221,7 @@ class TestImageGenerationService: result = await service.generate_image(sample_image_prompt) - assert result == "/static/images/placeholder.jpg" + assert result == "/static/images/replaceable_template_image.png" asyncio.run(run_test()) @@ -367,7 +367,7 @@ class TestImageGenerationEndpoint: with patch('api.v1.ppt.endpoints.images.get_images_directory', return_value=mock_images_directory): with patch('api.v1.ppt.endpoints.images.ImageGenerationService') as mock_service_class: mock_service_instance = Mock() - mock_service_instance.generate_image = AsyncMock(return_value="/static/images/placeholder.jpg") + mock_service_instance.generate_image = AsyncMock(return_value="/static/images/replaceable_template_image.png") mock_service_class.return_value = mock_service_instance response = client.get(f"/images/generate?prompt={test_prompt}") diff --git a/servers/fastapi/tests/test_slide_to_html.py b/servers/fastapi/tests/test_slide_to_html.py index 63f9fe92..6aefdae3 100644 --- a/servers/fastapi/tests/test_slide_to_html.py +++ b/servers/fastapi/tests/test_slide_to_html.py @@ -42,7 +42,7 @@ def test_slide_to_html_endpoint(): # Use a placeholder image path (since we can't easily test with real files) test_data = { - "image": "/static/images/placeholder.jpg", + "image": "/static/images/replaceable_template_image.png", "xml": test_xml } @@ -92,7 +92,7 @@ def test_slide_to_html_missing_xml(): """Test the endpoint with missing XML data.""" test_data = { - "image": "/static/images/placeholder.jpg" + "image": "/static/images/replaceable_template_image.png" # No XML data provided } diff --git a/servers/fastapi/utils/asset_directory_utils.py b/servers/fastapi/utils/asset_directory_utils.py index 5f8f13f8..28eca9ee 100644 --- a/servers/fastapi/utils/asset_directory_utils.py +++ b/servers/fastapi/utils/asset_directory_utils.py @@ -12,7 +12,7 @@ def resolve_app_path_to_filesystem(path_or_url: str) -> Optional[str]: Handles: - Path strings: /app_data/images/..., /static/..., absolute paths, relative - file:// URLs returned by export runtimes - - HTTP URLs whose path component is an absolute filesystem path (Mac/Electron): + - HTTP URLs whose path component is an absolute filesystem path: When img src is /Users/.../images/xxx.png, browser resolves to http://origin/Users/.../images/xxx.png. Next.js returns 404 for these. diff --git a/servers/fastapi/utils/oauth/openai_codex.py b/servers/fastapi/utils/oauth/openai_codex.py index c94b75eb..47bee191 100644 --- a/servers/fastapi/utils/oauth/openai_codex.py +++ b/servers/fastapi/utils/oauth/openai_codex.py @@ -402,7 +402,7 @@ class _CallbackHandler(BaseHTTPRequestHandler): self.wfile.write(b"Missing authorization code") return - # In the desktop/Electron app context the redirect URI is a localhost-only + # In local callback flows the redirect URI is localhost-only. # callback, so strict CSRF protection via state comparison is less critical. # We've seen intermittent state mismatches in the field (likely from # overlapping auth attempts or stale callback servers), so we treat a diff --git a/servers/fastapi/utils/ocr_language.py b/servers/fastapi/utils/ocr_language.py index aa988f27..bb4a4009 100644 --- a/servers/fastapi/utils/ocr_language.py +++ b/servers/fastapi/utils/ocr_language.py @@ -3,7 +3,7 @@ Map presentation UI language strings (LanguageType enum values from Next.js) to Tesseract / LiteParse OCR language codes (ISO 639-3 where applicable). Keep keys in sync with: -electron/servers/nextjs/app/(presentation-generator)/upload/type.ts → LanguageType +servers/nextjs/app/(presentation-generator)/upload/type.ts → LanguageType """ from __future__ import annotations diff --git a/servers/fastapi/utils/path_helpers.py b/servers/fastapi/utils/path_helpers.py index 424bceac..cf4548a7 100644 --- a/servers/fastapi/utils/path_helpers.py +++ b/servers/fastapi/utils/path_helpers.py @@ -1,7 +1,7 @@ """Paths relative to the FastAPI process working directory (Docker / local dev). The API is always started with cwd set to the `servers/fastapi` package root -(see start.js). No Electron, PyInstaller, or OS-specific layout handling. +(see start.js), without OS-specific layout handling. """ from __future__ import annotations diff --git a/servers/fastapi/utils/process_slides.py b/servers/fastapi/utils/process_slides.py index 616d4efb..9048ff68 100644 --- a/servers/fastapi/utils/process_slides.py +++ b/servers/fastapi/utils/process_slides.py @@ -200,7 +200,7 @@ def process_slide_add_placeholder_assets(slide: SlideModel): for image_path in image_paths: image_dict = get_dict_at_path(slide.content, image_path) # Use FastAPI static path for placeholder image - image_dict["__image_url__"] = "/static/images/placeholder.jpg" + image_dict["__image_url__"] = "/static/images/replaceable_template_image.png" set_dict_at_path(slide.content, image_path, image_dict) for icon_path in icon_paths: diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/PrivacySettings.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/PrivacySettings.tsx index c16e5f84..dfed7701 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/PrivacySettings.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/PrivacySettings.tsx @@ -11,14 +11,9 @@ const PrivacySettings = () => { useEffect(() => { async function fetchStatus() { try { - if (window.electron?.telemetryStatus) { - const data = await window.electron.telemetryStatus(); - setTrackingEnabled(data.telemetryEnabled); - } else { - const res = await fetch("/api/telemetry-status"); - const data = await res.json(); - setTrackingEnabled(data.telemetryEnabled); - } + const res = await fetch("/api/telemetry-status"); + const data = await res.json(); + setTrackingEnabled(data.telemetryEnabled); } catch { setTrackingEnabled(true); } @@ -32,18 +27,12 @@ const PrivacySettings = () => { setTelemetryEnabled(enabled); setSaving(true); try { - if (window.electron?.setUserConfig) { - await window.electron.setUserConfig({ + await fetch("/api/user-config", { + method: "POST", + body: JSON.stringify({ DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : "true", - } as any); - } else { - await fetch("/api/user-config", { - method: "POST", - body: JSON.stringify({ - DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : "true", - }), - }); - } + }), + }); } catch { setTrackingEnabled(prev); setTelemetryEnabled(prev ?? true); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx index b052d2ec..ffa4368e 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx @@ -15,7 +15,7 @@ export const CustomTemplateCard = memo(function CustomTemplateCard({ selectedTemplate, }: { template: CustomTemplates; - onSelectTemplate: (template: CustomTemplates) => void; + onSelectTemplate: (template: string) => void; selectedTemplate: string | null; }) { const { previewLayouts, loading } = useCustomTemplatePreview(template.id); @@ -29,7 +29,7 @@ export const CustomTemplateCard = memo(function CustomTemplateCard({ ? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm" : " border-[#E8E9EC]" )} - onClick={() => onSelectTemplate(template)} + onClick={() => onSelectTemplate(template.id)} > diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx index 48710796..4bf94643 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/TemplateSelection.tsx @@ -7,8 +7,6 @@ import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates"; import { Loader2 } from "lucide-react"; -import { usePathname } from "next/navigation"; -import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate"; import { CustomTemplateCard } from "./CustomTemplateCard"; @@ -66,8 +64,6 @@ const TemplateSelection: React.FC = memo(function Templa selectedTemplate, onSelectTemplate, }) { - const pathname = usePathname(); - useEffect(() => { const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' @@ -83,31 +79,13 @@ const TemplateSelection: React.FC = memo(function Templa const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries(); const handleCustomSelect = useCallback( - (template: CustomTemplates) => { - trackEvent(MixpanelEvent.Outline_Template_Selected, { - pathname, - template_type: "custom", - template_id: template.id, - template_name: template.name, - layout_count: template.layoutCount, - }); - onSelectTemplate(template.id); - }, - [onSelectTemplate, pathname] + (template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template), + [onSelectTemplate] ); const handleBuiltInSelect = useCallback( - (template: TemplateLayoutsWithSettings) => { - trackEvent(MixpanelEvent.Outline_Template_Selected, { - pathname, - template_type: "built_in", - template_id: template.id, - template_name: template.name, - layout_count: template.layouts.length, - }); - onSelectTemplate(template); - }, - [onSelectTemplate, pathname] + (template: TemplateLayoutsWithSettings) => onSelectTemplate(template), + [onSelectTemplate] ); const selectedCustomId = useMemo( diff --git a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx index 3575d4e4..cfdf172e 100644 --- a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "@/store/store"; +import "../utils/prism-languages"; import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -10,6 +11,7 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { AlertCircle } from "lucide-react"; import { setPresentationData } from "@/store/slices/presentationGeneration"; import { DashboardApi } from "../services/api/dashboard"; +import { setupImageUrlConverter } from "@/utils/image-url-converter"; import { V1ContentRender } from "../components/V1ContentRender"; @@ -43,6 +45,13 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { } } }, [presentationData]); + + // Ensure /app_data and /static image paths resolve through the backend origin. + useEffect(() => { + const observer = setupImageUrlConverter(); + return () => observer?.disconnect(); + }, []); + // Function to fetch the slides useEffect(() => { fetchUserSlides(); @@ -54,12 +63,18 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { const data = await DashboardApi.getPresentation(presentation_id); dispatch(setPresentationData(data)); setContentLoading(false); - if (data?.theme) { - applyTheme(data.theme); - } + if (data.fonts) { useFontLoader(data.fonts); } + if (data?.theme) { + try { + applyTheme(data.theme); + } catch (themeError) { + // Theme issues should not block export rendering. + console.warn("Theme application skipped for pdf-maker:", themeError); + } + } } catch (error) { setError(true); toast.error("Failed to load presentation"); @@ -73,6 +88,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { if (!element) return; if (!theme || !theme.data) { return; } if (!theme.data.colors['graph_0']) { return; } + if (!theme.data.fonts?.textFont?.name || !theme.data.fonts?.textFont?.url) { return; } const cssVariables = { '--primary-color': theme.data.colors['primary'], '--background-color': theme.data.colors['background'], @@ -137,7 +153,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
{!presentationData || @@ -161,8 +177,12 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { presentationData.slides.length > 0 && presentationData.slides.map((slide: any, index: number) => ( // [data-speaker-note] is used to extract the speaker note from the slide for export to pptx -
- +
))} diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx index 6111dfde..8d253464 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx @@ -150,20 +150,6 @@ const PresentationHeader = ({ return pptx_model; }; - const exportViaIpc = async (format: "pptx" | "pdf"): Promise => { - if (typeof window === 'undefined') return false; - if (!(window as any).electron?.exportPresentation) return false; - const result = await (window as any).electron.exportPresentation( - presentation_id, - presentationData?.title || 'presentation', - format - ); - if (!result?.success) { - throw new Error(result?.message || 'Export failed'); - } - return true; - }; - const handleExportPptx = async () => { if (isStreaming) return; @@ -179,11 +165,6 @@ const PresentationHeader = ({ // Save the presentation data before exporting await PresentationGenerationApi.updatePresentationContent(presentationData); - if (await exportViaIpc("pptx")) { - toast.success("PPTX exported successfully!"); - return; - } - const pptx_model = await get_presentation_pptx_model(presentation_id); if (!pptx_model) { throw new Error("Failed to get presentation PPTX model"); @@ -220,11 +201,6 @@ const PresentationHeader = ({ setIsExporting(true); // Save the presentation data before exporting await PresentationGenerationApi.updatePresentationContent(presentationData); - - if (await exportViaIpc("pdf")) { - toast.success("PDF exported successfully!"); - return; - } const response = await fetch('/api/export-as-pdf', { method: 'POST', body: JSON.stringify({ diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx index 703128c5..f380fd65 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx @@ -12,6 +12,7 @@ import Header from "../../(dashboard)/dashboard/components/Header"; import { toast } from "sonner"; import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates"; import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates"; +import { setupImageUrlConverter } from "@/utils/image-url-converter"; const GroupLayoutPreview = () => { const searchParams = useSearchParams(); @@ -43,6 +44,12 @@ const GroupLayoutPreview = () => { } }, [templateParams]); + // Keep backend-served assets on the active origin in Docker/nginx preview mode. + useEffect(() => { + const observer = setupImageUrlConverter(); + return () => observer?.disconnect(); + }, []); + const handleDeleteCustomTemplate = async () => { if (!customTemplateId) return; diff --git a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts index b1ff1843..7e126b6d 100644 --- a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts +++ b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts @@ -30,6 +30,22 @@ interface GetAllChildElementsAttributesArgs { } export async function GET(request: NextRequest) { + // This route requires server-side Puppeteer execution and is unavailable in static/edge builds. + const isStaticMode = + process.env.IS_STATIC_EXPORT === "true" || + process.env.NEXT_RUNTIME === "edge"; + + if (isStaticMode) { + return NextResponse.json( + { + error: "This API route requires a server runtime and is not available in static export mode", + message: "This functionality is only available in server deployments with Puppeteer support" + }, + { status: 501 } + ); + } + + // Full functionality for development/Docker let browser: Browser | null = null; let page: Page | null = null; @@ -424,11 +440,26 @@ async function getAllChildElementsAttributes({ ); }); - for (const { attributes } of elementsWithRootPosition) { - if (attributes.background && attributes.background.color) { - backgroundColor = attributes.background.color; - break; - } + const rootBackgroundCandidates = elementsWithRootPosition + .filter(({ attributes }) => { + return Boolean(attributes.background && attributes.background.color); + }) + .sort((a, b) => { + const zIndexA = a.attributes.zIndex || 0; + const zIndexB = b.attributes.zIndex || 0; + + // Prefer deeper nodes when z-index is tied so nested full-size backgrounds + // can override outer wrappers. + if (zIndexA === zIndexB) { + return b.depth - a.depth; + } + + // Prefer lower z-index candidates for slide background. + return zIndexA - zIndexB; + }); + + if (rootBackgroundCandidates.length > 0) { + backgroundColor = rootBackgroundCandidates[0].attributes.background?.color; } } diff --git a/servers/nextjs/app/hooks/compileLayout.ts b/servers/nextjs/app/hooks/compileLayout.ts index cf946190..b1110a06 100644 --- a/servers/nextjs/app/hooks/compileLayout.ts +++ b/servers/nextjs/app/hooks/compileLayout.ts @@ -5,6 +5,7 @@ import * as z from "zod"; import * as Recharts from "recharts"; import * as Babel from "@babel/standalone"; import * as d3 from "d3"; +import { resolveBackendAssetUrl } from "@/utils/api"; // import * as d3Cloud from "d3-cloud"; export interface CompiledLayout { @@ -17,14 +18,55 @@ export interface CompiledLayout { schemaJSON: any; } +function isLikelyBackendAssetPath(value: string): boolean { + if (!value) return false; + if (value.startsWith("file://")) return true; + if (value.startsWith("/app_data/") || value.startsWith("/static/")) return true; + if (value.startsWith("app_data/") || value.startsWith("static/")) return true; + return value.includes("/app_data/") || value.includes("/static/"); +} + +function normalizeLayoutAssetUrls(value: T): T { + if (typeof value === "string") { + const trimmedValue = value.trim(); + if (!isLikelyBackendAssetPath(trimmedValue)) { + return value; + } + return resolveBackendAssetUrl(trimmedValue) as T; + } + + if (Array.isArray(value)) { + return value.map((item) => normalizeLayoutAssetUrls(item)) as T; + } + + if (value && typeof value === "object") { + const normalizedEntries = Object.entries(value as Record).map( + ([key, item]) => [key, normalizeLayoutAssetUrls(item)] + ); + return Object.fromEntries(normalizedEntries) as T; + } + + return value; +} + +function normalizeHardcodedBackendUrlsInCode(layoutCode: string): string { + // Keep /app_data and /static paths origin-agnostic so nginx can proxy them. + return layoutCode.replace( + /https?:\/\/(?:127\.0\.0\.1|localhost|0\.0\.0\.0):(?:8000|5000)(?=\/(?:app_data|static)\/)/g, + "" + ); +} + /** * Compiles a layout code string into a usable React component */ export function compileCustomLayout(layoutCode: string): CompiledLayout | null { console.log('compileCustomLayout called'); try { + const normalizedLayoutCode = normalizeHardcodedBackendUrlsInCode(layoutCode); + // Clean up imports that we'll provide ourselves - const cleanCode = layoutCode + const cleanCode = normalizedLayoutCode // Remove React imports .replace(/import\s+React\s*,?\s*\{?[^}]*\}?\s*from\s+['"]react['"];?/g, "") .replace(/import\s+\*\s+as\s+React\s+from\s+['"]react['"];?/g, "") @@ -101,11 +143,17 @@ export function compileCustomLayout(layoutCode: string): CompiledLayout | null { return null; } + const wrappedComponent: React.ComponentType<{ data: any }> = ({ data, ...props }) => { + const normalizedData = React.useMemo(() => normalizeLayoutAssetUrls(data), [data]); + return React.createElement(result.component, { ...(props as any), data: normalizedData }); + }; + wrappedComponent.displayName = `CompiledTemplateLayout(${result.layoutName || result.layoutId || "Custom"})`; + // Parse schema to get sample data let sampleData: Record = {}; if (result.Schema) { try { - sampleData = result.Schema.parse({}); + sampleData = normalizeLayoutAssetUrls(result.Schema.parse({})); } catch (e) { console.warn("Could not parse schema defaults:", e); } @@ -113,7 +161,7 @@ export function compileCustomLayout(layoutCode: string): CompiledLayout | null { const schemaJSON = z.toJSONSchema(result.Schema); return { - component: result.component, + component: wrappedComponent, layoutId: result.layoutId, layoutName: result.layoutName, layoutDescription: result.layoutDescription, diff --git a/servers/nextjs/app/hooks/useCustomTemplates.ts b/servers/nextjs/app/hooks/useCustomTemplates.ts index ebc451c0..0a4ccc2d 100644 --- a/servers/nextjs/app/hooks/useCustomTemplates.ts +++ b/servers/nextjs/app/hooks/useCustomTemplates.ts @@ -5,6 +5,9 @@ import { useState, useEffect, useCallback } from "react"; import { compileCustomLayout, CompiledLayout } from "./compileLayout"; import TemplateService from "../(presentation-generator)/services/api/template"; +/** + * API response types + */ export interface TemplateSummary { @@ -217,17 +220,15 @@ export function useCustomTemplateSummaries() { setError(null); const data: TemplateSummary[] = await TemplateService.getCustomTemplateSummaries(); - const mappedTemplates: CustomTemplates[] = data - .filter((item) => item.total_layouts && item.total_layouts > 0) - .map((item) => { - return { - id: item.id, - name: item.name || "Custom Template", - layoutCount: item.total_layouts, - isCustom: true as const, - }; - }); + const mappedTemplates: CustomTemplates[] = data.filter(item => item.total_layouts && item.total_layouts > 0).map((item) => { + return { + id: item.id, + name: item.name || "Custom Template", + layoutCount: item.total_layouts, + isCustom: true as const, + } + }); setTemplates(mappedTemplates); } catch (err) { @@ -391,6 +392,7 @@ export function useCustomTemplatePreview(presentationId: string) { try { setLoading(true); const data = await TemplateService.getCustomTemplateDetails(presentationId); + // Compile first 4 layouts for preview const compiled: CompiledLayout[] = []; const layoutsToPreview = data.layouts.slice(0, 4); diff --git a/servers/nextjs/components/OnBoarding/FinalStep.tsx b/servers/nextjs/components/OnBoarding/FinalStep.tsx index 5a445b61..cd589ff4 100644 --- a/servers/nextjs/components/OnBoarding/FinalStep.tsx +++ b/servers/nextjs/components/OnBoarding/FinalStep.tsx @@ -34,14 +34,9 @@ const FinalStep = () => { useEffect(() => { async function fetchStatus() { try { - if (window.electron?.telemetryStatus) { - const data = await window.electron.telemetryStatus(); - setTrackingEnabled(data.telemetryEnabled); - } else { - const res = await fetch('/api/telemetry-status'); - const data = await res.json(); - setTrackingEnabled(data.telemetryEnabled); - } + const res = await fetch('/api/telemetry-status'); + const data = await res.json(); + setTrackingEnabled(data.telemetryEnabled); } catch { setTrackingEnabled(true); } @@ -54,18 +49,12 @@ const FinalStep = () => { setTrackingEnabled(enabled); setTelemetryEnabled(enabled); try { - if (window.electron?.setUserConfig) { - await window.electron.setUserConfig({ + await fetch('/api/user-config', { + method: 'POST', + body: JSON.stringify({ DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : 'true', - } as any); - } else { - await fetch('/api/user-config', { - method: 'POST', - body: JSON.stringify({ - DISABLE_ANONYMOUS_TRACKING: enabled ? undefined : 'true', - }), - }); - } + }), + }); } catch { setTrackingEnabled(prev); setTelemetryEnabled(prev ?? true); diff --git a/servers/nextjs/lib/run-bundled-pdf-export.ts b/servers/nextjs/lib/run-bundled-pdf-export.ts index c6715a25..015f4c10 100644 --- a/servers/nextjs/lib/run-bundled-pdf-export.ts +++ b/servers/nextjs/lib/run-bundled-pdf-export.ts @@ -156,7 +156,6 @@ export async function runBundledPdfExport(params: { env: { ...process.env, BUILT_PYTHON_MODULE_PATH: converter, - ELECTRON_RUN_AS_NODE: "0", }, }); const stderr: Buffer[] = []; diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index b0c0d0b0..3e25faf7 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -9,7 +9,7 @@ const nextConfig = { return [ { source: '/app_data/fonts/:path*', - destination: 'http://localhost:8000/app_data/fonts/:path*', + destination: 'http://localhost:5000/app_data/fonts/:path*', }, ]; }, diff --git a/servers/nextjs/types/global.d.ts b/servers/nextjs/types/global.d.ts index ff0087b4..8e6e14fc 100644 --- a/servers/nextjs/types/global.d.ts +++ b/servers/nextjs/types/global.d.ts @@ -13,29 +13,7 @@ interface TextFrameProps { // Add other properties as needed } -// Electron IPC types (optional in Docker/web; present when embedded in Electron) -interface ElectronAPI { - fileDownloaded: (filePath: string) => Promise; - exportAsPDF: (id: string, title: string) => Promise; - getUserConfig: () => Promise; - setUserConfig: (userConfig: any) => Promise; - getCanChangeKeys: () => Promise; - readFile: (filePath: string) => Promise<{ content: string }>; - getSlideMetadata: (url: string, theme: string, customColors?: any, tempDirectory?: string) => Promise; - getFooter: (userId: string) => Promise; - setFooter: (userId: string, properties: any) => Promise; - getTheme: (userId: string) => Promise; - setTheme: (userId: string, themeData: any) => Promise; - uploadImage: (file: Buffer) => Promise; - writeNextjsLog: (logData: string) => Promise; - clearNextjsLogs: () => Promise; - hasRequiredKey: () => Promise<{ hasKey: boolean }>; - telemetryStatus: () => Promise<{ telemetryEnabled: boolean }>; - getTemplates: () => Promise>; -} - interface Window { - electron?: ElectronAPI; env?: { NEXT_PUBLIC_FAST_API: string; NEXT_PUBLIC_URL: string; diff --git a/servers/nextjs/utils/api.ts b/servers/nextjs/utils/api.ts index 5ea72462..5c6c0203 100644 --- a/servers/nextjs/utils/api.ts +++ b/servers/nextjs/utils/api.ts @@ -1,47 +1,103 @@ -// Same-origin API and static assets: nginx proxies /api/v1, /static, /app_data to fixed internal ports. +// Utility to get the FastAPI base URL +export function getFastAPIUrl(): string { + if (process.env.NEXT_PUBLIC_FAST_API) { + return process.env.NEXT_PUBLIC_FAST_API; + } -function withLeadingSlash(path: string): string { - return path.startsWith("/") ? path : `/${path}`; + const queryFastApiUrl = getFastApiUrlFromQuery(); + if (queryFastApiUrl) { + return queryFastApiUrl; + } + + // Docker/web runtime: route backend assets and APIs through current origin + // (nginx reverse-proxies /api/v1, /app_data, /static). + if (typeof window !== "undefined") { + return window.location.origin; + } + + return "http://127.0.0.1:5000"; +} + +function getFastApiUrlFromQuery(): string | null { + if (typeof window === "undefined") return null; + try { + const params = new URLSearchParams(window.location.search); + const value = params.get("fastapiUrl"); + if (!value) return null; + + const parsed = new URL(value); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return null; + } + return parsed.origin; + } catch { + return null; + } } function isAbsoluteHttpUrl(path: string): boolean { return /^https?:\/\//i.test(path); } -/** Browser: current site origin. Server render: localhost FastAPI (dev only). */ -export function getFastAPIUrl(): string { - if (typeof window !== "undefined") { - return window.location.origin; - } - return "http://127.0.0.1:8000"; +function withLeadingSlash(path: string): string { + return path.startsWith("/") ? path : `/${path}`; } -/** Use relative URLs; nginx serves /api/v1 on the same host as the UI. */ +// Utility to construct API URL for Docker/web runtime. export function getApiUrl(path: string): string { if (isAbsoluteHttpUrl(path)) { return path; } - return withLeadingSlash(path); + + const normalizedPath = withLeadingSlash(path); + const isFastApiEndpoint = normalizedPath.startsWith("/api/v1/"); + const hasConfiguredFastApi = + !!process.env.NEXT_PUBLIC_FAST_API || !!getFastApiUrlFromQuery(); + + // In web/docker, /api/v1 is typically reverse-proxied by the web server. + // If a FastAPI origin is explicitly configured, use it instead of same-origin proxy. + if (isFastApiEndpoint && hasConfiguredFastApi) { + return `${getFastAPIUrl()}${normalizedPath}`; + } + + return normalizedPath; } -/** Keep /static and /app_data as same-origin paths the browser resolves. */ +function hasBackendAssetPrefix(path: string): boolean { + return path.startsWith("/static/") || path.startsWith("/app_data/"); +} + +// Resolve backend-served asset paths to the FastAPI origin. export function resolveBackendAssetUrl(path?: string): string { if (!path) return ""; - const trimmed = path.trim(); - if (!trimmed) return ""; + const trimmedPath = path.trim(); + if (!trimmedPath) return ""; if ( - trimmed.startsWith("data:") || - trimmed.startsWith("blob:") || - trimmed.startsWith("file:") + trimmedPath.startsWith("data:") || + trimmedPath.startsWith("blob:") || + trimmedPath.startsWith("file:") ) { - return trimmed; + return trimmedPath; } - if (isAbsoluteHttpUrl(trimmed)) { - return trimmed; + if (isAbsoluteHttpUrl(trimmedPath)) { + try { + const parsed = new URL(trimmedPath); + if (hasBackendAssetPrefix(parsed.pathname)) { + return `${getFastAPIUrl()}${parsed.pathname}${parsed.search}${parsed.hash}`; + } + return trimmedPath; + } catch { + return trimmedPath; + } } - return withLeadingSlash(trimmed); + const normalizedPath = withLeadingSlash(trimmedPath); + if (hasBackendAssetPrefix(normalizedPath)) { + return `${getFastAPIUrl()}${normalizedPath}`; + } + + return trimmedPath; } diff --git a/servers/nextjs/utils/mixpanel.ts b/servers/nextjs/utils/mixpanel.ts index c8c1ac82..17e4e12d 100644 --- a/servers/nextjs/utils/mixpanel.ts +++ b/servers/nextjs/utils/mixpanel.ts @@ -138,16 +138,8 @@ async function ensureTelemetryStatus(): Promise { if (!trackingCheckPromise) { trackingCheckPromise = (async () => { try { - let data; - // Check if running in Electron environment - if (typeof window !== 'undefined' && window.electron?.telemetryStatus) { - // Use Electron IPC handler - data = await window.electron.telemetryStatus(); - } else { - // Fallback to API route for web-based deployments - const res = await fetch('/api/telemetry-status'); - data = await res.json(); - } + const res = await fetch('/api/telemetry-status'); + const data = await res.json(); const enabled = Boolean(data?.telemetryEnabled); window.__mixpanel_telemetry_enabled = enabled; return enabled;