diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..df2d8fbf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "presenton", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "uuid": "^11.1.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..220d763d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "uuid": "^11.1.0" + } +} diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index 5c8aaec5..93517b12 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles from api.lifespan import app_lifespan from api.middlewares import UserConfigEnvUpdateMiddleware from api.v1.ppt.router import API_V1_PPT_ROUTER -from utils.asset_directory_utils import get_exports_directory, get_images_directory +from utils.asset_directory_utils import get_exports_directory, get_images_directory, get_uploads_directory app = FastAPI(lifespan=app_lifespan) @@ -25,6 +25,11 @@ app.mount( StaticFiles(directory=get_exports_directory()), name="app_data/exports", ) +app.mount( + "/app_data/uploads", + StaticFiles(directory=get_uploads_directory()), + name="app_data/uploads", +) # Middlewares diff --git a/servers/fastapi/models/pptx_models.py b/servers/fastapi/models/pptx_models.py index 768d31c1..dd42fb2a 100644 --- a/servers/fastapi/models/pptx_models.py +++ b/servers/fastapi/models/pptx_models.py @@ -61,11 +61,13 @@ class PptxFontModel(BaseModel): class PptxFillModel(BaseModel): color: str + opacity: float = 1.0 class PptxStrokeModel(BaseModel): color: str thickness: float + opacity: float = 1.0 class PptxShadowModel(BaseModel): @@ -85,6 +87,7 @@ class PptxParagraphModel(BaseModel): spacing: Optional[PptxSpacingModel] = None alignment: Optional[PP_ALIGN] = None font: Optional[PptxFontModel] = None + line_height: Optional[float] = None text: Optional[str] = None text_runs: Optional[List[PptxTextRunModel]] = None @@ -141,6 +144,7 @@ class PptxConnectorModel(PptxShapeModel): position: PptxPositionModel thickness: float = 0.5 color: str = "000000" + opacity: float = 1.0 class PptxSlideModel(BaseModel): diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index 53bd6fb8..1bbde2a5 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -10,6 +10,7 @@ from pptx.text.text import _Paragraph, TextFrame, Font, _Run from pptx.opc.constants import RELATIONSHIP_TYPE as RT from lxml.etree import fromstring, tostring from PIL import Image +from pptx.oxml.xmlchemy import OxmlElement from pptx.util import Pt from pptx.dml.color import RGBColor @@ -55,6 +56,13 @@ class PptxPresentationCreator: self._ppt.slide_width = Pt(1280) self._ppt.slide_height = Pt(720) + def get_sub_element(self, parent, tagname, **kwargs): + """Helper method to create XML elements""" + element = OxmlElement(tagname) + element.attrib.update(kwargs) + parent.append(element) + return element + async def fetch_network_assets(self): image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] @@ -158,6 +166,8 @@ class PptxPresentationCreator: ) connector_shape.line.width = Pt(connector_model.thickness) connector_shape.line.color.rgb = RGBColor.from_string(connector_model.color) + # Set line opacity using XML manipulation for better reliability + self.set_line_opacity(connector_shape, connector_model.opacity) def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path @@ -252,6 +262,9 @@ class PptxPresentationCreator: if paragraph_model.spacing: self.apply_spacing_to_paragraph(paragraph, paragraph_model.spacing) + if paragraph_model.line_height: + paragraph.line_spacing = paragraph_model.line_height + if paragraph_model.alignment: paragraph.alignment = paragraph_model.alignment @@ -365,6 +378,7 @@ class PptxPresentationCreator: else: shape.fill.solid() shape.fill.fore_color.rgb = RGBColor.from_string(fill.color) + self.set_fill_opacity(shape.fill, fill.opacity) def apply_stroke_to_shape( self, shape: Shape, stroke: Optional[PptxStrokeModel] = None @@ -375,6 +389,7 @@ class PptxPresentationCreator: shape.line.fill.solid() shape.line.fill.fore_color.rgb = RGBColor.from_string(stroke.color) shape.line.width = Pt(stroke.thickness) + self.set_fill_opacity(shape.line.fill, stroke.opacity) def apply_shadow_to_shape( self, shape: Shape, shadow: Optional[PptxShadowModel] = None @@ -427,6 +442,19 @@ class PptxPresentationCreator: nsmap=nsmap, ) + def set_fill_opacity(self, fill, opacity): + if opacity is None or opacity >= 1.0: + return + + alpha = int((opacity) * 100000) + + try: + ts = fill._xPr.solidFill + sF = ts.get_or_change_to_srgbClr() + self.get_sub_element(sF, "a:alpha", val=str(alpha)) + except Exception as e: + print(f"Could not set fill opacity: {e}") + def get_margined_position( self, position: PptxPositionModel, margin: Optional[PptxSpacingModel] ) -> PptxPositionModel: diff --git a/servers/fastapi/utils/asset_directory_utils.py b/servers/fastapi/utils/asset_directory_utils.py index 13a0673c..a88196c1 100644 --- a/servers/fastapi/utils/asset_directory_utils.py +++ b/servers/fastapi/utils/asset_directory_utils.py @@ -12,3 +12,8 @@ def get_exports_directory(): export_directory = os.path.join(get_app_data_directory_env(), "exports") os.makedirs(export_directory, exist_ok=True) return export_directory + +def get_uploads_directory(): + uploads_directory = os.path.join(get_app_data_directory_env(), "uploads") + os.makedirs(uploads_directory, exist_ok=True) + return uploads_directory diff --git a/servers/nextjs/app/(presentation-generator)/components/CustomThemeSettings.tsx b/servers/nextjs/app/(presentation-generator)/components/CustomThemeSettings.tsx deleted file mode 100644 index 02cd1e66..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/CustomThemeSettings.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, { useState, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { - setThemeColors, - setTheme, - setLoadingState, -} from "../store/themeSlice"; -import { ThemeType } from "../upload/type"; - -import { useThemeService, ThemeColors } from "../services/themeService"; -import { PresentationGenerationApi } from "../services/api/presentation-generation"; - -interface CustomThemeSettingsProps { - onClose?: () => void; - presentationId: string; -} - -const CustomThemeSettings = ({ - onClose, - presentationId, -}: CustomThemeSettingsProps) => { - const dispatch = useDispatch(); - const [draftColors, setDraftColors] = useState({ - background: "#63ceff", - slideBg: "#F4F4F4", - slideTitle: "#1A1A1A", - slideHeading: "#2D2D2D", - slideDescription: "#4A4A4A", - slideBox: "#d8c6c6", - iconBg: "#281810", - chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"], - fontFamily: "var(--font-inter)", - }); - - const themeService = useThemeService(); - - // Refs for tracking drag state and RAF - const isDragging = useRef(false); - const rafId = useRef(); - const currentKey = useRef(); - const currentValue = useRef(); - - const updateDraftColor = (key: string, value: string) => { - if (rafId.current) { - cancelAnimationFrame(rafId.current); - } - - rafId.current = requestAnimationFrame(() => { - setDraftColors((prev) => ({ - ...prev, - [key]: value, - })); - }); - }; - - const handleColorPickerChange = (key: string, value: string) => { - if (isDragging.current) { - // Update refs for current values - currentKey.current = key; - currentValue.current = value; - // Update preview immediately - const previewElement = document.getElementById(`preview-${key}`); - if (previewElement) { - previewElement.style.backgroundColor = value; - } - } else { - // For non-drag changes (like text input), update immediately - updateDraftColor(key, value); - } - }; - - const handleColorPickerMouseDown = () => { - isDragging.current = true; - }; - - const handleColorPickerMouseUp = () => { - isDragging.current = false; - // Apply the final color value - if (currentKey.current && currentValue.current) { - updateDraftColor(currentKey.current, currentValue.current); - } - }; - - const handleTextInputChange = (key: string, value: string) => { - updateDraftColor(key, value); - }; - - const handleSave = async () => { - try { - // Update UI immediately - const themeType = "custom" as ThemeType; - dispatch(setTheme(themeType)); - dispatch( - setThemeColors({ - ...draftColors, - theme: themeType, - }) - ); - - // Set CSS variables - const root = document.documentElement; - root.style.setProperty("--custom-slide-bg", draftColors.slideBg); - root.style.setProperty("--custom-slide-title", draftColors.slideTitle); - root.style.setProperty( - "--custom-slide-heading", - draftColors.slideHeading - ); - root.style.setProperty( - "--custom-slide-description", - draftColors.slideDescription - ); - root.style.setProperty("--custom-slide-box", draftColors.slideBox); - root.style.setProperty("--custom-icon-bg", draftColors.iconBg); - - // Save to file and API - await Promise.all([ - PresentationGenerationApi.setThemeColors(presentationId, { - name: themeType, - colors: { - ...draftColors, - }, - }), - themeService.saveTheme({ - name: "custom", - colors: { - ...draftColors, - theme: themeType, - }, - }), - ]); - - onClose?.(); - } catch (error) { - console.error("Failed to save custom theme:", error); - } - }; - - // Load saved theme - React.useEffect(() => { - const loadSavedCustomTheme = async () => { - try { - dispatch(setLoadingState(true)); - const savedTheme = await themeService.getTheme(); - if (savedTheme) { - setDraftColors(savedTheme.colors); - } - } catch (error) { - console.error("Failed to load theme preferences:", error); - } finally { - dispatch(setLoadingState(false)); - } - }; - - loadSavedCustomTheme(); - }, []); - - // Cleanup RAF on unmount - React.useEffect(() => { - return () => { - if (rafId.current) { - cancelAnimationFrame(rafId.current); - } - }; - }, []); - - const colorInputs = [ - { key: "background", label: "Background Color", icon: "🎨" }, - { key: "slideBg", label: "Slide Background Color", icon: "🎨" }, - { key: "slideTitle", label: "Title Color", icon: "📝" }, - { key: "slideHeading", label: "Heading Color", icon: "🔤" }, - { key: "slideDescription", label: "Description Color", icon: "📄" }, - { key: "slideBox", label: "Box Color", icon: "📦" }, - { key: "iconBg", label: "Icon Background Color", icon: "📦" }, - ]; - - return ( -
-
- {/* Live Preview */} -
-

Live Preview

-
-
-
- Sample Title -
-
- Heading -
-
- Description text -
-
-
-
-
-
- {colorInputs.map(({ key, label, icon }) => ( -
-
- handleColorPickerChange(key, e.target.value)} - onMouseDown={() => handleColorPickerMouseDown()} - onMouseUp={() => handleColorPickerMouseUp()} - onTouchStart={() => handleColorPickerMouseDown()} - onTouchEnd={() => handleColorPickerMouseUp()} - className="w-12 h-12 p-1 cursor-pointer border rounded-lg transition-all hover:border-[#5146E5] focus:border-[#5146E5]" - /> -
-
-
- - handleTextInputChange(key, e.target.value)} - className="h-8 font-mono text-sm" - placeholder="#000000" - /> -
-
- ))} -
-
-
- - -
-
- ); -}; - -export default CustomThemeSettings; diff --git a/servers/nextjs/app/(presentation-generator)/components/EditableText.tsx b/servers/nextjs/app/(presentation-generator)/components/EditableText.tsx deleted file mode 100644 index b2702807..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/EditableText.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { useSelector } from "react-redux"; -import TipTapEditor from "./Tiptap"; -import { RootState } from "@/store/store"; -import Typewriter from "./TypeWriter"; - -interface EditableTextProps { - slideIndex: number; - bodyIdx?: number; - elementId: string; - type: - | "title" - | "heading" - | "description-body" - | "description" - | "heading-description" - | "info-heading" - | "info-description"; - content: string; - isAlingCenter?: boolean; -} - -const EditableText = ({ - slideIndex, - elementId, - type, - content, - bodyIdx = 0, - isAlingCenter = false, -}: EditableTextProps) => { - const { isStreaming } = useSelector( - (state: RootState) => state.presentationGeneration - ); - - const elementRef = useRef(null); - - // Add useEffect to initialize content - useEffect(() => { - if (elementRef.current) { - const displayContent = content || getPlaceholder(); - elementRef.current.textContent = displayContent; - - // Add placeholder styling if needed - if (!content) { - elementRef.current.classList.add("text-gray-400"); - } - } - }, [content]); - - const getPlaceholder = () => { - switch (type) { - case "title": - return "Enter title"; - case "heading": - return "Enter heading"; - case "description-body": - return "Enter description"; - case "description": - return "Enter description"; - case "heading-description": - return "Enter description"; - case "info-heading": - return "Enter heading"; - case "info-description": - return "Enter description"; - default: - return "Enter text"; - } - }; - - const getTextStyle = () => { - const baseStyle = "outline-none transition-all duration-200"; - switch (type) { - case "title": - return `${baseStyle} text-[40px] slide-title leading-[48px] font-bold`; - case "heading": - return `${baseStyle} text-[24px] slide-heading leading-[32px] font-bold`; - case "description": - case "description-body": - case "heading-description": - return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`; - default: - return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`; - } - }; - - return ( - <> - {isStreaming ? ( -
- -
- ) : ( - - )} - - ); -}; - -export default React.memo(EditableText); diff --git a/servers/nextjs/app/(presentation-generator)/components/ElementMenu.tsx b/servers/nextjs/app/(presentation-generator)/components/ElementMenu.tsx deleted file mode 100644 index 7c7697ec..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/ElementMenu.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { MoreHorizontal } from "lucide-react"; -import React from "react"; - -interface ElementMenuProps { - index: number; - handleDeleteItem: (index: number) => void; -} - -const ElementMenu = ({ index, handleDeleteItem }: ElementMenuProps) => { - return ( - - - - - - handleDeleteItem(index)} - className="px-3 py-2 cursor-pointer" - > - Delete Item {index + 1} - - - - ); -}; - -// Prevent unnecessary re-renders -export default React.memo(ElementMenu, (prevProps, nextProps) => { - return ( - prevProps.index === nextProps.index && prevProps.index === nextProps.index - ); -}); diff --git a/servers/nextjs/app/(presentation-generator)/components/UserAccount.tsx b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx similarity index 95% rename from servers/nextjs/app/(presentation-generator)/components/UserAccount.tsx rename to servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx index 9d213c1a..5f0fbe2e 100644 --- a/servers/nextjs/app/(presentation-generator)/components/UserAccount.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { RootState } from "@/store/store"; import { useSelector } from "react-redux"; -const UserAccount = () => { +const HeaderNav = () => { const canChangeKeys = useSelector((state: RootState) => state.userConfig.can_change_keys); @@ -39,4 +39,4 @@ const UserAccount = () => { ); }; -export default UserAccount; +export default HeaderNav; diff --git a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx new file mode 100644 index 00000000..4ff3c840 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx @@ -0,0 +1,68 @@ +import { Trash2 } from 'lucide-react'; +import React from 'react' +import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader'; +import { useDispatch } from 'react-redux'; +import { addNewSlide } from '@/store/slices/presentationGeneration'; +import { Loader2 } from 'lucide-react'; + +interface NewSlideProps { + setShowNewSlideSelection: (show: boolean) => void; + group: string; + index: number; + presentationId: string; +} +const NewSlide = ({ setShowNewSlideSelection, group, index, presentationId }: NewSlideProps) => { + const dispatch = useDispatch(); + const handleNewSlide = (sampleData: any, id: string) => { + const newSlide = { + id: crypto.randomUUID(), + index: index, + content: sampleData, + layout_group: group, + layout: `${group}:${id}`, + presentation: presentationId + } + dispatch(addNewSlide({ slideData: newSlide, index })); + setShowNewSlideSelection(false); + } + const { layoutGroup, loading } = useGroupLayoutLoader(group) + + if (loading) { + return ( +
+
+

Select a Slide Layout

+ setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' /> +
+
+ +
+
+ ) + } + + return ( +
+
+ +

Select a Slide Layout

+ setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' /> +
+
+ {layoutGroup && layoutGroup?.layouts.map((layout: any, index: number) => { + const { component: LayoutComponent, sampleData, layoutId } = layout + return ( +
handleNewSlide(sampleData, layoutId)} key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video"> +
+
+ +
+
+ ) + })} +
+
+ ) +} + +export default NewSlide diff --git a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx index 4ceec318..e466711a 100644 --- a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx @@ -15,7 +15,7 @@ import { useGroupLayouts } from "../hooks/useGroupLayouts"; interface PresentationModeProps { slides: Slide[]; currentSlide: number; - currentTheme: string; + isFullscreen: boolean; onFullscreenToggle: () => void; onExit: () => void; @@ -26,7 +26,7 @@ const PresentationMode: React.FC = ({ slides, currentSlide, - currentTheme, + isFullscreen, onFullscreenToggle, onExit, @@ -190,10 +190,9 @@ const PresentationMode: React.FC = ({
{slides[currentSlide] && - renderSlideContent(slides[currentSlide])} + renderSlideContent(slides[currentSlide], false)}
diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/SlideFooter.tsx b/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx similarity index 97% rename from servers/nextjs/app/(presentation-generator)/components/slide_layouts/SlideFooter.tsx rename to servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx index 0ae124f3..6905aaca 100644 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/SlideFooter.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/SlideFooter.tsx @@ -18,12 +18,12 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import React, { useRef, useState, useEffect } from "react"; import { Camera, Loader2, Plus } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { getStaticFileUrl, isDarkColor } from "../../utils/others"; import { defaultFooterProperties, useFooterContext } from "../../context/footerContext"; import { FooterProperties } from "../../services/footerService"; +import { toast } from "sonner"; const SlideFooter: React.FC = () => { const [showEditor, setShowEditor] = useState(false); @@ -43,17 +43,13 @@ const SlideFooter: React.FC = () => { const handleSave = async () => { await saveFooterProperties(footerProperties); setIsPropertyChanged(false); - toast({ - title: "Footer properties saved successfully", - }); + toast.success("Footer properties saved successfully"); }; const handleReset = async () => { await resetFooterProperties(); setFooterProperties(defaultFooterProperties); - toast({ - title: "Footer properties reset to default", - }); + toast.success("Footer properties reset to default"); }; const updateProperty = (path: string, value: any): void => { @@ -186,9 +182,7 @@ const SlideFooter: React.FC = () => { if (!file) return; if (!file.type.startsWith("image/")) { - toast({ - title: "Please Upload An Image File", - }); + toast.error("Please Upload An Image File"); return; } @@ -208,10 +202,7 @@ const SlideFooter: React.FC = () => { })); } catch (error) { console.error("Error converting image:", error); - toast({ - title: "Error uploading image", - variant: "destructive", - }); + toast.error("Error uploading image"); } finally { setIsUploading({ ...isUploading, white: false }); } @@ -225,9 +216,7 @@ const SlideFooter: React.FC = () => { if (!file) return; if (!file.type.startsWith("image/")) { - toast({ - title: "Please Upload An Image File", - }); + toast.error("Please Upload An Image File"); return; } @@ -247,10 +236,7 @@ const SlideFooter: React.FC = () => { })); } catch (error) { console.error("Error converting image:", error); - toast({ - title: "Error uploading image", - variant: "destructive", - }); + toast.error("Error uploading image"); } finally { setIsUploading({ ...isUploading, dark: false }); } @@ -277,10 +263,8 @@ const SlideFooter: React.FC = () => { const handleSheetClose = () => { if (isPropertyChanged) { - toast({ - title: "Unsaved Changes", + toast.error("Unsaved Changes", { description: "Please save changes before closing the editor", - variant: "destructive", }); return; } diff --git a/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx b/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx deleted file mode 100644 index 33e11dfb..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; -import { Markdown } from "tiptap-markdown"; -import Underline from "@tiptap/extension-underline"; -import { - Bold, - Italic, - Underline as UnderlinedIcon, - Strikethrough, - Code, -} from "lucide-react"; - - - -const TipTapEditor = ({ - content, -}: { - content: string; - -}) => { - - const editor = useEditor({ - extensions: [StarterKit, Markdown, Underline], - content: content, - editorProps: { - attributes: { - class: "outline-none transition-all duration-200", - }, - }, - immediatelyRender: false, - }); - - return ( -
- -
- - - - - -
-
- - { - const markdown = editor?.storage.markdown.getMarkdown(); - console.log("🔍 markdown", markdown); - }} - - editor={editor} - /> -
- ); -}; - -export default TipTapEditor; diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx index 5482b702..0fd75f3a 100644 --- a/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx @@ -107,6 +107,7 @@ const TiptapTextReplacer: React.FC = ({ const root = ReactDOM.createRoot(tiptapContainer); root.render( { if (dataPath && onContentChange) { diff --git a/servers/nextjs/app/(presentation-generator)/components/TypeWriter.tsx b/servers/nextjs/app/(presentation-generator)/components/TypeWriter.tsx deleted file mode 100644 index a9b8acd9..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/TypeWriter.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTypewriter } from "@/hooks/useTypeWriter"; - -const Typewriter = ({ text, speed }: { text: string; speed: number }) => { - const { displayText, isCursorVisible } = useTypewriter(text, speed, true); - - return ( -

- {displayText} - {isCursorVisible && |} -

- ); -}; - -export default Typewriter; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx deleted file mode 100644 index 687b6306..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState, useEffect } from "react"; -import ChartEditor from "../ChartEditor"; -import { StoreChartData } from "../../utils/chartDataTransforms"; -import { useDispatch, useSelector } from "react-redux"; -import { - ChartSettings, - updateSlideChart, - updateSlideChartSettings, -} from "@/store/slices/presentationGeneration"; -import { renderChart } from "../slide_config"; -import { RootState } from "@/store/store"; - -interface AllChartProps { - chartData: StoreChartData; - slideIndex: number; -} - -const AllChart = ({ - chartData: initialChartData, - slideIndex, -}: AllChartProps) => { - const dispatch = useDispatch(); - const [localChartData, setLocalChartData] = - useState(initialChartData); - const [isEditorOpen, setIsEditorOpen] = useState(false); - const { currentColors } = useSelector((state: RootState) => state.theme); - - // Use chart settings from the Redux store - const chartSettings = useSelector((state: RootState) => { - const slide = - state.presentationGeneration?.presentationData?.slides[slideIndex]; - - - const style = slide?.content.graph.style || {}; - return Object.keys( - style === null || style === undefined ? {} : (style as ChartSettings) - ).length > 0 - ? (style as ChartSettings) - : { - showLegend: false, - showGrid: false, - showAxisLabel: true, - showDataLabel: true, - dataLabel: { - dataLabelPosition: - slide?.content.graph.type === "pie" - ? ("Outside" as const) - : ("Inside" as const), - dataLabelAlignment: "Center" as const, - }, - }; - }); - - useEffect(() => { - setLocalChartData(initialChartData); - }, [initialChartData]); - - const handleChartClick = () => { - setIsEditorOpen(true); - }; - - const onChartDataChange = (newData: StoreChartData) => { - dispatch(updateSlideChart({ index: slideIndex, chart: newData })); - setLocalChartData(newData); - }; - - const onChartSettingsChange = (newSettings: ChartSettings) => { - dispatch( - updateSlideChartSettings({ - index: slideIndex, - chartSettings: newSettings, - }) - ); - }; - - return ( - <> -
- {renderChart(localChartData, false, currentColors ?? [], chartSettings)} - {/* {localChartData.type} */} -
- - {localChartData && ( - setIsEditorOpen(false)} - chartData={localChartData} - onChartDataChange={onChartDataChange} - /> - )} - - ); -}; - -export default AllChart; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx deleted file mode 100644 index c1ee87b2..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { Trash2 } from 'lucide-react'; -import React from 'react' - -interface NewSlideProps { - onSelectLayout: (type: number) => void; - setShowNewSlideSelection: (show: boolean) => void; -} - -const LayoutPreview = ({ type }: { type: string }) => { - switch (type) { - case 'type1': - return ( -
-
-
-
-
-
-

image

-
-
- ) - case 'type2': - return ( -
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
- ))} -
-
- ) - case 'type4': - return ( -
-
-
- {[1, 2, 3].map((i) => ( -
-
-

image

-
-
-
- ))} -
-
- ) - case 'type5': - return ( -
-
-
-
-

chart

-
-
-
-
- ) - case 'type6': - return ( -
-
-
-
-
-
- {[1, 2].map((i) => ( -
-
-
-
-
-
- ))} -
-
- ) - case 'type7': - return ( -
-
-
- {[1, 2, 3].map((i) => ( -
-
-

Icon

-
-
-
-
- ))} -
-
- ) - case 'type8': - return ( -
-
-
-
-
-
- {[1, 2].map((i) => ( -
-
-

Icon

-
-
-
-
-
- ))} -
-
- ) - case 'type9': - return ( -
-
-
-
-

chart

-
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
- ))} -
-
- ) - - default: - return null - } -} - -const NewSlide = ({ onSelectLayout, setShowNewSlideSelection }: NewSlideProps) => { - return ( -
-
- -

Select a Slide Layout

- setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' /> -
-
- {['type1', 'type2', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9'].map((type) => ( -
onSelectLayout(parseInt(type.replace('type', '')))} - > -
-
-

Layout {type.replace('type', '')}

-
-
- -
-
-
- ))} -
-
- ) -} - -export default NewSlide diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type1Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type1Layout.tsx deleted file mode 100644 index 9570dbe5..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type1Layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import ImageEditor from "../ImageEditor"; -import { useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import SlideFooter from "./SlideFooter"; - -interface Type1LayoutProps { - title: string; - description: string; - slideId: string | null; - images: string[]; - slideIndex: number; - image_prompts?: string[] | null; - properties?: null | any; -} -const Type1Layout = ({ - title, - description, - images, - slideId, - slideIndex, - image_prompts, - properties, -}: Type1LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - return ( -
-
-
- - -
- - - - {/* {imagePosition === 'left' ? ( - <> - - - - ) : ( - <> - - - - )} */} -
- -
- ); -}; - -export default Type1Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type2Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type2Layout.tsx deleted file mode 100644 index aba6a0db..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type2Layout.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { MoreVertical, Plus } from "lucide-react"; -import ElementMenu from "../ElementMenu"; -import { useSelector } from "react-redux"; -import { numberTranslations } from "../../utils/others"; -import { RootState } from "@/store/store"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type2LayoutProps { - title: string; - body: Array<{ - heading: string; - description: string; - }>; - slideId: string | null; - - slideIndex: number; - language: string; - design_index: number; -} - -const Type2Layout = ({ - title, - body, - slideId, - slideIndex, - design_index, - language, -}: Type2LayoutProps) => { - const { currentColors } = useSelector( - (state: RootState) => state.theme - ); - const { handleAddItem, handleDeleteItem, handleVariantChange } = - useSlideOperations(slideIndex); - - const onAddItem = () => { - if (body.length < 4) { - handleAddItem({ item: { heading: "", description: "" } }); - } - }; - - const onDeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - } - }; - - const VariantMenu = () => ( - - - - - - handleVariantChange({ variant: 1 })} - className={`px-3 py-2 cursor-pointer ${design_index === 1 ? "bg-blue-50" : "" - }`} - > - Default Layout - - handleVariantChange({ variant: 2 })} - className={`px-3 py-2 cursor-pointer ${design_index === 2 ? "bg-blue-50" : "" - }`} - > - Numbered Layout - - handleVariantChange({ variant: 3 })} - className={`px-3 py-2 cursor-pointer ${design_index === 3 ? "bg-blue-50" : "" - }`} - > - Timeline Layout - - - - ); - - const isGridLayout = body.length >= 4; - - const renderContent = () => { - if (design_index === 3) { - return ( -
-
- - {/* Three Dots Icon */} - - - {/* Plus Icon */} - - {/* Timeline Header with Numbers and Line */} -
- {/* Horizontal Line */} -
- - {/* Timeline Numbers */} - {body.map((_, index) => ( -
- - {numberTranslations[language][index || 0]} - -
- ))} -
- - {/* Timeline Content */} -
- {body.map((item, index) => ( -
- -
- - -
-
- ))} -
-
- ); - } - if (isGridLayout) { - return ( -
- {/* Hover Border and Icons for entire layout */} -
- - {/* Three Dots Icon */} - - - {body.map((item, index) => ( -
-
- {design_index === 2 && ( -
- { - numberTranslations[ - language as keyof typeof numberTranslations - ][index] - } -
- )} - -
- - - -
-
-
- ))} -
- ); - } - - // Horizontal layout for 2-3 items - return ( -
- {/* Hover Border and Icons for entire layout */} -
- - {/* Three Dots Icon */} - - - {/* Plus Icon */} - {body.length < 4 && ( - - )} - {body.map((item, index) => ( -
- - - {design_index === 2 && ( -
- { - numberTranslations[ - language as keyof typeof numberTranslations - ][index] - } -
- )} -
- - - -
-
- ))} -
- ); - }; - - return ( -
-
- -
- - {renderContent()} - -
- ); -}; - -export default Type2Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type4Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type4Layout.tsx deleted file mode 100644 index 8fa91f26..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type4Layout.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import ImageEditor from "../ImageEditor"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import ElementMenu from "../ElementMenu"; -import { Plus } from "lucide-react"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type4LayoutProps { - title: string; - body: Array<{ - heading: string; - description: string; - }>; - slideId: string | null; - images: string[]; - slideIndex: number; - image_prompts?: string[] | null; - properties?: null | any; -} - -const Type4Layout = ({ - title, - body, - slideId, - images, - slideIndex, - image_prompts, - properties, -}: Type4LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - const { - handleAddItem, - handleDeleteItem, - handleImageChange, - handleDeleteImage, - } = useSlideOperations(slideIndex); - - const AddItem = () => { - if (body.length < 3) { - handleImageChange({ imageUrl: "", imageIndex: slideIndex }); - handleAddItem({ - item: { heading: "Enter Heading", description: "Enter Description" }, - }); - } - }; - const DeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - handleDeleteImage({ imageIndex: index }); - } - }; - const getGridCols = (length: number) => { - switch (length) { - case 1: return 'lg:grid-cols-1'; - case 2: return 'lg:grid-cols-2'; - case 3: return 'lg:grid-cols-3'; - case 4: return 'lg:grid-cols-4'; - // Add more cases as needed - default: return 'lg:grid-cols-1'; - } - } - - return ( -
-
- -
- -
-
- - {body.map((item, index) => ( -
- - - -
- - -
-
- ))} -
- -
- ); -}; - -export default Type4Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type5Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type5Layout.tsx deleted file mode 100644 index 6d2b2b49..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type5Layout.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import { RootState } from "@/store/store"; -import { useSelector } from "react-redux"; -import AllChart from "./AllChart"; -import SlideFooter from "./SlideFooter"; - -interface Type5LayoutProps { - title: string; - description: string; - slideId: string | null; - chartComponent?: React.ReactNode; - graphData?: any; - slideIndex: number; - isFullSizeGraph?: boolean; -} - -const Type5Layout = ({ - title, - description, - slideId, - chartComponent, - graphData, - slideIndex, - isFullSizeGraph = false, -}: Type5LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - - return ( -
- -
-
- -
-
- -
-
- -
- ); -}; - -export default Type5Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type6Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type6Layout.tsx deleted file mode 100644 index c5f6dc01..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type6Layout.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import { Plus } from "lucide-react"; -import ElementMenu from "../ElementMenu"; -import { useDispatch, useSelector } from "react-redux"; -import { numberTranslations } from "../../utils/others"; -import { RootState } from "@/store/store"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type6LayoutProps { - title: string; - description: string; - body: Array<{ - heading: string; - description: string; - }>; - slideId: string | null; - slideIndex: number; - language: string; -} - -const Type6Layout = ({ - title, - description, - body, - slideId, - slideIndex, - language, -}: Type6LayoutProps) => { - const dispatch = useDispatch(); - const { currentColors } = useSelector((state: RootState) => state.theme); - const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex); - - const AddItem = () => { - if (body.length < 3) { - handleAddItem({ item: { heading: "", description: "" } }); - } - }; - const DeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - } - }; - return ( -
-
- {/* Left section - Description */} -
- - - -
- - {/* Right section - Numbered items */} -
-
- -
- {body.map((item, index) => ( -
- -
-
- {numberTranslations[language][index || 0]} -
-
- - -
-
-
- ))} -
-
-
- -
- ); -}; - -export default Type6Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type7Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type7Layout.tsx deleted file mode 100644 index f1831be6..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type7Layout.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import IconsEditor from "../IconsEditor"; -import { Plus } from "lucide-react"; -import ElementMenu from "../ElementMenu"; -import { useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type2LayoutProps { - title: string; - body: Array<{ - heading: string; - description: string; - }>; - icons: string[]; - slideIndex: number; - slideId: string | null; - icon_queries?: Array<{ queries: string[] }> | null; -} - -const Type7Layout = ({ - title, - body, - icons, - slideIndex, - slideId, - icon_queries, -}: Type2LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex); - - const AddItem = () => { - if (body.length < 4) { - handleAddItem({ item: { heading: "", description: "" } }); - } - }; - const DeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - } - }; - const getGridCols = (length: number) => { - switch (length) { - case 1: return 'lg:grid-cols-1'; - case 2: return 'lg:grid-cols-2'; - case 3: return 'lg:grid-cols-3'; - case 4: return 'lg:grid-cols-4'; - case 5: return 'lg:grid-cols-5'; - case 6: return 'lg:grid-cols-6'; - case 7: return 'lg:grid-cols-7'; - // Add more cases as needed - default: return 'lg:grid-cols-1'; - } - } - - const isGridLayout = body.length >= 4; - - const renderContent = () => { - if (isGridLayout) { - return ( -
4 ? 'md:grid-cols-3' : 'md:grid-cols-2'} gap-4 sm:gap-6 lg:gap-8 mt-4 lg:mt-12 w-full relative group`} - > -
- - - {body.map((item, index) => ( -
- -
-
- -
-
- - -
-
-
- ))} -
- ); - } - - - // Horizontal layout for 2-3 items - return ( -
-
- - - - {body.map((item, index) => ( -
- - -
- - -
-
- ))} -
- ); - }; - - return ( -
-
- -
- {renderContent()} - -
- ); -}; - -export default Type7Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type8Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type8Layout.tsx deleted file mode 100644 index b1af87b3..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type8Layout.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import IconsEditor from "../IconsEditor"; -import { Plus } from "lucide-react"; -import ElementMenu from "../ElementMenu"; -import { useSelector } from "react-redux"; -import { RootState } from "@/store/store"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type6LayoutProps { - title: string; - description: string; - body: Array<{ - heading: string; - description: string; - }>; - icons: string[]; - slideId: string | null; - slideIndex: number; - icon_queries?: Array<{ queries: string[] }> | null; -} - -const Type8Layout = ({ - title, - description, - body, - icons, - slideIndex, - slideId, - icon_queries, -}: Type6LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex); - - const AddItem = () => { - if (body.length < 3) { - handleAddItem({ item: { heading: "", description: "" } }); - } - }; - const DeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - } - }; - - return ( -
-
- {/* Left section - Description */} -
- - - -
- - {/* Right section - Numbered items */} -
-
- - - -
- {body && body.length > 0 && body.length === 2 - ? body.map((item, index) => ( -
- - - -
- - -
-
- )) - : body.map((item, index) => ( -
- -
-
- -
-
- - -
-
-
- ))} -
-
-
- -
- ); -}; - -export default Type8Layout; diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type9Layout.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type9Layout.tsx deleted file mode 100644 index b42706ef..00000000 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/Type9Layout.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from "react"; -import EditableText from "../EditableText"; -import { Plus } from "lucide-react"; -import ElementMenu from "../ElementMenu"; -import { useSelector } from "react-redux"; -import { numberTranslations } from "../../utils/others"; -import { RootState } from "@/store/store"; -import AllChart from "./AllChart"; -import { useSlideOperations } from "../../hooks/use-slide-operations"; -import SlideFooter from "./SlideFooter"; - -interface Type9LayoutProps { - title: string; - body: Array<{ - heading: string; - description: string; - }>; - graphData?: any; - slideId: string | null; - language: string; - slideIndex: number; -} - -const Type9Layout = ({ - title, - body, - graphData, - slideId, - slideIndex, - language, -}: Type9LayoutProps) => { - const { currentColors } = useSelector((state: RootState) => state.theme); - const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex); - const AddItem = () => { - if (body.length < 3) { - handleAddItem({ item: { heading: "", description: "" } }); - } - }; - const DeleteItem = (index: number) => { - if (body.length > 2) { - handleDeleteItem({ itemIndex: index }); - } - }; - return ( -
-
- {/* Left section - Chart */} -
- -
-
- -
-
-
- - {/* Right section - Numbered items */} - -
-
- -
- {body.length > 0 && - body.map((item, index) => ( -
- -
-
- {numberTranslations[language][index || 0]} -
- -
- - -
-
-
- ))} -
-
-
- -
- ); -}; - -export default Type9Layout; diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index 77550392..20f2c292 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -1,7 +1,7 @@ "use client"; import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import dynamic from 'next/dynamic'; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import * as z from 'zod'; export interface LayoutInfo { @@ -98,8 +98,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`); if (!module.default) { - toast({ - title: `${file} has no default export`, + toast.error(`${file} has no default export`, { description: 'Please ensure the layout file exports a default component', }); console.warn(`❌ ${file} has no default export`); @@ -107,8 +106,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) } if (!module.Schema) { - toast({ - title: `${file} has no Schema export`, + toast.error(`${file} has no Schema export`, { description: 'Please ensure the layout file exports a Schema', }); console.warn(`❌ ${file} has no Schema export`); diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index 0480be69..7b106d23 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -13,7 +13,6 @@ "use client"; -import styles from "../styles/main.module.css"; import { useEffect, useState, useRef, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { OverlayLoader } from "@/components/ui/overlay-loader"; @@ -23,13 +22,12 @@ import { useDispatch, useSelector } from "react-redux"; import { useRouter } from "next/navigation"; import { RootState } from "@/store/store"; import { Button } from "@/components/ui/button"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import MarkdownRenderer from "./MarkdownRenderer"; import { getIconFromFile } from "../../utils/others"; import { ChevronRight, PanelRightOpen, X } from "lucide-react"; import ToolTip from "@/components/ToolTip"; import Header from "@/app/dashboard/components/Header"; -import { useLayout } from "../../context/LayoutContext"; // Types interface LoadingState { @@ -124,11 +122,7 @@ const DocumentsPreviewPage: React.FC = () => { }); } catch (error) { console.error('Error reading files:', error); - toast({ - title: "Error", - description: "Failed to read document content", - variant: "destructive", - }); + toast.error("Failed to read document content"); } setDownloadingDocuments([]); } @@ -157,11 +151,7 @@ const DocumentsPreviewPage: React.FC = () => { router.push("/outline"); } catch (error) { console.error("Error in presentation creation:", error); - toast({ - title: "Error in presentation creation.", - description: "Please try again.", - variant: "destructive", - }); + toast.error("Error in presentation creation. Please try again."); setShowLoading({ message: "Error in presentation creation.", show: true, @@ -214,7 +204,7 @@ const DocumentsPreviewPage: React.FC = () => { if (!isOpen) return null; return ( -
setIsOpen(false)} @@ -230,7 +220,7 @@ const DocumentsPreviewPage: React.FC = () => {
updateSelectedDocument(key)} - className={`${selectedDocument === key ? styles.selected_border : "" + className={`${selectedDocument === key ? 'border border-blue-500' : "" } flex p-2 rounded-sm gap-2 items-center cursor-pointer`} > { }; return ( -
+
{ - const dispatch = useDispatch(); - - const handleAddItem = ({ item }: { item: any }) => { - dispatch(addSlideBodyItem({ index: slideIndex, item })); - }; - - const handleDeleteItem = ({ itemIndex }: { itemIndex: number }) => { - dispatch(deleteSlideBodyItem({ index: slideIndex, itemIdx: itemIndex })); - }; - - const handleVariantChange = ({ variant }: { variant: number }) => { - dispatch(updateSlideVariant({ index: slideIndex, variant })); - }; - - const handleImageChange = ({ - imageUrl, - imageIndex, - }: { - imageUrl: string; - imageIndex: number; - }) => { - dispatch( - updateSlideImage({ - index: slideIndex, - imageIdx: imageIndex, - image: imageUrl, - }) - ); - }; - const handleDeleteImage = ({ imageIndex }: { imageIndex: number }) => { - dispatch(deleteSlideImage({ index: slideIndex, imageIdx: imageIndex })); - }; - - // Add other common slide operations here - - return { - handleAddItem, - handleDeleteItem, - handleVariantChange, - handleImageChange, - handleDeleteImage, - }; -}; diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx index 29c5e29a..8208530d 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx @@ -35,7 +35,7 @@ export const useGroupLayouts = () => { // Render slide content with group validation, automatic Tiptap text editing, and editable images/icons const renderSlideContent = useMemo(() => { - return (slide: any, isEditMode: boolean = true) => { + return (slide: any, isEditMode: boolean) => { const Layout = getGroupLayout(slide.layout, slide.layout_group); if (!Layout) { return ( @@ -55,6 +55,7 @@ export const useGroupLayouts = () => { isEditMode={isEditMode} > { eventSource.close(); } catch (error) { console.error("Error parsing accumulated chunks:", error); - toast({ - title: "Error", - description: "Failed to parse presentation data", - variant: "destructive", - }); + toast.error("Failed to parse presentation data"); eventSource.close(); } accumulatedChunks = ""; @@ -76,19 +72,11 @@ export const useOutlineStreaming = (presentationId: string | null) => { eventSource.onerror = () => { setStreamState({ isStreaming: false, isLoading: false }); eventSource.close(); - toast({ - title: "Connection Error", - description: "Failed to connect to the server. Please try again.", - variant: "destructive", - }); + toast.error("Failed to connect to the server. Please try again."); }; } catch (error) { setStreamState({ isStreaming: false, isLoading: false }); - toast({ - title: "Error", - description: "Failed to initialize connection", - variant: "destructive", - }); + toast.error("Failed to initialize connection"); } }; diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts index 79a492ed..b8ee619e 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from "react"; import { useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { useLayout } from "../../context/LayoutContext"; @@ -27,19 +27,15 @@ export const usePresentationGeneration = ( const validateInputs = useCallback(() => { if (!outlines || outlines.length === 0) { - toast({ - title: "No Outlines", + toast.error("No Outlines", { description: "Please wait for outlines to load before generating presentation", - variant: "destructive", }); return false; } if (!selectedLayoutGroup) { - toast({ - title: "Select Layout Group", + toast.error("Select Layout Group", { description: "Please select a layout group before generating presentation", - variant: "destructive", }); return false; } @@ -101,10 +97,8 @@ export const usePresentationGeneration = ( } } catch (error) { console.error("Error in data generation", error); - toast({ - title: "Generation Error", + toast.error("Generation Error", { description: "Failed to generate presentation. Please try again.", - variant: "destructive", }); } finally { setLoadingState(DEFAULT_LOADING_STATE); diff --git a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx index e1cb7b4d..9c63e419 100644 --- a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx @@ -8,7 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { DashboardApi } from "@/app/dashboard/api/dashboard"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; @@ -40,11 +40,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { setContentLoading(false); } catch (error) { setError(true); - toast({ - title: "Error", - description: "Failed to load presentation", - variant: "destructive", - }); + toast.error("Failed to load presentation"); console.error("Error fetching user slides:", error); setContentLoading(false); } diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx index 2518f450..12ec7a05 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx @@ -1,12 +1,9 @@ "use client"; import { Button } from "@/components/ui/button"; import { - Menu, - Palette, SquareArrowOutUpRight, Play, Loader2, - ExternalLink, } from "lucide-react"; import React, { useState } from "react"; import Wrapper from "@/components/Wrapper"; @@ -16,38 +13,20 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import UserAccount from "../../components/UserAccount"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { OverlayLoader } from "@/components/ui/overlay-loader"; -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { useDispatch, useSelector } from "react-redux"; import Link from "next/link"; -import { ThemeType } from "@/app/(presentation-generator)/upload/type"; -import { - setTheme, - setThemeColors, - defaultColors, - serverColors, -} from "../../store/themeSlice"; -import CustomThemeSettings from "../../components/CustomThemeSettings"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { RootState } from "@/store/store"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; -import ThemeSelector from "./ThemeSelector"; -import Modal from "./Modal"; import Announcement from "@/components/Announcement"; -import { getFontLink, getStaticFileUrl } from "../../utils/others"; +import { getStaticFileUrl } from "../../utils/others"; import { PptxPresentationModel } from "@/types/pptx_models"; +import HeaderNav from "../../components/HeaderNab"; const Header = ({ @@ -61,66 +40,10 @@ const Header = ({ const [showLoader, setShowLoader] = useState(false); const router = useRouter(); - const [showCustomThemeModal, setShowCustomThemeModal] = useState(false); - const [showDownloadModal, setShowDownloadModal] = useState(false); - const [downloadPath, setDownloadPath] = useState(""); - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); + const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); - const dispatch = useDispatch(); - const handleThemeSelect = async (value: string) => { - if (isStreaming) return; - if (value === "custom") { - setShowCustomThemeModal(true); - return; - } else { - const themeType = value as ThemeType; - const themeColors = serverColors[themeType] || defaultColors[themeType]; - - if (themeColors) { - try { - // Update UI - dispatch(setTheme(themeType)); - dispatch(setThemeColors({ ...themeColors, theme: themeType })); - // Set CSS variables - const root = document.documentElement; - root.style.setProperty( - `--${themeType}-slide-bg`, - themeColors.slideBg - ); - root.style.setProperty( - `--${themeType}-slide-title`, - themeColors.slideTitle - ); - root.style.setProperty( - `--${themeType}-slide-heading`, - themeColors.slideHeading - ); - root.style.setProperty( - `--${themeType}-slide-description`, - themeColors.slideDescription - ); - root.style.setProperty( - `--${themeType}-slide-box`, - themeColors.slideBox - ); - - - } catch (error) { - console.error("Failed to update theme:", error); - toast({ - title: "Error updating theme", - description: - "Failed to update the presentation theme. Please try again.", - variant: "destructive", - }); - } - } - } - }; const get_presentation_pptx_model = async (id: string): Promise => { const response = await fetch(`/api/presentation_to_pptx_model?id=${id}`); @@ -148,11 +71,9 @@ const Header = ({ } catch (error) { console.error("Export failed:", error); setShowLoader(false); - toast({ - title: "Having trouble exporting!", + toast.error("Having trouble exporting!", { description: "We are having trouble exporting your presentation. Please try again.", - variant: "default", }); } finally { setShowLoader(false); @@ -176,19 +97,16 @@ const Header = ({ if (response.ok) { const { path: pdfPath } = await response.json(); - const staticFileUrl = getStaticFileUrl(pdfPath); - window.open(staticFileUrl, '_blank'); + window.open(pdfPath, '_blank'); } else { throw new Error("Failed to export PDF"); } } catch (err) { console.error(err); - toast({ - title: "Having trouble exporting!", + toast.error("Having trouble exporting!", { description: "We are having trouble exporting your presentation. Please try again.", - variant: "default", }); } finally { setShowLoader(false); @@ -212,15 +130,8 @@ const Header = ({ pptx export Export as PPTX - {/*
- -
*/} -

- Font Used: - - {getFontLink(currentColors.fontFamily).name || ''} - -

+ +
); @@ -282,81 +193,19 @@ const Header = ({ {isStreaming && ( )} - - {/* Custom Theme Modal */} - setShowCustomThemeModal(false)} - title="Custom Theme Colors" - > - setShowCustomThemeModal(false)} - presentationId={presentation_id} - /> - + + - +
{/* Mobile Menu */}
- - - - - - -
- - -
-
-
+ +
- {/* Download Modal */} - setShowDownloadModal(false)} - title="File Downloaded" - > -
-

Your file is saved at:

-

{downloadPath}

-
-
+
); }; diff --git a/servers/nextjs/app/(presentation-generator)/components/LoadingState.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/LoadingState.tsx similarity index 100% rename from servers/nextjs/app/(presentation-generator)/components/LoadingState.tsx rename to servers/nextjs/app/(presentation-generator)/presentation/components/LoadingState.tsx diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index a6bb0976..dfff071b 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -6,7 +6,6 @@ import { Skeleton } from "@/components/ui/skeleton"; import PresentationMode from "../../components/PresentationMode"; import SidePanel from "../components/SidePanel"; import SlideContent from "../components/SlideContent"; -import LoadingState from "../../components/LoadingState"; import Header from "../components/Header"; import { Button } from "@/components/ui/button"; import { AlertCircle, Loader2 } from "lucide-react"; @@ -18,6 +17,7 @@ import { useAutoSave } from "../hooks"; import { PresentationPageProps } from "../types"; +import LoadingState from "./LoadingState"; const PresentationPage: React.FC = ({ presentation_id }) => { // State management @@ -27,10 +27,7 @@ const PresentationPage: React.FC = ({ presentation_id }) const [error, setError] = useState(false); const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false); - // Redux state - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); + const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); @@ -43,7 +40,7 @@ const PresentationPage: React.FC = ({ presentation_id }) }); // Custom hooks - const { fetchUserSlides, handleDeleteSlide } = usePresentationData( + const { fetchUserSlides } = usePresentationData( presentation_id, setLoading, setError @@ -72,9 +69,7 @@ const PresentationPage: React.FC = ({ presentation_id }) fetchUserSlides ); - const onDeleteSlide = (index: number) => { - handleDeleteSlide(index, presentationData); - }; + const onSlideChange = (newSlide: number) => { handleSlideChange(newSlide, presentationData); @@ -86,7 +81,7 @@ const PresentationPage: React.FC = ({ presentation_id }) = ({ presentation_id })
@@ -172,7 +167,7 @@ const PresentationPage: React.FC = ({ presentation_id }) slide={slide} index={index} presentationId={presentation_id} - onDeleteSlide={onDeleteSlide} + /> ))} diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index 16c57120..f14364ab 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -45,9 +45,7 @@ const SidePanel = ({ const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); + const dispatch = useDispatch(); // Use the centralized group layouts hook @@ -159,16 +157,10 @@ const SidePanel = ({ `} >
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index 5c33adf5..beda2448 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -8,28 +8,26 @@ import { } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; import { SendHorizontal } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import ToolTip from "@/components/ToolTip"; import { RootState } from "@/store/store"; import { useDispatch, useSelector } from "react-redux"; -import { addSlide, updateSlide } from "@/store/slices/presentationGeneration"; -import NewSlide from "../../components/slide_layouts/NewSlide"; -import { getEmptySlideContent } from "../../utils/NewSlideContent"; +import { deletePresentationSlide, updateSlide } from "@/store/slices/presentationGeneration"; import { useGroupLayouts } from "../../hooks/useGroupLayouts"; +import NewSlide from "../../components/NewSlide"; interface SlideContentProps { slide: any; index: number; presentationId: string; - onDeleteSlide: (index: number) => void; } const SlideContent = ({ slide, index, presentationId, - onDeleteSlide, + }: SlideContentProps) => { const dispatch = useDispatch(); const [isUpdating, setIsUpdating] = useState(false); @@ -47,11 +45,7 @@ const SlideContent = ({ ) as HTMLInputElement; const value = element?.value; if (!value?.trim()) { - toast({ - title: "Error", - description: "Please enter a prompt before submitting", - variant: "destructive", - }); + toast.error("Please enter a prompt before submitting"); return; } setIsUpdating(true); @@ -66,45 +60,25 @@ const SlideContent = ({ if (response) { console.log("response", response); dispatch(updateSlide({ index: slide.index, slide: response })); - toast({ - title: "Success", - description: "Slide updated successfully", - }); + toast.success("Slide updated successfully"); } } catch (error) { console.error("Error updating slide:", error); - toast({ - title: "Error", - description: "Failed to update slide. Please try again.", - variant: "destructive", - }); + toast.error("Failed to update slide. Please try again."); } finally { setIsUpdating(false); } }; - - const handleNewSlide = (type: number, index: number) => { - const newSlide: Slide = getEmptySlideContent( - type, - index + 1, - presentationData?.id! - ); - - dispatch(addSlide({ slide: newSlide, index: index + 1 })); - setShowNewSlideSelection(false); - - // Scroll to the newly added slide after a short delay to ensure it's rendered - setTimeout(() => { - const newSlideElement = document.getElementById(`slide-${newSlide.id}`); - if (newSlideElement) { - newSlideElement.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - }, 100); + const onDeleteSlide = async () => { + try { + dispatch(deletePresentationSlide(slide.index)); + } catch (error) { + console.error("Error deleting slide:", error); + } }; + + // Scroll to the new slide when streaming and new slides are being generated useEffect(() => { if ( @@ -139,16 +113,16 @@ const SlideContent = ({ {isStreaming && ( )} -
+
{/* render slides */} {loading ?
: slideContent} - {/* {!showNewSlideSelection && ( + {!showNewSlideSelection && (
- {!isStreaming && ( + {!isStreaming && !loading && (
setShowNewSlideSelection(true)} className=" bg-white shadow-md w-[80px] py-2 border hover:border-[#5141e5] duration-300 flex items-center justify-center rounded-lg cursor-pointer mx-auto" @@ -159,16 +133,18 @@ const SlideContent = ({
)} - {showNewSlideSelection && ( + {showNewSlideSelection && !loading && ( handleNewSlide(type, slide.index)} + index={index} + group={slide.layout_group} setShowNewSlideSelection={setShowNewSlideSelection} + presentationId={presentationId} /> - )} */} - {!isStreaming && ( + )} + {!isStreaming && !loading && (
onDeleteSlide(slide.index)} + onClick={onDeleteSlide} className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform" > diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx deleted file mode 100644 index fcbdb4f3..00000000 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React from "react"; - -const ThemeSelector = ({ - onSelect, - selectedTheme, -}: { - onSelect: (theme: string) => void; - selectedTheme: string; -}) => { - return ( -
- - - - - - - - - -
- ); -}; - -export default ThemeSelector; - -const ThemePreview = ({ - theme, - color, - isSelected, -}: { - theme: string; - color: string; - isSelected: boolean; -}) => ( -
-
-
-
-
-
-
-
- - {theme} - -
-); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx index 596e2405..963b17cc 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx @@ -13,7 +13,7 @@ export const useAutoSave = ({ debounceMs = 2000, enabled = true, }: UseAutoSaveOptions = {}) => { - const { presentationData } = useSelector( + const { presentationData, isStreaming, isLoading } = useSelector( (state: RootState) => state.presentationGeneration ); @@ -61,7 +61,7 @@ export const useAutoSave = ({ // Effect to trigger auto-save when presentation data changes useEffect(() => { - if (!enabled || !presentationData) return; + if (!enabled || !presentationData || isStreaming || isLoading) return; // Trigger debounced save debouncedSave(presentationData); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts index 4c7681f9..90d60c2c 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts @@ -1,9 +1,8 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from 'react'; import { useDispatch } from "react-redux"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { DashboardApi } from "@/app/dashboard/api/dashboard"; -import { PresentationGenerationApi } from "../../services/api/presentation-generation"; -import { setPresentationData, deletePresentationSlide } from "@/store/slices/presentationGeneration"; +import { setPresentationData } from "@/store/slices/presentationGeneration"; export const usePresentationData = ( presentationId: string, @@ -12,7 +11,6 @@ export const usePresentationData = ( ) => { const dispatch = useDispatch(); - const fetchUserSlides = useCallback(async () => { try { const data = await DashboardApi.getPresentation(presentationId); @@ -22,35 +20,17 @@ export const usePresentationData = ( } } catch (error) { setError(true); - toast({ - title: "Error", - description: "Failed to load presentation", - variant: "destructive", - }); + toast.error("Failed to load presentation"); console.error("Error fetching user slides:", error); setLoading(false); } }, [presentationId, dispatch, setLoading, setError]); - const handleDeleteSlide = useCallback(async (index: number, presentationData: any) => { - dispatch(deletePresentationSlide(index)); - try { - await PresentationGenerationApi.deleteSlide( - presentationId, - presentationData?.slides[index].id! - ); - } catch (error) { - console.error("Error deleting slide:", error); - toast({ - title: "Error", - description: "Failed to delete slide", - variant: "destructive", - }); - } - }, [presentationId, dispatch]); + useEffect(() => { + fetchUserSlides(); + }, [fetchUserSlides]); return { fetchUserSlides, - handleDeleteSlide, }; }; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts index 66b7c1b1..78ccea88 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts @@ -1,7 +1,8 @@ import { useEffect, useRef } from "react"; -import { useDispatch } from "react-redux"; -import { setPresentationData, setStreaming } from "@/store/slices/presentationGeneration"; +import { useDispatch, useSelector } from "react-redux"; +import { clearPresentationData, setPresentationData, setStreaming } from "@/store/slices/presentationGeneration"; import { jsonrepair } from "jsonrepair"; +import { RootState } from "@/store/store"; export const usePresentationStreaming = ( presentationId: string, @@ -10,6 +11,8 @@ export const usePresentationStreaming = ( setError: (error: boolean) => void, fetchUserSlides: () => void ) => { + const { presentationData } = useSelector((state: RootState) => state.presentationGeneration); + const dispatch = useDispatch(); const previousSlidesLength = useRef(0); @@ -19,6 +22,7 @@ export const usePresentationStreaming = ( const initializeStream = async () => { dispatch(setStreaming(true)); + dispatch(clearPresentationData()); eventSource = new EventSource( `/api/v1/ppt/presentation/stream?presentation_id=${presentationId}` @@ -98,7 +102,9 @@ export const usePresentationStreaming = ( if (stream) { initializeStream(); } else { - fetchUserSlides(); + if(!presentationData || presentationData.slides.length === 0){ + fetchUserSlides(); + } } return () => { diff --git a/servers/nextjs/app/(presentation-generator)/store/themeSlice.ts b/servers/nextjs/app/(presentation-generator)/store/themeSlice.ts deleted file mode 100644 index d848cf05..00000000 --- a/servers/nextjs/app/(presentation-generator)/store/themeSlice.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { ThemeType } from "@/app/(presentation-generator)/upload/type"; -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -export const defaultColors = { - light: { - background: "#c8c7c9", - slideBg: "#F2F2F2", - slideTitle: "#000000", - slideHeading: "#1a1a1a", - slideDescription: "#333333", - slideBox: "#ffffff", - iconBg: "#1F1F2D", - chartColors: ["#1F1F2D", "#3F3F5D", "#62628E", "#8F8FB2", "#C0C0D3"], - fontFamily: "var(--font-inter)", - }, - dark: { - background: "#000000", - slideBg: "#1E1E1E", - slideTitle: "#ffffff", - slideHeading: "#f5f5f5", - slideDescription: "#e0e0e0", - slideBox: "#2d2d2d", - iconBg: "#5E8CF0", - chartColors: ["#5E8CF0", "#8800ff", "#b200ff", "#d700ff", "#ef00ff"], - fontFamily: "var(--font-inter)", - }, - faint_yellow: { - background: "#d9cebc", - slideBg: "#F8F4E8", - slideTitle: "#2C1810", - slideHeading: "#4A3728", - slideDescription: "#665E57", - slideBox: "#FFFFFF", - iconBg: "#281810", - chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"], - fontFamily: "var(--font-inter)", - }, - custom: { - background: "#63ceff", - slideBg: "#F4F4F4", - slideTitle: "#1A1A1A", - slideHeading: "#2D2D2D", - slideDescription: "#4A4A4A", - slideBox: "#d8c6c6", - iconBg: "#281810", - chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"], - fontFamily: "var(--font-inter)", - }, - cream: { - background: "#DDCFBB", - slideBg: "#F9F6F0", - slideTitle: "#484237", - slideHeading: "#484237", - slideDescription: "#595F6C", - slideBox: "#EEE9DD", - iconBg: "#A6825B", - chartColors: ["#765939", "#A6825B", "#B89B7C", "#CAB49D", "#DBCDBD"], - fontFamily: "var(--font-fraunces)", - }, - royal_blue: { - background: "#010103", - slideBg: "#091433", - slideTitle: "#ffffff", - slideHeading: "#ffffff", - slideDescription: "#E6E6E6", - slideBox: "#29136C", - iconBg: "#5E8CF0", - chartColors: ["#5E8CF0", "#496CEB", "#f051b5", "#F7A8FF", "#FCD8FF"], - fontFamily: "var(--font-instrument-sans)", - }, - light_red: { - background: "#F8E9E8", - slideBg: "#FFFAFA", - slideTitle: "#181D27", - slideHeading: "#252B37", - slideDescription: "#595F6C", - slideBox: "#F3E8E8", - iconBg: "#F0695F", - chartColors: [ - "#F0695F", - "#450808", - "#8F1010", - "#C1392F", - "#EC5555", - "#F49E9E", - ], - fontFamily: "var(--font-montserrat)", - }, - dark_pink: { - background: "#F3AEED", - slideBg: "#F9E8FF", - slideTitle: "#261827", - slideHeading: "#252B37", - slideDescription: "#6A596C", - slideBox: "#F0D4F7", - iconBg: "#D02CE5", - chartColors: ["#D02CE5", "#B414C9", "#6E1886", "#A724CC", "#C65FE3"], - fontFamily: "var(--font-inria-serif)", - }, -}; - -// Store the server-provided colors -export const serverColors: { [key in ThemeType]?: ThemeColors } = {}; - -export interface ThemeColors { - background: string; - slideBg: string; - slideTitle: string; - slideHeading: string; - slideDescription: string; - slideBox: string; - iconBg: string; - chartColors: string[]; - fontFamily: string; -} - -interface ThemeState { - currentTheme: ThemeType; - currentColors: ThemeColors; - isLoading: boolean; -} - -const initialState: ThemeState = { - currentTheme: ThemeType.Light, - currentColors: defaultColors.light, - isLoading: false, -}; - -const themeSlice = createSlice({ - name: "theme", - initialState, - reducers: { - setTheme: (state, action: PayloadAction) => { - state.currentTheme = action.payload; - // Use server colors if available, otherwise fall back to default - state.currentColors = - serverColors[action.payload] || defaultColors[action.payload]; - }, - setThemeColors: ( - state, - action: PayloadAction & { theme: ThemeType }> - ) => { - const newColors = { ...state.currentColors, ...action.payload }; - state.currentColors = newColors; - state.currentTheme = action.payload.theme; - // Store the colors for this theme - serverColors[action.payload.theme] = newColors; - }, - setLoadingState: (state, action: PayloadAction) => { - state.isLoading = action.payload; - }, - loadSavedTheme: (state, action: PayloadAction) => { - if (action.payload.name === "custom") { - state.currentTheme = ThemeType.Custom; - state.currentColors = action.payload.colors; - serverColors.custom = action.payload.colors; - } - }, - }, -}); - -export const { setTheme, setThemeColors, setLoadingState, loadSavedTheme } = - themeSlice.actions; -export default themeSlice.reducer; diff --git a/servers/nextjs/app/(presentation-generator)/styles/themes.css b/servers/nextjs/app/(presentation-generator)/styles/themes.css deleted file mode 100644 index 0063ee06..00000000 --- a/servers/nextjs/app/(presentation-generator)/styles/themes.css +++ /dev/null @@ -1,148 +0,0 @@ -/* Light Theme */ -.slide-theme[data-theme="light"] { - --slide-bg: var(--light-slide-bg); - --slide-title: var(--light-slide-title); - --slide-heading: var(--light-slide-heading); - --slide-description: var(--light-slide-description); - --slide-box: var(--light-slide-box); -} - -/* Dark Theme */ -.slide-theme[data-theme="dark"] { - --slide-bg: var(--dark-slide-bg); - --slide-title: var(--dark-slide-title); - --slide-heading: var(--dark-slide-heading); - --slide-description: var(--dark-slide-description); - --slide-box: var(--dark-slide-box); -} - -/* Classic Theme */ -.slide-theme[data-theme="faint_yellow"] { - --slide-bg: var(--faint_yellow-slide-bg); - --slide-title: var(--faint_yellow-slide-title); - --slide-heading: var(--faint_yellow-slide-heading); - --slide-description: var(--faint_yellow-slide-description); - --slide-box: var(--faint_yellow-slide-box); -} - -/* Custom Theme */ -.slide-theme[data-theme="custom"] { - --slide-bg: var(--custom-slide-bg); - --slide-title: var(--custom-slide-title); - --slide-heading: var(--custom-slide-heading); - --slide-description: var(--custom-slide-description); - --slide-box: var(--custom-slide-box); -} - -/* Royal Blue Theme */ -.slide-theme[data-theme="royal_blue"] { - --slide-bg: var(--royal_blue-slide-bg); - --slide-title: var(--royal_blue-slide-title); - --slide-heading: var(--royal_blue-slide-heading); - --slide-description: var(--royal_blue-slide-description); - --slide-box: var(--royal_blue-slide-box); -} - -/* Light Red Theme */ -.slide-theme[data-theme="light_red"] { - --slide-bg: var(--light_red-slide-bg); - --slide-title: var(--light_red-slide-title); - --slide-heading: var(--light_red-slide-heading); - --slide-description: var(--light_red-slide-description); - --slide-box: var(--light_red-slide-box); -} - -/* Dark Pink Theme */ -.slide-theme[data-theme="dark_pink"] { - --slide-bg: var(--dark_pink-slide-bg); - --slide-title: var(--dark_pink-slide-title); - --slide-heading: var(--dark_pink-slide-heading); - --slide-description: var(--dark_pink-slide-description); - --slide-box: var(--dark_pink-slide-box); -} - -/* Cream Theme */ -.slide-theme[data-theme="cream"] { - --slide-bg: var(--cream-slide-bg); - --slide-title: var(--cream-slide-title); - --slide-heading: var(--cream-slide-heading); - --slide-description: var(--cream-slide-description); - --slide-box: var(--cream-slide-box); -} - - -/* Apply theme styles - Make sure these selectors are more specific */ -.slide-theme .slide-container { - background-color: var(--slide-bg); - -} - - -.slide-theme .slide-title { - color: var(--slide-title); -} - -.slide-theme .slide-heading { - color: var(--slide-heading); -} - -.slide-theme .slide-description { - color: var(--slide-description); -} - -.slide-theme .slide-box { - background-color: var(--slide-box); -} - -/* Add to your existing CSS */ -.slide-theme { - transition: background-color 0.3s ease, color 0.3s ease; -} - -.slide-theme .border-color{ - border-color: var(--slide-box); -} - - -@keyframes progress { - 0% { width: 5%; } - 20% { width: 25%; } - 50% { width: 45%; } - 75% { width: 75%; } - 90% { width: 85%; } - 100% { width: 90%; } -} - -@keyframes fade-in { - 0% { opacity: 0; transform: translateY(10px); } - 100% { opacity: 1; transform: translateY(0); } -} - -.animate-progress { - animation: progress 20s ease-out forwards; -} - -.animate-fade-in { - animation: fade-in 0.5s ease-out forwards; -} - -@keyframes border-dance { - 0% { - background-position: 0 0, 100% 0, 100% 100%, 0 100%; - } - 100% { - background-position: 100% 0, 100% 100%, 0 100%, 0 0; - } -} - -.animate-border { - background-image: - linear-gradient(90deg, #5141e5 50%, transparent 50%), /* top */ - linear-gradient(90deg, #5141e5 50%, transparent 50%), /* right */ - linear-gradient(90deg, #5141e5 50%, transparent 50%), /* bottom */ - linear-gradient(90deg, #5141e5 50%, transparent 50%); /* left */ - background-repeat: repeat-x, repeat-y, repeat-x, repeat-y; - background-size: 15px 2px, 2px 15px, 15px 2px, 2px 15px; - background-position: 0 0, 100% 0, 100% 100%, 0 100%; - animation: border-dance 6s infinite linear; -} \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/theme/ThemePage.tsx b/servers/nextjs/app/(presentation-generator)/theme/ThemePage.tsx deleted file mode 100644 index fb03d405..00000000 --- a/servers/nextjs/app/(presentation-generator)/theme/ThemePage.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { Card } from "@/components/ui/card"; -import { defaultColors, setTheme, ThemeColors } from "../store/themeSlice"; -import Header from "@/app/dashboard/components/Header"; -import Wrapper from "@/components/Wrapper"; -import { useDispatch } from "react-redux"; -import { useRouter } from "next/navigation"; -import { ThemeType } from "../upload/type"; -import { Button } from "@/components/ui/button"; - -import { toast } from "@/hooks/use-toast"; - -interface ThemeCardProps { - name: string; - font: string; - colors: ThemeColors; - selected: boolean; - onClick: () => void; -} - -const ThemeCard = ({ - name, - font, - colors, - selected, - onClick, -}: ThemeCardProps) => { - return ( -
- -
-
-
-

- {name} -

-

- This is the body paragraph -

-
-
-
-
-
- ); -}; - -const ThemePage = () => { - const themes = [ - { - name: "Dark Theme", - colors: defaultColors.dark, - type: "dark", - font: "var(--font-inter)", - }, - - { - name: "Royal Blue Theme", - colors: defaultColors.royal_blue, - type: "royal_blue", - font: "var(--font-instrument-sans)", - }, - { - name: "Creme Theme", - colors: defaultColors.cream, - type: "cream", - font: "var(--font-fraunces)", - }, - { - name: "Light Red Theme", - colors: defaultColors.light_red, - type: "light_red", - font: "var(--font-montserrat)", - }, - { - name: "Dark Pink Theme", - colors: defaultColors.dark_pink, - type: "dark_pink", - font: "var(--font-inria-serif)", - }, - { - name: "Light Theme", - colors: defaultColors.light, - type: "light", - font: "var(--font-inter)", - }, - - { - name: "Faint Yellow Theme", - colors: defaultColors.faint_yellow, - type: "faint_yellow", - font: "var(--font-inter)", - }, - ]; - const dispatch = useDispatch(); - const router = useRouter(); - const [selectedTheme, setSelectedTheme] = useState(null); - const handleThemeClick = async (theme: ThemeColors, type: string) => { - setSelectedTheme(type as ThemeType); - }; - const handleSubmit = () => { - if (!selectedTheme) { - toast({ - title: "Please select a theme", - variant: "destructive", - }); - return; - } - dispatch(setTheme(selectedTheme as ThemeType)); - - router.push("/outline"); - }; - - return ( -
-
- -

Select a Theme

-
- {themes.map((theme, index) => ( - handleThemeClick(theme.colors, theme.type)} - /> - ))} -
- -
-
- ); -}; - -export default ThemePage; diff --git a/servers/nextjs/app/(presentation-generator)/theme/loading.tsx b/servers/nextjs/app/(presentation-generator)/theme/loading.tsx deleted file mode 100644 index 3a4314c1..00000000 --- a/servers/nextjs/app/(presentation-generator)/theme/loading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Skeleton } from '@/components/ui/skeleton' -import React from 'react' - -const loading = () => { - return ( -
- -
- - -
- { - Array.from({ length: 6 }).map((_, index) => ( - - )) - } -
-
-
- - ) -} - -export default loading \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/theme/page.tsx b/servers/nextjs/app/(presentation-generator)/theme/page.tsx deleted file mode 100644 index 0437a922..00000000 --- a/servers/nextjs/app/(presentation-generator)/theme/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import ThemePage from './ThemePage' -import { Metadata } from 'next' - -export const metadata: Metadata = { - title: "Theme Selection | Presenton.ai", - description: "Select a Presenton theme which will be suitable for your presentation", -} - -const page = () => { - return ( - - ) -} - -export default page diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx index 90a1beb1..72d0525f 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx @@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react' import { File, X, Upload } from 'lucide-react' -import { useToast } from '@/hooks/use-toast' +import { toast } from 'sonner' import { cn } from '@/lib/utils' interface FileWithId extends File { @@ -17,7 +17,6 @@ interface SupportingDocProps { const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const [isDragging, setIsDragging] = useState(false) const fileInputRef = useRef(null) - const { toast } = useToast() // Convert Files to FileWithId with proper type checking const filesWithIds: FileWithId[] = files.map(file => { @@ -57,19 +56,15 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const invalidFiles = droppedFiles.filter(file => !validTypes.includes(file.type)); if (invalidFiles.length > 0) { - toast({ - title: 'Invalid file type', + toast.error('Invalid file type', { description: 'Please upload only PDF, TXT, PPTX, or DOCX files', - variant: 'destructive', }); return; } if (hasPdf && droppedFiles.some(file => file.type === 'application/pdf')) { - toast({ - title: 'Multiple PDF files are not allowed', + toast.error('Multiple PDF files are not allowed', { description: 'Please select only one PDF file', - variant: 'destructive', }); return; } @@ -82,8 +77,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const updatedFiles = [...files, ...validFiles] onFilesChange(updatedFiles) - toast({ - title: 'Files selected', + toast.success('Files selected', { description: `${validFiles.length} file(s) have been added`, }) } @@ -102,8 +96,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => { const updatedFiles = [...files, ...validFiles] onFilesChange(updatedFiles) - toast({ - title: 'Files selected', + toast.success('Files selected', { description: `${validFiles.length} file(s) have been added`, }) } diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx new file mode 100644 index 00000000..e21ece2b --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/upload/components/ToastTesting.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import { Button } from '@/components/ui/button' +import { toast } from 'sonner' + +const ToastTesting = () => { + return ( +
+

Toast Testing - All Variants

+ +
+ {/* Success Toast */} + + + {/* Error Toast */} + + + {/* Info Toast */} + + + {/* Warning Toast */} + + + {/* Loading Toast */} + + + + + {/* Promise Toast */} + + + + + + + + + + + +
+ + +
+ ) +} + +export default ToastTesting diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index bd513217..a8e58058 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -20,12 +20,12 @@ import { LanguageType, PresentationConfig } from "../type"; import SupportingDoc from "./SupportingDoc"; import { Button } from "@/components/ui/button"; import { ChevronRight } from "lucide-react"; -import { useToast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { setPptGenUploadState } from "@/store/slices/presentationGenUpload"; -import { useLayout } from "../../context/LayoutContext"; +import ToastTesting from "./ToastTesting"; // Types for loading state interface LoadingState { @@ -39,7 +39,6 @@ interface LoadingState { const UploadPage = () => { const router = useRouter(); const dispatch = useDispatch(); - const { toast } = useToast(); // State management const [files, setFiles] = useState([]); @@ -72,18 +71,12 @@ const UploadPage = () => { */ const validateConfiguration = (): boolean => { if (!config.language || !config.slides) { - toast({ - title: "Please select number of Slides & Language", - variant: "destructive", - }); + toast.error("Please select number of Slides & Language"); return false; } if (!config.prompt.trim() && files.length === 0) { - toast({ - title: "No Prompt or Document Provided", - variant: "destructive", - }); + toast.error("No Prompt or Document Provided"); return false; } return true; @@ -177,10 +170,8 @@ const UploadPage = () => { duration: 0, showProgress: false, }); - toast({ - title: "Error", + toast.error("Error", { description: "Failed to generate presentation. Please try again.", - variant: "destructive", }); }; @@ -200,6 +191,7 @@ const UploadPage = () => { onConfigChange={handleConfigChange} />
+
{ - const randomData = randomChartGenerator(); - - return { - id: generateRandomId(), - name: "Sales Performance", - type: "bar", - presentation: presentation_id, - postfix: "", - data: randomData, - }; -}; - -export const getEmptySlideContent = ( - type: number, - index: number, - presentation_id: string -): Slide => { - const baseSlide: Slide = { - id: generateRandomId(), - type, - index, - design_index: 1, - properties: null, - images: [], - icons: [], - graph_id: null, - presentation: presentation_id, - content: { - title: "", - body: "", - infographics: [], - }, - }; - const graph = randomGraph(presentation_id); - - switch (type) { - case 1: - return { - ...baseSlide, - images: [""], - content: { - title: "New Title", - body: "Add your description here", - image_prompts: [""], - }, - }; - case 2: - return { - ...baseSlide, - content: { - title: "New Title", - body: [ - { heading: "First Point", description: "Add description" }, - { heading: "Second Point", description: "Add description" }, - ], - }, - }; - case 4: - return { - ...baseSlide, - images: ["", "", ""], - content: { - title: "New Title", - body: [ - { heading: "First Item", description: "Add description" }, - { heading: "Second Item", description: "Add description" }, - { heading: "Third Item", description: "Add description" }, - ], - image_prompts: ["", "", ""], - }, - }; - case 5: - return { - ...baseSlide, - graph_id: graph.id, - content: { - graph: graph, - title: "New Title", - body: "Add your description here", - }, - }; - case 6: - return { - ...baseSlide, - content: { - title: "New Title", - description: "Add your description here", - body: [ - { heading: "First Point", description: "Add description" }, - { heading: "Second Point", description: "Add description" }, - ], - }, - }; - case 7: - return { - ...baseSlide, - icons: ["", "", ""], - content: { - title: "New Title", - body: [ - { heading: "First Point", description: "Add description" }, - { heading: "Second Point", description: "Add description" }, - ], - icon_queries: [ - { - queries: [""], - }, - ], - }, - }; - case 8: - return { - ...baseSlide, - icons: ["", "", ""], - content: { - title: "New Title", - description: "Add your description here", - body: [ - { heading: "First Point", description: "Add description" }, - { heading: "Second Point", description: "Add description" }, - ], - icon_queries: [ - { - queries: [""], - }, - ], - }, - }; - case 9: - return { - ...baseSlide, - graph_id: graph.id, - content: { - graph: graph, - title: "New Subheading", - body: [ - { heading: "First Point", description: "Add description" }, - { heading: "Second Point", description: "Add description" }, - ], - }, - }; - - default: - return baseSlide; - } -}; diff --git a/servers/nextjs/app/(presentation-generator)/utils/chart.tsx b/servers/nextjs/app/(presentation-generator)/utils/chart.tsx deleted file mode 100644 index daced823..00000000 --- a/servers/nextjs/app/(presentation-generator)/utils/chart.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { formatLargeNumber } from "@/lib/utils"; -import { Chart } from "@/store/slices/presentationGeneration"; - -export const formatTooltipValue = (localChartData: Chart, value: number) => { - const formattedValue = formatLargeNumber(value); - if (localChartData.postfix) { - return `${formattedValue}${localChartData.postfix}`; - } - return formattedValue; -}; -export const transformedData = (localChartData: Chart) => { - if (!localChartData) return []; - if (!localChartData.data || localChartData.data.categories.length === 0) - return []; - if (localChartData && localChartData.type === "pie") { - return localChartData.data.categories.map((category, index) => ({ - name: category, - value: localChartData.data.series[0].data[index], - actualValue: localChartData.data.series[0].data[index], - seriesName: localChartData.data.series[0].name || "Series 1", - })); - } else { - return localChartData.data.categories.map((category, index) => { - const dataPoint: any = { name: category }; - localChartData.data.series.forEach((serie) => { - const seriesName = serie.name || "Series"; - dataPoint[seriesName] = serie.data[index]; - dataPoint[`${seriesName}Value`] = serie.data[index]; - }); - return dataPoint; - }); - } -}; - -export const formatYAxisTick = (value: number) => { - if (value >= 1_000_000_000_000) { - return `${(value / 1_000_000_000_000).toFixed(0)}T`; - } else if (value >= 1_000_000_000) { - return `${(value / 1_000_000_000).toFixed(0)}B`; - } else if (value >= 1_000_000) { - return `${(value / 1_000_000).toFixed(0)}M`; - } else if (value >= 1_000) { - return `${(value / 1_000).toFixed(0)}k`; - } - return value.toString(); -}; diff --git a/servers/nextjs/app/(presentation-generator)/utils/chartDataTransforms.ts b/servers/nextjs/app/(presentation-generator)/utils/chartDataTransforms.ts deleted file mode 100644 index fb07825f..00000000 --- a/servers/nextjs/app/(presentation-generator)/utils/chartDataTransforms.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Store Chart Data Type -export interface StoreChartData { - id: string; - name: string; - type: 'bar' | 'line' | 'pie'; - style: { - - }; - presentation: string; - postfix: string; - data: { - categories: string[]; - series: Array<{ - name?: string; - data: number[]; - }>; - }; -} diff --git a/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts b/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts deleted file mode 100644 index c76facc4..00000000 --- a/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts +++ /dev/null @@ -1,285 +0,0 @@ -import * as z from 'zod'; -import fs from 'fs'; -import * as path from 'path'; - -interface LayoutInfo { - id: string; - name: string; - description: string; - json_schema: Record; - group: string; -} - -interface LayoutGroup { - id: string; - ordered: boolean; - slides: string[]; -} - -interface LayoutStructure { - name: string; - ordered: boolean; - slides: LayoutInfo[]; -} - -interface GroupedLayoutsResponse { - group: string; - files: string[]; -} - -// Cache for layouts to avoid repeated file system operations -let layoutsCache: LayoutStructure[] | null = null; - -/** - * Dynamically imports a layout file and extracts its schema and metadata - */ -async function extractLayoutFromFile(filePath: string, fileName: string, groupName: string): Promise { - try { - // Import the layout module dynamically - const module = await import(filePath); - - // Check if the module has a Schema export - if (!module.Schema) { - console.warn(`No Schema export found in ${fileName}`); - return null; - } - - // Extract layout metadata (optional) - const layoutId = module.layoutId || fileName.replace(/\.tsx?$/, '').toLowerCase().replace(/layout$/, ''); - const layoutName = module.layoutName || fileName.replace(/\.tsx?$/, '').replace(/([A-Z])/g, ' $1').trim(); - const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`; - - // Convert Zod schema to JSON schema - const jsonSchema = z.toJSONSchema(module.Schema, { - override: (ctx) => { - delete ctx.jsonSchema.default; - }, - }); - - return { - id: layoutId, - name: layoutName, - description: layoutDescription, - json_schema: jsonSchema, - group: groupName - }; - } catch (error: unknown) { - console.error(`Error extracting layout from ${fileName}:`, error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(`Failed to extract schema from ${fileName}: ${errorMessage}`); - } -} - -/** - * Gets all layout files from the grouped presentation-layouts directory - */ -async function getGroupedLayoutFiles(): Promise { - const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts'); - - if (!fs.existsSync(layoutsDirectory)) { - throw new Error(`Layouts directory not found at ${layoutsDirectory}`); - } - - const items = fs.readdirSync(layoutsDirectory, { withFileTypes: true }); - const groupDirectories = items - .filter(item => item.isDirectory()) - .map(dir => dir.name); - - const allLayouts: GroupedLayoutsResponse[] = []; - - for (const groupName of groupDirectories) { - try { - const groupPath = path.join(layoutsDirectory, groupName); - const groupFiles = fs.readdirSync(groupPath); - - // Filter for TypeScript/TSX files, excluding setting.json and other non-layout files - const layoutFiles = groupFiles.filter(file => - (file.endsWith('.ts') || file.endsWith('.tsx')) && - file !== 'setting.json' && - !file.startsWith('.') && - !file.includes('.test.') && - !file.includes('.spec.') - ); - - if (layoutFiles.length > 0) { - allLayouts.push({ - group: groupName, - files: layoutFiles - }); - } - } catch (error) { - console.error(`Error reading group directory ${groupName}:`, error); - // Continue with other groups even if one fails - } - } - - return allLayouts; -} - -/** - * Extracts layout groups from layoutGroup.ts file in presentation-layouts directory - */ -async function extractLayoutGroups(): Promise { - try { - const layoutGroupPath = path.join(process.cwd(), 'presentation-layouts', 'layoutGroup.ts'); - - if (!fs.existsSync(layoutGroupPath)) { - throw new Error('layoutGroup.ts file not found in presentation-layouts directory'); - } - - const module = await import(layoutGroupPath); - - // Extract all exported layout groups - const layoutGroups: LayoutGroup[] = []; - - Object.keys(module).forEach(key => { - const exportedItem = module[key]; - - // Check if it's a layout group object - if (exportedItem && - typeof exportedItem === 'object' && - exportedItem.id && - Array.isArray(exportedItem.slides)) { - - layoutGroups.push({ - id: exportedItem.id, - ordered: exportedItem.ordered || false, - slides: exportedItem.slides - }); - } - }); - - if (layoutGroups.length === 0) { - throw new Error('No valid layout groups found in layoutGroup.ts'); - } - - return layoutGroups; - } catch (error) { - console.error('Error extracting layout groups:', error); - throw error; - } -} - -/** - * Maps layout information to layout groups - */ -function mapLayoutsToGroups( - layoutInfos: LayoutInfo[], - layoutGroups: LayoutGroup[] -): LayoutStructure[] { - return layoutGroups.map(group => { - const groupSlides: LayoutInfo[] = []; - - // Map slides in the group to their layout info - group.slides.forEach(slideId => { - const layoutInfo = layoutInfos.find(layout => - layout.id === slideId || - layout.id.replace('-', '') === slideId.replace('-', '') || - layout.id.toLowerCase() === slideId.toLowerCase() - ); - - if (layoutInfo) { - groupSlides.push(layoutInfo); - } else { - console.warn(`Layout info not found for slide ID: ${slideId}`); - } - }); - - return { - name: group.id, - ordered: group.ordered, - slides: groupSlides - }; - }); -} - -/** - * Main function to extract all layouts dynamically from grouped structure - */ -export async function extractLayouts(): Promise { - // Return cached layouts if available - if (layoutsCache) { - return layoutsCache; - } - - try { - // Get all grouped layout files - const groupedLayoutFiles = await getGroupedLayoutFiles(); - - if (groupedLayoutFiles.length === 0) { - throw new Error('No layout files found in the presentation-layouts directory'); - } - - // Extract layout information from each file in each group - const allLayoutPromises: Promise[] = []; - - for (const groupData of groupedLayoutFiles) { - for (const fileName of groupData.files) { - const filePath = path.join(process.cwd(), 'presentation-layouts', groupData.group, fileName); - allLayoutPromises.push(extractLayoutFromFile(filePath, fileName, groupData.group)); - } - } - - const layoutResults = await Promise.all(allLayoutPromises); - - // Filter out null results (files without valid schemas) - const validLayouts = layoutResults.filter((layout): layout is LayoutInfo => layout !== null); - - if (validLayouts.length === 0) { - throw new Error('No valid schemas found in any layout files'); - } - - // Extract layout groups - const layoutGroups = await extractLayoutGroups(); - - // Map layouts to groups - const mappedLayouts = mapLayoutsToGroups(validLayouts, layoutGroups); - - // Cache the results - layoutsCache = mappedLayouts; - - return mappedLayouts; - } catch (error) { - console.error('Error extracting layouts:', error); - throw error; - } -} - -/** - * Clears the layout cache (useful for development/testing) - */ -export function clearLayoutCache(): void { - layoutsCache = null; -} - -/** - * Gets a specific layout by ID - */ -export async function getLayoutById(layoutId: string): Promise { - const layouts = await extractLayouts(); - - for (const group of layouts) { - const layout = group.slides.find(slide => slide.id === layoutId); - if (layout) { - return layout; - } - } - - return null; -} - -/** - * Gets all available layout IDs - */ -export async function getAllLayoutIds(): Promise { - const layouts = await extractLayouts(); - const ids: string[] = []; - - layouts.forEach(group => { - group.slides.forEach(slide => { - ids.push(slide.id); - }); - }); - - return ids; -} \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/utils/others.ts b/servers/nextjs/app/(presentation-generator)/utils/others.ts index cd9f8307..e6ab9b56 100644 --- a/servers/nextjs/app/(presentation-generator)/utils/others.ts +++ b/servers/nextjs/app/(presentation-generator)/utils/others.ts @@ -52,170 +52,10 @@ export function removeUUID(fileName: string) { } -export function generateRandomId(): string { - const length = 36; - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'; - let id = ''; - for (let i = 0; i < length; i++) { - const randomIndex = Math.floor(Math.random() * chars.length); - id += chars[randomIndex]; - } - return id; -} -export const getFontLink = (fontName: string) => { - if (!fontName) { - return { link: '', name: '' }; - } - if (fontName.includes('instrument')) { - return { link: 'https://fonts.google.com/specimen/Instrument+Sans', name: 'Instrument Sans' } - } - if (fontName.includes('fraunces')) { - return { link: 'https://fonts.google.com/specimen/Fraunces', name: 'Fraunces' } - } - if (fontName.includes('montserrat')) { - return { link: 'https://fonts.google.com/specimen/Montserrat', name: 'Montserrat' } - } - if (fontName.includes('inria-serif')) { - return { link: 'https://fonts.google.com/specimen/Inria+Serif', name: 'Inria Serif' } - } - if (fontName.includes('inter')) { - return { link: 'https://fonts.google.com/specimen/Inter', name: 'Inter' } - } - else { - return { link: '', name: '' }; - } -} -export const numberTranslations: any = { - // Key languages - "English (English)": ["01", "02", "03", "04", "05"], - "English(English)": ["01", "02", "03", "04", "05"], - English: ["01", "02", "03", "04", "05"], - "Spanish (Español)": ["01", "02", "03", "04", "05"], - "French (Français)": ["01", "02", "03", "04", "05"], - "German (Deutsch)": ["01", "02", "03", "04", "05"], - "Portuguese (Português)": ["01", "02", "03", "04", "05"], - "Italian (Italiano)": ["01", "02", "03", "04", "05"], - "Dutch (Nederlands)": ["01", "02", "03", "04", "05"], - "Russian (Русский)": ["01", "02", "03", "04", "05"], - "Chinese (Simplified & Traditional - 中文, 汉语/漢語)": [ - "一", - "二", - "三", - "四", - "五", - ], - "Japanese (日本語)": ["一", "二", "三", "四", "五"], - "Korean (한국어)": ["일", "이", "삼", "사", "오"], - "Arabic (العربية)": ["١", "٢", "٣", "٤", "٥"], - "Hindi (हिन्दी)": ["०१", "०२", "०३", "०४", "०५"], - "Bengali (বাংলা)": ["০১", "০২", "০৩", "০৪", "০৫"], - - // European Languages - "Polish (Polski)": ["01", "02", "03", "04", "05"], - "Czech (Čeština)": ["01", "02", "03", "04", "05"], - "Slovak (Slovenčina)": ["01", "02", "03", "04", "05"], - "Hungarian (Magyar)": ["01", "02", "03", "04", "05"], - "Romanian (Română)": ["01", "02", "03", "04", "05"], - "Bulgarian (Български)": ["01", "02", "03", "04", "05"], - "Greek (Ελληνικά)": ["α΄", "β΄", "γ΄", "δ΄", "ε΄"], - "Serbian (Српски)": ["01", "02", "03", "04", "05"], - "Croatian (Hrvatski)": ["01", "02", "03", "04", "05"], - "Bosnian (Bosanski)": ["01", "02", "03", "04", "05"], - "Slovenian (Slovenščina)": ["01", "02", "03", "04", "05"], - "Finnish (Suomi)": ["01", "02", "03", "04", "05"], - "Swedish (Svenska)": ["01", "02", "03", "04", "05"], - "Danish (Dansk)": ["01", "02", "03", "04", "05"], - "Norwegian (Norsk)": ["01", "02", "03", "04", "05"], - "Icelandic (Íslenska)": ["01", "02", "03", "04", "05"], - "Lithuanian (Lietuvių)": ["01", "02", "03", "04", "05"], - "Latvian (Latviešu)": ["01", "02", "03", "04", "05"], - "Estonian (Eesti)": ["01", "02", "03", "04", "05"], - "Maltese (Malti)": ["01", "02", "03", "04", "05"], - "Welsh (Cymraeg)": ["01", "02", "03", "04", "05"], - "Irish (Gaeilge)": ["01", "02", "03", "04", "05"], - "Scottish Gaelic (Gàidhlig)": ["01", "02", "03", "04", "05"], - - // Middle Eastern and Central Asian Languages - "Hebrew (עברית)": ["א׳", "ב׳", "ג׳", "ד׳", "ה׳"], - "Persian/Farsi (فارسی)": ["۱", "۲", "۳", "۴", "۵"], - "Turkish (Türkçe)": ["01", "02", "03", "04", "05"], - "Kurdish (Kurdî / کوردی)": ["١", "٢", "٣", "٤", "٥"], - "Pashto (پښتو)": ["١", "٢", "٣", "٤", "٥"], - "Dari (دری)": ["١", "٢", "٣", "٤", "٥"], - "Uzbek (Oʻzbek)": ["01", "02", "03", "04", "05"], - "Kazakh (Қазақша)": ["01", "02", "03", "04", "05"], - "Tajik (Тоҷикӣ)": ["01", "02", "03", "04", "05"], - "Turkmen (Türkmençe)": ["01", "02", "03", "04", "05"], - "Azerbaijani (Azərbaycan dili)": ["01", "02", "03", "04", "05"], - - // South Asian Languages - "Urdu (اردو)": ["١", "٢", "٣", "٤", "٥"], - "Tamil (தமிழ்)": ["௧", "௨", "௩", "௪", "௫"], - "Telugu (తెలుగు)": ["౧", "౨", "౩", "౪", "౫"], - "Marathi (मराठी)": ["०१", "०२", "०३", "०४", "०५"], - "Punjabi (ਪੰਜਾਬੀ / پنجابی)": ["੦੧", "੦੨", "੦੩", "੦੪", "੦੫"], - "Gujarati (ગુજરાતી)": ["૦૧", "૦૨", "૦૩", "૦૪", "૦૫"], - "Malayalam (മലയാളം)": ["൧", "൨", "൩", "൪", "൫"], - "Kannada (ಕನ್ನಡ)": ["೧", "೨", "೩", "೪", "೫"], - "Odia (ଓଡ଼ିଆ)": ["୧", "୨", "୩", "୪", "୫"], - "Sinhala (සිංහල)": ["෧", "෨", "෩", "෪", "෫"], - "Nepali (नेपाली)": ["०१", "०२", "०३", "०४", "०५"], - - // East and Southeast Asian Languages - "Thai (ไทย)": ["๑", "๒", "๓", "๔", "๕"], - "Vietnamese (Tiếng Việt)": ["01", "02", "03", "04", "05"], - "Lao (ລາວ)": ["໑", "໒", "໓", "໔", "໕"], - "Khmer (ភាសាខ្មែរ)": ["១", "២", "៣", "៤", "៥"], - "Burmese (မြန်မာစာ)": ["၁", "၂", "၃", "၄", "၅"], - "Tagalog/Filipino (Tagalog/Filipino)": ["01", "02", "03", "04", "05"], - "Javanese (Basa Jawa)": ["01", "02", "03", "04", "05"], - "Sundanese (Basa Sunda)": ["01", "02", "03", "04", "05"], - "Malay (Bahasa Melayu)": ["01", "02", "03", "04", "05"], - "Mongolian (Монгол)": ["01", "02", "03", "04", "05"], - - // African Languages - "Swahili (Kiswahili)": ["01", "02", "03", "04", "05"], - "Hausa (Hausa)": ["01", "02", "03", "04", "05"], - "Yoruba (Yoruba)": ["01", "02", "03", "04", "05"], - "Igbo (Igbo)": ["01", "02", "03", "04", "05"], - "Amharic (አማርኛ)": ["፩", "፪", "፫", "፬", "፭"], - "Zulu (isiZulu)": ["01", "02", "03", "04", "05"], - "Xhosa (isiXhosa)": ["01", "02", "03", "04", "05"], - "Shona (ChiShona)": ["01", "02", "03", "04", "05"], - "Somali (Soomaaliga)": ["01", "02", "03", "04", "05"], - - // Indigenous and Lesser-Known Languages - "Basque (Euskara)": ["01", "02", "03", "04", "05"], - "Catalan (Català)": ["01", "02", "03", "04", "05"], - "Galician (Galego)": ["01", "02", "03", "04", "05"], - "Quechua (Runasimi)": ["01", "02", "03", "04", "05"], - "Nahuatl (Nāhuatl)": ["01", "02", "03", "04", "05"], - "Hawaiian (ʻŌlelo Hawaiʻi)": ["01", "02", "03", "04", "05"], - "Maori (Te Reo Māori)": ["01", "02", "03", "04", "05"], - "Tahitian (Reo Tahiti)": ["01", "02", "03", "04", "05"], - "Samoan (Gagana Samoa)": ["01", "02", "03", "04", "05"], -}; - -export const ThemeImagePrompt = { - light: - "Classy and modern with a corporate and minimalist touch. Tone is serious yet elegant, using a palette of light, white, and cool gray colors.", - dark: "Luxurious and futuristic with a simple, clean design. Professional yet elegant using a color scheme of dark, black, and high contrast.", - faint_yellow: - "Fresh young creatively vibrant style, utilizing a playful mixture of light colors like orange, salmon, and pastel purple, all set against a warm gradient.", - cream: - "Elegant with a classic and professional look. Subtle and minimalist using a warm palette of cream, beige, and light beige colors.", - royal_blue: - "Playful and creative, bold and loud with a futuristic touch, using a gradient of vibrant colors including blue, purple, and royal blue.", - light_red: - "Fun and organic with a playful and inspirational aesthetic, featuring pastel colors like pink, coral, and orange for a vibrant and warm feel.", - dark_pink: - " Inspirational and creative with a youthful and playful tone, featuring light, pastel colors including blue, pink, and purple, all blending in a vibrant gradient.", - custom: "", -}; export function sanitizeFilename(input: string, replacement = '') { diff --git a/servers/nextjs/app/api/export-as-pdf/route.ts b/servers/nextjs/app/api/export-as-pdf/route.ts index 30393da6..77798b8e 100644 --- a/servers/nextjs/app/api/export-as-pdf/route.ts +++ b/servers/nextjs/app/api/export-as-pdf/route.ts @@ -8,6 +8,7 @@ import { NextResponse, NextRequest } from 'next/server'; export async function POST(req: NextRequest) { const { id, title } = await req.json(); + console.log('path', process.env.APP_DATA_DIRECTORY); if (!id) { return NextResponse.json({ error: "Missing Presentation ID" }, { status: 400 }); } @@ -26,7 +27,8 @@ export async function POST(req: NextRequest) { }); browser.close(); const sanitizedTitle = sanitizeFilename(title); - const destinationPath = path.join(process.env.APP_DATA_DIRECTORY!, `${sanitizedTitle}.pdf`); + const destinationPath = path.join(process.env.APP_DATA_DIRECTORY!,'exports', `${sanitizedTitle}.pdf`); + console.log('destinationPath', destinationPath); await fs.promises.writeFile(destinationPath, pdfBuffer); return NextResponse.json({ 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 02da924f..eb51b36c 100644 --- a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts +++ b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts @@ -4,6 +4,10 @@ import puppeteer, { Browser, ElementHandle } from "puppeteer"; import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes"; import { convertElementAttributesToPptxSlides } from "@/utils/pptx_models_utils"; import { PptxPresentationModel } from "@/types/pptx_models"; +import fs from "fs"; +import path from "path"; +import crypto from "crypto"; +import sharp from "sharp"; // Interface for getAllChildElementsAttributes function arguments interface GetAllChildElementsAttributesArgs { @@ -12,6 +16,8 @@ interface GetAllChildElementsAttributesArgs { depth?: number; inheritedFont?: ElementAttributes['font']; inheritedBackground?: ElementAttributes['background']; + inheritedBorderRadius?: number[]; + screenshotsDir: string; } @@ -24,8 +30,19 @@ export async function GET(request: NextRequest) { args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + // Ensure screenshots directory exists + const tempDir = process.env.TEMP_DIRECTORY; + if (!tempDir) { + console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot'); + return undefined; + } + const screenshotsDir = path.join(tempDir, 'screenshots'); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir, { recursive: true }); + } + const slides = await getSlides(browser, id); - const slides_attributes = await getSlidesAttributes(slides); + const slides_attributes = await getSlidesAttributes(slides, screenshotsDir); const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes.elements, slides_attributes.backgroundColors); const presentation_pptx_model: PptxPresentationModel = { slides: slides_pptx_models, @@ -56,10 +73,14 @@ async function getPresentationId(request: NextRequest) { return id; } -async function getSlidesAttributes(slides: ElementHandle[]) { - const slideResults = await Promise.all(slides.map(async (slide) => { - return await getAllChildElementsAttributes({ element: slide }); - })); +async function getSlidesAttributes(slides: ElementHandle[], screenshotsDir: string) { + const slideResults: SlideAttributesResult[] = []; + //? Can't use Promise.all because of the screenshot + //? taking screenshot with mess up position of elements + for (const slide of slides) { + const result = await getAllChildElementsAttributes({ element: slide, screenshotsDir }); + slideResults.push(result); + } const elements = slideResults.map(result => result.elements); const backgroundColors = slideResults.map(result => result.backgroundColor); @@ -93,9 +114,10 @@ async function getPresentationPage(browser: Browser, id: string) { page.on('console', (msg) => { const type = msg.type(); const text = msg.text(); + console.log(`${type}: ${text}`); }); - await page.setViewport({ width: 1640, height: 720, deviceScaleFactor: 1 }); + await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 }); await page.goto(`http://localhost/presentation?id=${id}`, { waitUntil: "networkidle0", timeout: 60000, @@ -104,7 +126,7 @@ async function getPresentationPage(browser: Browser, id: string) { } -async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground }: GetAllChildElementsAttributesArgs): Promise { +async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise { // Get rootRect if not provided (first call) const currentRootRect = rootRect || await element.evaluate((el) => { const rect = el.getBoundingClientRect(); @@ -116,6 +138,43 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth = }; }); + // Check if this element is SVG or canvas or table + const tagName = await element.evaluate((el) => el.tagName.toLowerCase()); + + + if (tagName === 'svg' || tagName === 'canvas' || tagName === 'table') { + return { + elements: [], + backgroundColor: undefined + }; + + // // Get basic attributes for the element + // const attributes = await getElementAttributes(element); + // // Take screenshot of SVG/canvas/table element with accurate colors and opacity + // const screenshotPath = await takeElementScreenshot(element, screenshotsDir); + + // // Update image source to point to the screenshot + // if (screenshotPath) { + // attributes.imageSrc = screenshotPath; + // } + + // // Adjust position relative to root + // if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) { + // attributes.position = { + // left: attributes.position.left - currentRootRect.left, + // top: attributes.position.top - currentRootRect.top, + // width: attributes.position.width, + // height: attributes.position.height, + // }; + // } + + // // Return early without processing children for these elements + // return { + // elements: [attributes], + // backgroundColor: undefined + // }; + } + // Get direct children only (not all descendants) const directChildElementHandles = await element.$$(':scope > *'); @@ -134,6 +193,10 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth = if (inheritedBackground && !attributes.background && attributes.shadow) { attributes.background = inheritedBackground; } + // Apply inherited border radius if element doesn't have it + if (inheritedBorderRadius && !attributes.borderRadius) { + attributes.borderRadius = inheritedBorderRadius; + } // Adjust position relative to root if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) { @@ -155,6 +218,8 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth = depth: depth + 1, inheritedFont: attributes.font || inheritedFont, inheritedBackground: attributes.background || inheritedBackground, + inheritedBorderRadius: attributes.borderRadius || inheritedBorderRadius, + screenshotsDir, }); allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 }))); } @@ -232,7 +297,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth = } -// Do not edit this function, it is used to get the attributes of an element async function getElementAttributes(element: ElementHandle): Promise { const attributes = await element.evaluate((el: Element) => { @@ -333,6 +397,7 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise): Promise singleLineHeight * 2; // Allow some tolerance + const hasOverflow = htmlEl.scrollHeight > htmlEl.clientHeight; + + const isMultiline = hasExplicitLineBreaks || hasTextWrapping || hasOverflow; + + // Only return line height if text is multiline + if (isMultiline && lineHeight && lineHeight !== 'normal') { + const parsedLineHeight = parseFloat(lineHeight); + if (!isNaN(parsedLineHeight)) { + return parsedLineHeight; + } + } + + return undefined; + } + function parseMargin(computedStyles: CSSStyleDeclaration) { const marginTop = parseFloat(computedStyles.marginTop); const marginBottom = parseFloat(computedStyles.marginBottom); @@ -657,6 +753,8 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise): Promise, screenshotsDir: string): Promise { + try { + // Check element visibility and dimensions + const elementInfo = await element.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const styles = window.getComputedStyle(el); + + // Check if element is visible + const isVisible = styles.visibility !== 'hidden' && + styles.display !== 'none' && + styles.opacity !== '0'; + + if (!isVisible || rect.width <= 0 || rect.height <= 0) { + return null; + } + + return { + width: rect.width, + height: rect.height + }; + }); + + if (!elementInfo) { + console.warn('Element is not visible or has invalid dimensions, skipping screenshot'); + return undefined; + } + + // Generate unique filename + const uuid = crypto.randomUUID(); + const filename = `${uuid}.png`; + const filePath = path.join(screenshotsDir, filename); + + // Take screenshot of the element with accurate colors and opacity + // This captures the element exactly as rendered in the browser with all CSS styles applied + await element.screenshot({ + path: filePath as `${string}.png`, + type: 'png', + omitBackground: true // Use transparent background for better quality + }); + + console.log(`Screenshot saved: ${filePath}`); + return filePath; + + } catch (error) { + console.error('Error taking element screenshot:', error); + return undefined; + } +} + diff --git a/servers/nextjs/app/api/upload-image/route.ts b/servers/nextjs/app/api/upload-image/route.ts index 24ab96e1..8504f168 100644 --- a/servers/nextjs/app/api/upload-image/route.ts +++ b/servers/nextjs/app/api/upload-image/route.ts @@ -22,14 +22,13 @@ export async function POST(request: NextRequest) { const buffer = Buffer.from(bytes); // Create uploads directory if it doesn't exist - const uploadsDir = path.join(userDataDir, "images"); + const uploadsDir = path.join(userDataDir, "uploads"); fs.mkdirSync(uploadsDir, { recursive: true }); - console.log("uploadsDir", uploadsDir); + // Generate unique filename const filename = `${crypto.randomBytes(16).toString("hex")}.png`; const filePath = path.join(uploadsDir, filename); - console.log("filePath", filePath); // Write file to disk fs.writeFileSync(filePath, buffer); diff --git a/servers/nextjs/app/dashboard/components/Header.tsx b/servers/nextjs/app/dashboard/components/Header.tsx index 6ca4c67e..6063e8e1 100644 --- a/servers/nextjs/app/dashboard/components/Header.tsx +++ b/servers/nextjs/app/dashboard/components/Header.tsx @@ -3,9 +3,9 @@ import Wrapper from "@/components/Wrapper"; import React from "react"; import Link from "next/link"; -import UserAccount from "@/app/(presentation-generator)/components/UserAccount"; import BackBtn from "@/components/BackBtn"; import { usePathname } from "next/navigation"; +import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab"; const Header = () => { const pathname = usePathname(); return ( @@ -23,7 +23,7 @@ const Header = () => {
- +
diff --git a/servers/nextjs/app/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/dashboard/components/PresentationCard.tsx index b1497c11..55294912 100644 --- a/servers/nextjs/app/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/dashboard/components/PresentationCard.tsx @@ -9,7 +9,7 @@ import { PopoverContent, } from "@/components/ui/popover"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts"; export const PresentationCard = ({ @@ -39,35 +39,22 @@ export const PresentationCard = ({ e.preventDefault(); e.stopPropagation(); - toast({ - title: "Deleting presentation", + toast.loading("Deleting presentation", { description: "Please wait while we delete the presentation", - variant: "default", }); const response = await DashboardApi.deletePresentation(id); if (response) { - toast({ - title: "Presentation deleted", + toast.success("Presentation deleted", { description: "The presentation has been deleted successfully", - variant: "default", }); if (onDeleted) { onDeleted(id); } } else { - toast({ - title: "Error", - description: "Failed to delete presentation", - variant: "destructive", - }); + toast.error("Error deleting presentation"); } }; - - - - - return ( - e.stopPropagation()}> -
-} - -// Zod schema for type safety and sample data generation -export const Schema = z.object({ - title: z.string(), - description: z.string(), - // ... other fields -}) -``` - -## 🚀 Getting Started - -1. **Add Layout Components**: Place your layout files in the appropriate directory -2. **Export Requirements**: Ensure each layout exports both a default component and Schema -3. **Navigate**: Use the navigation controls or quick select grid -4. **Inspect**: View layout information and sample data structure -5. **Test**: See how your layouts render with realistic data - -## 🎯 Benefits - -- **Modular Architecture**: Easy to maintain and extend -- **Type Safety**: Full TypeScript support prevents runtime errors -- **Beautiful UI**: Professional design that's pleasant to use -- **Developer Experience**: Quick feedback loop for layout development -- **Responsive**: Works on all device sizes -- **Accessible**: Keyboard navigation and screen reader support - -## 📈 Performance - -- **Lazy Loading**: Components are imported only when needed -- **Optimized Rendering**: Efficient re-renders with React best practices -- **Minimal Bundle**: Modular structure enables tree shaking -- **Caching**: Sample data generation is memoized \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx index d2c3e6fd..af64dd0d 100644 --- a/servers/nextjs/app/layout-preview/[slug]/page.tsx +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -70,7 +70,7 @@ const GroupLayoutPreview = () => { {/* Layout Grid */}
- {layoutGroup.layouts.map((layout, index) => { + {layoutGroup.layouts.map((layout: any, index: number) => { const { component: LayoutComponent, sampleData, name, fileName } = layout return ( diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index addae119..00a14e2a 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -1,8 +1,8 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' -import { toast } from '@/hooks/use-toast' +import { toast } from 'sonner' interface UseGroupLayoutLoaderReturn { layoutGroup: LayoutGroup | null @@ -11,21 +11,38 @@ interface UseGroupLayoutLoaderReturn { retry: () => void } +// Global cache to store layout groups and avoid re-fetching +const layoutGroupCache = new Map() +const loadingGroupsCache = new Set() + export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => { const [layoutGroup, setLayoutGroup] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const hasMountedRef = useRef(false) const loadGroupLayouts = async () => { + // Check cache first + if (layoutGroupCache.has(groupSlug)) { + setLayoutGroup(layoutGroupCache.get(groupSlug)!) + setLoading(false) + setError(null) + return + } + + // Prevent multiple simultaneous requests for the same group + if (loadingGroupsCache.has(groupSlug)) { + return + } + try { setLoading(true) setError(null) - setLayoutGroup(null) + loadingGroupsCache.add(groupSlug) const response = await fetch('/api/layouts') if (!response.ok) { - toast({ - title: 'Error loading layouts', + toast.error('Error loading layouts', { description: response.statusText, }) return @@ -57,8 +74,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) if (!module.default) { - toast({ - title: `${layoutName} has no default export`, + toast.error(`${layoutName} has no default export`, { description: 'Please ensure the layout file exports a default component', }) console.warn(`${layoutName} has no default export`) @@ -66,8 +82,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } if (!module.Schema) { - toast({ - title: `${layoutName} is missing required Schema export`, + toast.error(`${layoutName} is missing required Schema export`, { description: 'Please ensure the layout file exports a Schema', }) console.error(`${layoutName} is missing required Schema export`) @@ -76,6 +91,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet // Use empty object to let schema apply its default values const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { name: layoutName, @@ -83,7 +99,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet schema: module.Schema, sampleData, fileName, - groupName: targetGroupData.groupName + groupName: targetGroupData.groupName, + layoutId } groupLayouts.push(layoutInfo) @@ -98,13 +115,15 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet if (module.default && module.Schema) { const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { name: layoutName, component: module.default, schema: module.Schema, sampleData, fileName, - groupName: targetGroupData.groupName + groupName: targetGroupData.groupName, + layoutId } groupLayouts.push(layoutInfo) } else { @@ -117,17 +136,20 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } if (groupLayouts.length === 0) { - toast({ - title: 'No valid layouts found', + toast.error('No valid layouts found', { description: `No valid layouts found in "${groupSlug}" group.`, }) setError(`No valid layouts found in "${groupSlug}" group.`) } else { - setLayoutGroup({ + const group: LayoutGroup = { groupName: targetGroupData.groupName, layouts: groupLayouts, settings: groupSettings - }) + } + + // Cache the result + layoutGroupCache.set(groupSlug, group) + setLayoutGroup(group) setError(null) } @@ -136,15 +158,19 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet setError(error instanceof Error ? error.message : 'Failed to load group layouts') } finally { setLoading(false) + loadingGroupsCache.delete(groupSlug) } } const retry = () => { + // Clear cache for this group to force reload + layoutGroupCache.delete(groupSlug) loadGroupLayouts() } useEffect(() => { - if (groupSlug) { + if (groupSlug && !hasMountedRef.current) { + hasMountedRef.current = true loadGroupLayouts() } }, [groupSlug]) diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 7e45519d..3d4e3a2d 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' -import { toast } from '@/hooks/use-toast' +import { toast } from 'sonner' interface UseLayoutLoaderReturn { layoutGroups: LayoutGroup[] @@ -25,8 +25,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const response = await fetch('/api/layouts') if (!response.ok) { - toast({ - title: 'Error loading layouts', + toast.error('Error loading layouts', { description: response.statusText, }) return @@ -50,21 +49,16 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { - toast({ - title: `${layoutName} has no default export`, + toast.error(`${layoutName} has no default export`, { description: 'Please ensure the layout file exports a default component', - }) console.warn(`${layoutName} has no default export`) continue } if (!module.Schema) { - toast({ - title: `${layoutName} is missing required Schema export`, + toast.error(`${layoutName} is missing required Schema export`, { description: 'Please ensure the layout file exports a Schema', - - }) console.error(`${layoutName} is missing required Schema export`) continue @@ -73,6 +67,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { // Use empty object to let schema apply its default values // User will need to provide actual data when using the layouts const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { name: layoutName, @@ -80,7 +75,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { schema: module.Schema, sampleData, fileName, - groupName: groupData.groupName + groupName: groupData.groupName, + layoutId } groupLayouts.push(layoutInfo) @@ -97,13 +93,15 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { if (module.default && module.Schema) { // Use empty object to let schema apply its default values const sampleData = module.Schema.parse({}) + const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { name: layoutName, component: module.default, schema: module.Schema, sampleData, fileName, - groupName: groupData.groupName + groupName: groupData.groupName, + layoutId } groupLayouts.push(layoutInfo) allLayouts.push(layoutInfo) @@ -126,10 +124,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } if (allLayouts.length === 0) { - toast({ - title: 'No valid layouts found', + toast.error('No valid layouts found', { description: 'Make sure your layout files export both a default component and a Schema.', - }) setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.') } else { diff --git a/servers/nextjs/app/layout-preview/types/index.ts b/servers/nextjs/app/layout-preview/types/index.ts index 668bf428..f784952f 100644 --- a/servers/nextjs/app/layout-preview/types/index.ts +++ b/servers/nextjs/app/layout-preview/types/index.ts @@ -6,6 +6,7 @@ export interface LayoutInfo { sampleData: any fileName: string groupName: string + layoutId: string } export interface GroupSetting { diff --git a/servers/nextjs/app/layout.tsx b/servers/nextjs/app/layout.tsx index d8ea12de..118b15ac 100644 --- a/servers/nextjs/app/layout.tsx +++ b/servers/nextjs/app/layout.tsx @@ -3,9 +3,9 @@ import localFont from "next/font/local"; import { Fraunces, Montserrat, Inria_Serif, Roboto, Instrument_Sans } from "next/font/google"; import "./globals.css"; import { Providers } from "./providers"; -import { Toaster } from "@/components/ui/toaster"; import { FooterProvider } from "./(presentation-generator)/context/footerContext"; import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext"; +import { Toaster } from "sonner"; const fraunces = Fraunces({ subsets: ["latin"], @@ -105,13 +105,11 @@ export default function RootLayout({ - - {children} - + ); diff --git a/servers/nextjs/app/settings/SettingPage.tsx b/servers/nextjs/app/settings/SettingPage.tsx index c7c39f9d..d70d90a7 100644 --- a/servers/nextjs/app/settings/SettingPage.tsx +++ b/servers/nextjs/app/settings/SettingPage.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import Header from "../dashboard/components/Header"; import Wrapper from "@/components/Wrapper"; import { Settings, Key, Loader2, Check, ChevronsUpDown } from "lucide-react"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { RootState } from "@/store/store"; import { useSelector } from "react-redux"; import { handleSaveLLMConfig } from "@/utils/storeHelpers"; @@ -156,22 +156,16 @@ const SettingsPage = () => { setIsLoading(true); await pullOllamaModels(); } - toast({ - title: "Success", - description: "Configuration saved successfully", - }); + toast.success("Configuration saved successfully"); setIsLoading(false); router.back(); } catch (error) { console.error("Error:", error); - toast({ - title: "Error", - description: - error instanceof Error - ? error.message - : "Failed to save configuration", - variant: "destructive", - }); + toast.error( + error instanceof Error + ? error.message + : "Failed to save configuration" + ); setIsLoading(false); } }; @@ -284,23 +278,18 @@ const SettingsPage = () => { const isModelAvailable = data.includes(llmConfig.CUSTOM_MODEL); if (!isModelAvailable) { setLlmConfig({ ...llmConfig, CUSTOM_MODEL: "" }); - toast({ - title: "Model Unavailable", - description: `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.`, - variant: "destructive", - }); + toast.error( + `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.` + ); } } } catch (error) { console.error("Error fetching custom models:", error); // Don't set customModelsChecked to true on error, so the button remains visible setCustomModels([]); - toast({ - title: "Error", - description: - "Failed to fetch available models. Please check your URL and API key.", - variant: "destructive", - }); + toast.error( + "Failed to fetch available models. Please check your URL and API key." + ); } finally { setCustomModelsLoading(false); } @@ -385,15 +374,15 @@ const SettingsPage = () => { key={provider} onClick={() => changeProvider(provider)} className={`relative p-4 rounded-lg border-2 transition-all duration-200 ${llmConfig.LLM === provider - ? "border-blue-500 bg-blue-50" - : "border-gray-200 hover:border-blue-200 hover:bg-gray-50" + ? "border-blue-500 bg-blue-50" + : "border-gray-200 hover:border-blue-200 hover:bg-gray-50" }`} >
{provider === "openai" @@ -770,8 +759,8 @@ const SettingsPage = () => { customModelsLoading || !llmConfig.CUSTOM_LLM_URL } className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL - ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" - : "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20" + ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" + : "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20" }`} > {customModelsLoading ? ( @@ -804,8 +793,8 @@ const SettingsPage = () => { customModelsLoading || !llmConfig.CUSTOM_LLM_URL } className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 font-semibold ${customModelsLoading || !llmConfig.CUSTOM_LLM_URL - ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" - : "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20" + ? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500" + : "bg-white border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-2 focus:ring-gray-500/20" }`} > {customModelsLoading ? ( @@ -995,10 +984,10 @@ const SettingsPage = () => { (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) } className={`mt-8 w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${isLoading || - (llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) || - (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) - ? "bg-gray-400 cursor-not-allowed" - : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200" + (llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) || + (llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) + ? "bg-gray-400 cursor-not-allowed" + : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200" } text-white`} > {isLoading ? ( diff --git a/servers/nextjs/app/settings/page.tsx b/servers/nextjs/app/settings/page.tsx index 1aca4ed3..e60eb189 100644 --- a/servers/nextjs/app/settings/page.tsx +++ b/servers/nextjs/app/settings/page.tsx @@ -1,6 +1,12 @@ import React from 'react' import SettingPage from './SettingPage' + +export const metadata = { + title: 'Settings | Presenton', + description: 'Settings page', +} const page = () => { + return ( ) diff --git a/servers/nextjs/components/Home.tsx b/servers/nextjs/components/Home.tsx index 8df8cc13..deeea0a3 100644 --- a/servers/nextjs/components/Home.tsx +++ b/servers/nextjs/components/Home.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { toast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { Info, ExternalLink, @@ -276,22 +276,20 @@ export default function Home() { setIsLoading(true); await pullOllamaModels(); } - toast({ - title: "Success", - description: "Configuration saved successfully", - }); + toast.success("Configuration saved successfully"); setIsLoading(false); router.push("/upload"); } catch (error) { console.error("Error:", error); - toast({ - title: "Error", - description: - error instanceof Error - ? error.message - : "Failed to save configuration", - variant: "destructive", - }); + toast.error( + error instanceof Error + ? error.message + : "Failed to save configuration", + { + description: + "Failed to save configuration", + } + ); setIsLoading(false); } }; @@ -413,12 +411,13 @@ export default function Home() { console.error("Error fetching custom models:", error); // Don't set customModelsChecked to true on error, so the button remains visible setCustomModels([]); - toast({ - title: "Error", - description: - "Failed to fetch available models. Please check your URL and API key.", - variant: "destructive", - }); + toast.error( + "Failed to fetch available models. Please check your URL and API key.", + { + description: + "Failed to fetch available models. Please check your URL and API key.", + } + ); } finally { setCustomModelsLoading(false); } diff --git a/servers/nextjs/components/ui/alert-dialog.tsx b/servers/nextjs/components/ui/alert-dialog.tsx deleted file mode 100644 index 57760f2e..00000000 --- a/servers/nextjs/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/servers/nextjs/components/ui/alert.tsx b/servers/nextjs/components/ui/alert.tsx deleted file mode 100644 index 5afd41d1..00000000 --- a/servers/nextjs/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/servers/nextjs/components/ui/aspect-ratio.tsx b/servers/nextjs/components/ui/aspect-ratio.tsx deleted file mode 100644 index d6a5226f..00000000 --- a/servers/nextjs/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client" - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" - -const AspectRatio = AspectRatioPrimitive.Root - -export { AspectRatio } diff --git a/servers/nextjs/components/ui/avatar.tsx b/servers/nextjs/components/ui/avatar.tsx deleted file mode 100644 index 51e507ba..00000000 --- a/servers/nextjs/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName - -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName - -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/servers/nextjs/components/ui/badge.tsx b/servers/nextjs/components/ui/badge.tsx deleted file mode 100644 index e87d62bf..00000000 --- a/servers/nextjs/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/servers/nextjs/components/ui/breadcrumb.tsx b/servers/nextjs/components/ui/breadcrumb.tsx deleted file mode 100644 index 1fd4b2c0..00000000 --- a/servers/nextjs/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cn } from "@/lib/utils" -import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode - } ->(({ ...props }, ref) =>