Fix: PDF export empty slide issue
This commit is contained in:
parent
47295572fe
commit
22ed1ff2e3
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
|
|
@ -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')
|
||||
|
|
@ -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)
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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]">
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue