Fix: PDF export empty slide issue

This commit is contained in:
shiva raj badu 2026-03-26 22:43:18 +05:45
parent 47295572fe
commit 22ed1ff2e3
No known key found for this signature in database
21 changed files with 24 additions and 574429 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,69 +0,0 @@
('/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/fastapi',
True,
False,
True,
None,
None,
False,
False,
None,
True,
False,
None,
None,
None,
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/fastapi.pkg',
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/PYZ-00.pyz',
'PYZ'),
('struct',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod03_ctypes.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/loader/pyiboot01_bootstrap.py',
'PYSOURCE'),
('runtime_hook_docling',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/runtime_hook_docling.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_inspect',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_setuptools',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_setuptools.py',
'PYSOURCE'),
('pyi_rth_pkgres',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgres.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/rthooks/pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_nltk',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/rthooks/pyi_rth_nltk.py',
'PYSOURCE'),
('server',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/server.py',
'PYSOURCE')],
[],
False,
False,
1771514382,
[('run',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run',
'EXECUTABLE')],
'/lib/x86_64-linux-gnu/libpython3.11.so.1.0')

View file

@ -1,64 +0,0 @@
('/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/fastapi.pkg',
{'BINARY': True,
'DATA': True,
'EXECUTABLE': True,
'EXTENSION': True,
'PYMODULE': True,
'PYSOURCE': True,
'PYZ': False,
'SPLASH': True,
'SYMLINK': False},
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/PYZ-00.pyz',
'PYZ'),
('struct',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/build/server/localpycs/pyimod03_ctypes.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/loader/pyiboot01_bootstrap.py',
'PYSOURCE'),
('runtime_hook_docling',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/runtime_hook_docling.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_inspect',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_setuptools',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_setuptools.py',
'PYSOURCE'),
('pyi_rth_pkgres',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgres.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/rthooks/pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_nltk',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/.venv/lib/python3.11/site-packages/_pyinstaller_hooks_contrib/rthooks/pyi_rth_nltk.py',
'PYSOURCE'),
('server',
'/home/sudipnext/Documents/presenton-3/electron/servers/fastapi/server.py',
'PYSOURCE')],
'libpython3.11.so.1.0',
True,
False,
False,
[],
None,
None,
None)

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,18 @@ import { Loader2 } from "lucide-react";
export const V1ContentRender = ({ slide, isEditMode, theme }: { slide: any, isEditMode: boolean, theme?: any, enableEditMode?: boolean }) => {
export const V1ContentRender = ({
slide,
isEditMode,
theme,
}: {
slide: any;
isEditMode: boolean;
theme?: any;
enableEditMode?: boolean;
}) => {
const dispatch = useDispatch();
const containerRef = useRef<HTMLDivElement | null>(null);
@ -86,7 +97,10 @@ export const V1ContentRender = ({ slide, isEditMode, theme }: { slide: any, isEd
if (isEditMode) {
return (
<SlideErrorBoundary label={`Slide ${slide.index + 1}`}>
<div ref={containerRef} className={`w-full h-full border border-[#EDEEEF] `}>
<div
ref={containerRef}
className={` `}
>
<EditableLayoutWrapper
slideIndex={slide.index}

View file

@ -148,7 +148,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
<div className="">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
className=" mx-auto flex flex-col items-center overflow-hidden justify-center "
>
{!presentationData ||
@ -172,8 +172,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
<div key={index} className="w-full" data-speaker-note={slide.speaker_note}>
<V1ContentRender slide={slide} isEditMode={true} theme={null}
<div key={index} className="w-full " data-speaker-note={slide.speaker_note}>
<V1ContentRender
slide={slide}
isEditMode={true}
theme={null}
/>
</div>
))}

View file

@ -1,328 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
SquareArrowOutUpRight,
Play,
Loader2,
Redo2,
Undo2,
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
import { useRouter, usePathname } from "next/navigation";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import { useDispatch, useSelector } from "react-redux";
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";
import PDFIMAGE from "@/public/pdf.svg";
import PPTXIMAGE from "@/public/pptx.svg";
import Image from "next/image";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
import ToolTip from "@/components/ToolTip";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { clearHistory } from "@/store/slices/undoRedoSlice";
import { getApiUrl } from "@/utils/api";
const Header = ({
presentation_id,
currentSlide,
}: {
presentation_id: string;
currentSlide?: number;
}) => {
const [open, setOpen] = useState(false);
const [showLoader, setShowLoader] = useState(false);
const router = useRouter();
const pathname = usePathname();
const dispatch = useDispatch();
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const { onUndo, onRedo, canUndo, canRedo } = usePresentationUndoRedo();
const get_presentation_pptx_model = async (id: string): Promise<PptxPresentationModel> => {
// Use Electron IPC if available (Electron build), otherwise use HTTP API
if (typeof window !== 'undefined' && (window as any).electron?.getPresentationPptxModel) {
const result = await (window as any).electron.getPresentationPptxModel(id);
if (!result.success) {
throw new Error(result.error || 'Failed to get presentation PPTX model');
}
return result.data;
}
// Fallback to HTTP API for non-Electron environments
const response = await fetch(getApiUrl(`/api/presentation_to_pptx_model?id=${id}`));
const pptx_model = await response.json();
return pptx_model;
};
const exportViaIpc = async (format: "pptx" | "pdf"): Promise<boolean> => {
if (typeof window === 'undefined') return false;
if (!(window as any).electron?.exportPresentation) return false;
trackEvent(
format === "pptx"
? MixpanelEvent.Header_ExportAsPPTX_API_Call
: MixpanelEvent.Header_ExportAsPDF_API_Call
);
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;
try {
setOpen(false);
setShowLoader(true);
// Save the presentation data before exporting
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
await PresentationGenerationApi.updatePresentationContent(presentationData);
if (await exportViaIpc("pptx")) {
toast.success("PPTX exported successfully!");
return;
}
throw new Error("Export is only supported in the desktop app.");
} catch (error) {
console.error("Export failed:", error);
setShowLoader(false);
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
});
} finally {
setShowLoader(false);
}
};
const handleExportPdf = async () => {
if (isStreaming) return;
try {
setOpen(false);
setShowLoader(true);
// Save the presentation data before exporting
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
await PresentationGenerationApi.updatePresentationContent(presentationData);
trackEvent(MixpanelEvent.Header_ExportAsPDF_API_Call);
if (await exportViaIpc("pdf")) {
toast.success("PDF exported successfully!");
return;
}
// Fallback to API route for web-based deployments
const response = await fetch('/api/export-as-pdf', {
method: 'POST',
body: JSON.stringify({
id: presentation_id,
title: presentationData?.title,
})
});
if (response.ok) {
const { path: pdfPath } = await response.json();
// window.open(pdfPath, '_blank');
downloadLink(pdfPath);
} else {
throw new Error("Failed to export PDF");
}
} catch (err) {
console.error(err);
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
});
} finally {
setShowLoader(false);
}
};
const handleReGenerate = () => {
dispatch(clearPresentationData());
dispatch(clearHistory())
trackEvent(MixpanelEvent.Header_ReGenerate_Button_Clicked, { pathname });
router.push(`/presentation?id=${presentation_id}&stream=true`);
};
const downloadLink = (path: string) => {
// if we have popup access give direct download if not redirect to the path
if (window.opener) {
window.open(path, '_blank');
} else {
const link = document.createElement('a');
link.href = path;
link.download = path.split('/').pop() || 'download';
document.body.appendChild(link);
link.click();
}
};
const ExportOptions = ({ mobile }: { mobile: boolean }) => (
<div className={`space-y-2 max-md:mt-4 ${mobile ? "" : "bg-white"} rounded-lg`}>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname });
handleExportPdf();
}}
variant="ghost"
className={`pb-4 border-b rounded-none border-gray-300 w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6 border-none rounded-lg" : ""}`} >
<Image src={PDFIMAGE} alt="pdf export" width={30} height={30} />
Export as PDF
</Button>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname });
handleExportPptx();
}}
variant="ghost"
className={`w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6" : ""}`}
>
<Image src={PPTXIMAGE} alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
</div>
);
const MenuItems = ({ mobile }: { mobile: boolean }) => (
<div className="flex flex-col lg:flex-row items-center gap-4">
{/* undo redo */}
<button onClick={handleReGenerate} disabled={isStreaming || !presentationData} className="text-white disabled:opacity-50" >
Re-Generate
</button>
<div className="flex items-center gap-2 ">
<ToolTip content="Undo">
<button disabled={!canUndo} className="text-white disabled:opacity-50" onClick={() => {
onUndo();
}}>
<Undo2 className="w-6 h-6 " />
</button>
</ToolTip>
<ToolTip content="Redo">
<button disabled={!canRedo} className="text-white disabled:opacity-50" onClick={() => {
onRedo();
}}>
<Redo2 className="w-6 h-6 " />
</button>
</ToolTip>
</div>
{/* Present Button */}
<Button
onClick={() => {
const to = `?id=${presentation_id}&mode=present&slide=${currentSlide || 0}`;
trackEvent(MixpanelEvent.Navigation, { from: pathname, to });
router.push(to);
}}
variant="ghost"
className="border border-white font-bold text-white rounded-[32px] transition-all duration-300 group"
>
<Play className="w-4 h-4 mr-1 stroke-white group-hover:stroke-black" />
Present
</Button>
{/* Desktop Export Button with Popover */}
<div style={{
zIndex: 100
}} className="hidden lg:block relative ">
<Popover open={open} onOpenChange={setOpen} >
<PopoverTrigger asChild>
<Button className={`border py-5 text-[#5146E5] font-bold rounded-[32px] transition-all duration-500 hover:border hover:bg-[#5146E5] hover:text-white w-full ${mobile ? "" : "bg-white"}`}>
<SquareArrowOutUpRight className="w-4 h-4 mr-1" />
Export
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[250px] space-y-2 py-3 px-2 ">
<ExportOptions mobile={false} />
</PopoverContent>
</Popover>
</div>
{/* Mobile Export Section */}
<div className="lg:hidden flex flex-col w-full">
<ExportOptions mobile={true} />
</div>
</div>
);
return (
<>
<OverlayLoader
show={showLoader}
text="Exporting presentation..."
showProgress={true}
duration={40}
/>
<div
className="bg-[#5146E5] w-full shadow-lg sticky top-0 ">
<Announcement />
<Wrapper className="flex items-center justify-between py-1">
<Link href="/dashboard" className="min-w-[162px]">
<img
className="h-16"
src="/logo-white.png"
alt="Presentation logo"
/>
</Link>
{/* Desktop Menu */}
<div className="hidden lg:flex items-center gap-4 2xl:gap-6">
{isStreaming && (
<Loader2 className="animate-spin text-white font-bold w-6 h-6" />
)}
<MenuItems mobile={false} />
<HeaderNav />
</div>
{/* Mobile Menu */}
<div className="lg:hidden flex items-center gap-4">
<HeaderNav />
</div>
</Wrapper>
</div>
</>
);
};
export default Header;

View file

@ -1,312 +0,0 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { HelpCircle, X, Search } from "lucide-react";
import React, { useState, useEffect, useRef } from "react";
const helpQuestions = [
{
id: 1,
category: "Images",
question: "How do I change an image?",
answer:
"Click on any image to reveal the image toolbar. You'll see options to Edit, Adjust position, and change how the image fits within its container. The Edit option allows you to replace or modify the current image.",
},
{
id: 2,
category: "Images",
question: "Can I generate new images with AI?",
answer:
"Yes! Click on any image and select the Edit option from the toolbar. In the side panel that appears, you'll find the AI Generate tab. Enter your prompt describing the image you want, and our AI will generate an image based on your description.",
},
{
id: 3,
category: "Images",
question: "How do I upload my own images?",
answer:
"Click on any image, then select Edit from the toolbar. In the side panel, click on the Upload tab at the top. You can browse your files to select one. Once uploaded, you can apply it to your design.",
},
{
id: 11,
category: "AI Prompts",
question: "Can I change slide layout through prompt?",
answer:
"Yes you can! Click on the WandSparkles icon on the top left of each slide and it will give you a prompt input box. Describe your layout requirements and the AI will change the slide layout accordingly.",
},
{
id: 12,
category: "AI Prompts",
question: "Can I change slide image through prompt?",
answer:
"Yes you can! Click on the WandSparkles icon on the top left of each slide and it will give you a prompt input box. Describe the image you want and the AI will update the slide image based on your requirements.",
},
{
id: 14,
category: "AI Prompts",
question: "Can I change content through prompt?",
answer:
"Yes you can! Click on the WandSparkles icon on the top left of each slide and it will give you a prompt input box. Describe what content you want and the AI will update the slide's text and content based on your description.",
},
{
id: 4,
category: "Text",
question: "How can I format and highlight text?",
answer:
"Select any text to see the formatting toolbar appear. You'll have options for Bold, Italic, Underline, Strikethrough,and more.",
},
{
id: 5,
category: "Icons",
question: "How do I change icons?",
answer:
"Click on any existing icon to modify it. In the icon selector panel, you can browse icos or use the search function to find specific icons. We offer thousands of icons in various styles.",
},
{
id: 16,
category: "Layout",
question: "Can I change the position of slide?",
answer:
"Of course, On side panel you can drag the slide and place wherever you want.",
},
{
id: 15,
category: "Layout",
question: "Can I add new slide between the slide?",
answer:
"Yes you can just click on the plus icon below each slide.It will display the all the layouts and choose required one.",
},
{
id: 6,
category: "Layout",
question: "Can I add more sections to my slides?",
answer:
"Absolutely! Hover near the bottom of any text box or content block, and you'll see a + icon appear. Click this button to add a new section below the current one. You can also use the Insert menu to add specific section types.",
},
{
id: 8,
category: "Export",
question: "How do I download or export my presentation?",
answer:
"Click the Export button in the top right menu. You can choose to download as PDF, PowerPoint.",
},
];
const Help = () => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [filteredQuestions, setFilteredQuestions] = useState(helpQuestions);
const [categories, setCategories] = useState<string[]>([]);
const [selectedCategory, setSelectedCategory] = useState("All");
const modalRef = useRef<HTMLDivElement>(null);
// Extract unique categories and create "All" category list
useEffect(() => {
const uniqueCategories = Array.from(
new Set(helpQuestions.map((q) => q.category))
);
setCategories(["All", ...uniqueCategories]);
}, []);
// Filter questions based on search query and selected category
useEffect(() => {
let results = helpQuestions;
// Filter by category if not "All"
if (selectedCategory !== "All") {
results = results.filter((q) => q.category === selectedCategory);
}
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
results = results.filter(
(q) =>
q.question.toLowerCase().includes(query) ||
q.answer.toLowerCase().includes(query)
);
}
setFilteredQuestions(results);
}, [searchQuery, selectedCategory]);
// Close modal when clicking outside
useEffect(() => {
const handleClickOutside = (event: any) => {
if (
modalRef.current &&
!modalRef.current.contains(event.target) &&
!event.target.closest(".help-button")
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
const handleOpenClose = () => {
setIsOpen(!isOpen);
};
// Animation helpers
const modalClass = isOpen
? "opacity-100 scale-100"
: "opacity-0 scale-95 pointer-events-none";
return (
<>
{/* Help Button */}
<button
onClick={handleOpenClose}
className="help-button hidden fixed bottom-6 right-6 h-12 w-12 z-50 bg-emerald-600 hover:bg-emerald-700 rounded-full md:flex justify-center items-center cursor-pointer shadow-lg transition-all duration-300 hover:shadow-xl"
aria-label="Help Center"
>
{isOpen ? (
<X className="text-white h-5 w-5" />
) : (
<HelpCircle className="text-white h-5 w-5" />
)}
</button>
{/* Help Modal */}
<div
className={`fixed bottom-20 right-6 z-50 max-w-md w-full transition-all duration-300 transform ${modalClass}`}
ref={modalRef}
>
<div className="bg-white rounded-lg shadow-2xl border border-gray-200 overflow-hidden">
{/* Header */}
<div className="bg-emerald-600 text-white px-6 py-4 flex justify-between items-center">
<h2 className="text-lg font-medium">Help Center</h2>
<button
onClick={() => setIsOpen(false)}
className="hover:bg-emerald-700 p-1 rounded"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Search */}
<div className="px-6 pt-4 pb-2">
<div className="relative">
<input
type="text"
placeholder="Search help topics..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
/>
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
</div>
</div>
{/* Category Pills */}
<div className="px-6 pb-3 flex gap-2 overflow-x-auto hide-scrollbar">
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 py-1 rounded-full text-sm whitespace-nowrap ${selectedCategory === category
? "bg-emerald-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{category}
</button>
))}
</div>
{/* FAQ Accordion */}
<div className="max-h-96 overflow-y-auto px-6 pb-6">
{filteredQuestions.length > 0 ? (
<Accordion type="single" collapsible className="w-full">
{filteredQuestions.map((faq, index) => (
<AccordionItem
key={index}
value={`item-${index}`}
className="border-b border-gray-200 last:border-b-0"
>
<AccordionTrigger className="hover:no-underline py-3 px-1 text-left flex">
<div className="flex-1 pr-2">
<span className="text-gray-900 font-medium text-sm md:text-base">
{faq.question}
</span>
<span className="block text-xs text-emerald-600 mt-0.5">
{faq.category}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-1 pb-3">
<div className="text-sm text-gray-600 leading-relaxed rounded bg-gray-50 p-3">
{faq.answer}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<div className="py-8 text-center text-gray-500">
<p>No results found for "{searchQuery}"</p>
<button
onClick={() => {
setSearchQuery("");
setSelectedCategory("All");
}}
className="mt-2 text-emerald-600 hover:underline text-sm"
>
Clear search
</button>
</div>
)}
</div>
{/* Footer */}
<div className="bg-gray-50 px-6 py-3 border-t border-gray-200 text-xs text-gray-500 text-center">
Still need help?{" "}
<a href="/contact" className="text-emerald-600 hover:underline">
Contact Support
</a>
</div>
</div>
</div>
{/* Custom AccordionTrigger implementation (since shadcn's might not be available) */}
{!AccordionTrigger && (
<style jsx>{`
.accordion-trigger {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
text-align: left;
transition: all 0.2s;
}
.accordion-trigger:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.accordion-content {
overflow: hidden;
height: 0;
transition: height 0.2s ease;
}
.accordion-content[data-state="open"] {
height: auto;
}
`}</style>
)}
</>
);
};
export default Help;

View file

@ -252,7 +252,7 @@ const PresentationHeader = ({
{isPresentationSaving && <div className="flex items-center gap-2">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
</div>}
<ThemeSelector current_theme={presentationData?.theme || {}} themes={themes} />
{presentationData && presentationData.slides && !presentationData.slides[0].layout.includes("custom") && <ThemeSelector current_theme={presentationData?.theme || {}} themes={themes} />}
<div className="flex items-center gap-2 bg-[#F6F6F9] px-3.5 h-[38px] border border-[#EDECEC] rounded-[80px]">

View file

@ -276,82 +276,6 @@ thead {
@apply prose prose-slate max-w-none;
}
/* .markdown-content h1 {
@apply text-xl font-bold mb-4 text-gray-900;
}
.markdown-content h2 {
@apply text-lg font-bold mb-3 text-gray-900;
}
.markdown-content h3 {
@apply text-base font-bold mb-2 text-gray-900;
}
.markdown-content h4 {
@apply text-sm font-bold mb-2 text-gray-900;
}
.markdown-content h5 {
@apply text-xs font-bold mb-1 text-gray-900;
}
.markdown-content h6 {
@apply text-xs font-semibold mb-1 text-gray-900;
}
.markdown-content p {
@apply mb-4 text-base text-gray-700;
}
.markdown-content ul {
@apply list-disc pl-6 mb-4;
}
.markdown-content ol {
@apply list-decimal pl-6 mb-4;
}
.markdown-content li {
@apply mb-1;
}
.markdown-content strong,
.markdown-content b {
@apply font-bold text-gray-900;
}
.markdown-content em {
@apply italic;
}
.markdown-content blockquote {
@apply border-l-4 border-gray-300 pl-4 italic my-4;
}
.markdown-content code {
@apply bg-gray-100 px-1 py-0.5 rounded font-mono text-sm;
}
.markdown-content pre {
@apply bg-gray-100 p-4 rounded-lg my-4 overflow-x-auto;
}
.markdown-content a {
@apply text-blue-600 hover:text-blue-800 underline;
}
.markdown-content table {
@apply min-w-full border border-gray-300 my-4;
}
.markdown-content th {
@apply bg-gray-100 border border-gray-300 px-4 py-2 font-bold;
}
.markdown-content td {
@apply border border-gray-300 px-4 py-2;
} */
/* Override Tailwind Typography prose heading sizes for markdown editor */
.prose h1 {