diff --git a/.github/workflows/dependency_check.yml b/.github/workflows/dependency_check.yml index d2076b30..6f1a97a2 100644 --- a/.github/workflows/dependency_check.yml +++ b/.github/workflows/dependency_check.yml @@ -97,10 +97,9 @@ jobs: uses: actions/cache@v4 with: path: .tsbuildinfo - key: ${{ runner.os }}-tsbuildinfo-${{ hashFiles('tsconfig.build.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }} + key: ${{ runner.os }}-tsbuildinfo-${{ hashFiles('tsconfig.json', 'tsconfig.build.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }} restore-keys: | - ${{ runner.os }}-tsbuildinfo-${{ hashFiles('tsconfig.build.json') }}- - ${{ runner.os }}-tsbuildinfo- + ${{ runner.os }}-tsbuildinfo-${{ hashFiles('tsconfig.json', 'tsconfig.build.json') }}- - name: Run Type Checking run: npm run typecheck diff --git a/components/presentation/mobile-presentation-generator.tsx b/components/presentation/mobile-presentation-generator.tsx index 6cc381ff..cc07f9fa 100644 --- a/components/presentation/mobile-presentation-generator.tsx +++ b/components/presentation/mobile-presentation-generator.tsx @@ -1,7 +1,10 @@ "use client"; import { useState } from "react"; -import { requestNotificationPermission, showDocumentNotification } from "@/lib/notifications"; +import { + requestNotificationPermission, + showDocumentNotification, +} from "@/lib/notifications"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -12,11 +15,12 @@ import { MobileSlideViewer } from "@/components/presentation/mobile-slide-viewer import { DiagramPreview } from "@/components/diagram/diagram-preview"; import { cn } from "@/lib/utils"; import { createClient } from "@/lib/supabase/client"; -import { - Loader2, - Sparkles, - ArrowRight, - Brain, +import { sanitizeMarkup } from "@/lib/sanitize-markup"; +import { + Loader2, + Sparkles, + ArrowRight, + Brain, Palette, Play, ChevronRight, @@ -29,17 +33,17 @@ import { CheckCircle2, Crown, Star, - Zap + Zap, } from "lucide-react"; -type GenerationStep = 'input' | 'outline' | 'theme' | 'preview'; +type GenerationStep = "input" | "outline" | "theme" | "preview"; export function MobilePresentationGenerator() { const [prompt, setPrompt] = useState(""); const [websiteUrl, setWebsiteUrl] = useState(""); - const [inputMode, setInputMode] = useState<'text' | 'url'>('text'); + const [inputMode, setInputMode] = useState<"text" | "url">("text"); const [pageCount, setPageCount] = useState(8); - const [currentStep, setCurrentStep] = useState('input'); + const [currentStep, setCurrentStep] = useState("input"); const [isGenerating, setIsGenerating] = useState(false); const [isFetchingUrl, setIsFetchingUrl] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState("modern-business"); @@ -54,34 +58,31 @@ export function MobilePresentationGenerator() { ...slide, imageUrl: slide.imageUrl || slide.image || null, chartData: slide.chartData || slide.charts || null, - content: slide.content || slide.body_text || slide.bodyText || '', + content: slide.content || slide.body_text || slide.bodyText || "", bullets: slide.bullets || slide.bullet_points || slide.bulletPoints || [], })); }; const normalizeVisualType = (value: unknown): string => { - if (typeof value !== 'string') return ''; + if (typeof value !== "string") return ""; const visualType = value.trim().toLowerCase(); - if (['svg', 'svg_code', 'svgcode'].includes(visualType)) return 'svg_code'; - if (['mermaid', 'diagram'].includes(visualType)) return 'mermaid'; - if (['html', 'html_tailwind', 'tailwind', 'mockup'].includes(visualType)) return 'html_tailwind'; - if (['chart', 'chart_data', 'data'].includes(visualType)) return 'chart_data'; + if (["svg", "svg_code", "svgcode"].includes(visualType)) return "svg_code"; + if (["mermaid", "diagram"].includes(visualType)) return "mermaid"; + if (["html", "html_tailwind", "tailwind", "mockup"].includes(visualType)) + return "html_tailwind"; + if (["chart", "chart_data", "data"].includes(visualType)) + return "chart_data"; return visualType; }; - const sanitizeMarkup = (markup: string): string => { - return markup - .replace(/[\s\S]*?<\/script>/gi, '') - .replace(/\son[a-z]+=(["']).*?\1/gi, '') - .replace(/javascript:/gi, ''); - }; - const renderMobileCodeVisual = (slide: any) => { - const visualType = normalizeVisualType(slide.visual_type || slide.visualType); + const visualType = normalizeVisualType( + slide.visual_type || slide.visualType, + ); const visualContent = slide.visual_content ?? slide.visualContent; if (!visualType) return null; - if (visualType === 'mermaid' && typeof visualContent === 'string') { + if (visualType === "mermaid" && typeof visualContent === "string") { return (
@@ -89,7 +90,7 @@ export function MobilePresentationGenerator() { ); } - if (visualType === 'svg_code' && typeof visualContent === 'string') { + if (visualType === "svg_code" && typeof visualContent === "string") { const svg = sanitizeMarkup(visualContent); return (
@@ -98,7 +99,7 @@ export function MobilePresentationGenerator() { ); } - if (visualType === 'html_tailwind' && typeof visualContent === 'string') { + if (visualType === "html_tailwind" && typeof visualContent === "string") { const html = sanitizeMarkup(visualContent); return (
@@ -107,23 +108,27 @@ export function MobilePresentationGenerator() { ); } - if (visualType === 'chart_data') { + if (visualType === "chart_data") { const chartData = slide.chartData || slide.charts || visualContent; if (!chartData?.data || !Array.isArray(chartData.data)) return null; return (

- 📊 {chartData.title || 'Data Visualization'} + 📊 {chartData.title || "Data Visualization"}

{chartData.data.slice(0, 5).map((item: any, idx: number) => (
{item.name} - {item.value} + + {item.value} +
))}
@@ -135,8 +140,12 @@ export function MobilePresentationGenerator() { }; const hasCodeVisual = (slide: any) => { - const visualType = normalizeVisualType(slide.visual_type || slide.visualType); - return ['svg_code', 'mermaid', 'html_tailwind', 'chart_data'].includes(visualType); + const visualType = normalizeVisualType( + slide.visual_type || slide.visualType, + ); + return ["svg_code", "mermaid", "html_tailwind", "chart_data"].includes( + visualType, + ); }; const MAX_FREE_PAGES = 8; @@ -149,7 +158,7 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-blue-50 to-indigo-100", accentColor: "from-blue-600 to-indigo-700", isPremium: false, - features: ["Professional Charts", "Corporate Colors", "Clean Typography"] + features: ["Professional Charts", "Corporate Colors", "Clean Typography"], }, { id: "creative-gradient", @@ -158,7 +167,7 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-purple-50 via-pink-50 to-orange-50", accentColor: "from-purple-600 via-pink-600 to-orange-600", isPremium: false, - features: ["Gradient Backgrounds", "Creative Layouts", "Bold Typography"] + features: ["Gradient Backgrounds", "Creative Layouts", "Bold Typography"], }, { id: "minimalist-pro", @@ -167,7 +176,7 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-gray-50 to-slate-100", accentColor: "from-gray-700 to-slate-800", isPremium: false, - features: ["Minimal Design", "Perfect Spacing", "Subtle Shadows"] + features: ["Minimal Design", "Perfect Spacing", "Subtle Shadows"], }, { id: "tech-modern", @@ -176,7 +185,7 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-cyan-50 to-blue-100", accentColor: "from-cyan-500 to-blue-600", isPremium: true, - features: ["Neon Accents", "Dark Theme", "Tech Graphics"] + features: ["Neon Accents", "Dark Theme", "Tech Graphics"], }, { id: "elegant-dark", @@ -185,7 +194,7 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-gray-800 to-gray-900", accentColor: "from-yellow-400 to-amber-500", isPremium: true, - features: ["Dark Luxury", "Gold Accents", "Premium Feel"] + features: ["Dark Luxury", "Gold Accents", "Premium Feel"], }, { id: "startup-pitch", @@ -194,8 +203,8 @@ export function MobilePresentationGenerator() { preview: "bg-gradient-to-br from-green-50 to-emerald-100", accentColor: "from-green-600 to-emerald-700", isPremium: true, - features: ["Pitch Optimized", "Growth Charts", "Investor Ready"] - } + features: ["Pitch Optimized", "Growth Charts", "Investor Ready"], + }, ]; const fetchUrlContent = async () => { @@ -210,20 +219,20 @@ export function MobilePresentationGenerator() { setIsFetchingUrl(true); try { - const response = await fetch('/api/fetch-url-content', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/fetch-url-content", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: websiteUrl }), }); if (!response.ok) { - throw new Error('Failed to fetch URL content'); + throw new Error("Failed to fetch URL content"); } const data = await response.json(); const contentSummary = `Create a presentation based on this content from ${data.title}:\n\n${data.content}`; setPrompt(contentSummary); - setInputMode('text'); + setInputMode("text"); toast({ title: "✅ Content Extracted!", @@ -253,8 +262,10 @@ export function MobilePresentationGenerator() { setIsGenerating(true); try { // Get authentication token - const { data: { session } } = await supabase.auth.getSession(); - + const { + data: { session }, + } = await supabase.auth.getSession(); + if (!session) { toast({ title: "Authentication Required", @@ -265,18 +276,18 @@ export function MobilePresentationGenerator() { return; } - const response = await fetch('/api/generate/presentation-outline', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session.access_token}`, + const response = await fetch("/api/generate/presentation-outline", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.access_token}`, }, body: JSON.stringify({ prompt, pageCount }), }); if (!response.ok) { const errorData = await response.json(); - + // Handle specific error codes if (response.status === 401) { toast({ @@ -286,23 +297,25 @@ export function MobilePresentationGenerator() { }); return; } - + if (response.status === 402) { toast({ title: "Not Enough Credits", - description: errorData.message || "You need more credits to generate this presentation.", + description: + errorData.message || + "You need more credits to generate this presentation.", variant: "destructive", }); return; } - - throw new Error(errorData.error || 'Failed to generate outlines'); + + throw new Error(errorData.error || "Failed to generate outlines"); } const data = await response.json(); setSlideOutlines(data.outlines); // Don't set slides yet - wait for full generation after template selection - setCurrentStep('outline'); + setCurrentStep("outline"); toast({ title: "✅ Structure Created!", @@ -311,7 +324,9 @@ export function MobilePresentationGenerator() { } catch (error: any) { toast({ title: "Generation failed", - description: error.message || "Could not generate slide outlines. Please try again.", + description: + error.message || + "Could not generate slide outlines. Please try again.", variant: "destructive", }); } finally { @@ -321,8 +336,8 @@ export function MobilePresentationGenerator() { const generateFullPresentation = async () => { setIsGenerating(true); - setCurrentStep('preview'); - + setCurrentStep("preview"); + try { // Request notification permissions for long-running task requestNotificationPermission().catch(console.error); @@ -332,22 +347,22 @@ export function MobilePresentationGenerator() { description: "Creating slides with unique images and charts", }); - const response = await fetch('/api/generate/presentation-full', { - method: 'POST', + const response = await fetch("/api/generate/presentation-full", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ - outlines: slideOutlines, + body: JSON.stringify({ + outlines: slideOutlines, template: selectedTemplate, prompt, - generationMode: 'code-driven' + generationMode: "code-driven", }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || 'Failed to generate presentation'); + throw new Error(errorData.error || "Failed to generate presentation"); } const data = await response.json(); @@ -361,9 +376,8 @@ export function MobilePresentationGenerator() { // Show push notification showDocumentNotification("🎉 Presentation Ready!", { body: `${data.slides.length} slides created successfully. Click to view!`, - data: { url: window.location.pathname } + data: { url: window.location.pathname }, }).catch(console.error); - } catch (error: any) { toast({ title: "Generation failed", @@ -374,35 +388,37 @@ export function MobilePresentationGenerator() { // Show failure notification showDocumentNotification("❌ Generation Failed", { body: "Failed to generate your presentation. Please try again.", - data: { url: window.location.pathname } + data: { url: window.location.pathname }, }).catch(console.error); - setCurrentStep('theme'); + setCurrentStep("theme"); } finally { setIsGenerating(false); } }; const resetToInput = () => { - setCurrentStep('input'); + setCurrentStep("input"); setSlideOutlines([]); setSlides([]); }; // Step 1: Input - if (currentStep === 'input') { + if (currentStep === "input") { return (
{/* Background matching your theme */}
- +
{/* Header */}
- AI Presentation Studio + + AI Presentation Studio +

@@ -412,29 +428,30 @@ export function MobilePresentationGenerator() {

- Describe your topic or paste a URL and let AI create a professional presentation + Describe your topic or paste a URL and let AI create a + professional presentation

{/* Input Mode Toggle */}
{prompt && (
-

✅ Content Extracted

-

{prompt.substring(0, 150)}...

+

+ ✅ Content Extracted +

+

+ {prompt.substring(0, 150)}... +

)}
@@ -514,11 +535,17 @@ export function MobilePresentationGenerator() { min="3" max={MAX_FREE_PAGES} value={pageCount} - onChange={(e) => setPageCount(Math.min(parseInt(e.target.value) || 3, MAX_FREE_PAGES))} + onChange={(e) => + setPageCount( + Math.min(parseInt(e.target.value) || 3, MAX_FREE_PAGES), + ) + } className="w-20 text-center border-gray-200" disabled={isGenerating} /> - Max {MAX_FREE_PAGES} slides + + Max {MAX_FREE_PAGES} slides +
@@ -526,11 +553,15 @@ export function MobilePresentationGenerator() {
- AI Images + + AI Images +
- Smart Charts + + Smart Charts +
@@ -567,14 +598,18 @@ export function MobilePresentationGenerator() {
Fast Generation
-
In Seconds
+
+ In Seconds +
🎨
Beautiful Design
-
Pro Quality
+
+ Pro Quality +
@@ -584,13 +619,13 @@ export function MobilePresentationGenerator() { } // Step 2: Outline Preview - if (currentStep === 'outline') { + if (currentStep === "outline") { return (
{/* Background matching theme */}
- +
{/* Header */}
@@ -605,43 +640,53 @@ export function MobilePresentationGenerator() {

- {slideOutlines?.length || 0} slides planned • Choose your style next + {slideOutlines?.length || 0} slides planned • Choose your style + next

{/* Outlines List */}
- {slideOutlines && slideOutlines.length > 0 && slideOutlines.map((outline, index) => ( - -
-
- {index + 1} -
-
-

- {outline.title} -

-

- {outline.content} -

-
- {outline.imageUrl && ( -
- - Image -
- )} - {outline.chartData && ( -
- - Chart -
- )} + {slideOutlines && + slideOutlines.length > 0 && + slideOutlines.map((outline, index) => ( + +
+
+ {index + 1} +
+
+

+ {outline.title} +

+

+ {outline.content} +

+
+ {outline.imageUrl && ( +
+ + + Image + +
+ )} + {outline.chartData && ( +
+ + + Chart + +
+ )} +
-
- - ))} + + ))}
{/* Actions */} @@ -655,7 +700,7 @@ export function MobilePresentationGenerator() { ← Back - + {/* Progress Dots */}
{slides.map((_, index) => ( @@ -470,8 +493,8 @@ export function MobileSlideViewer({ slides, onClose }: MobileSlideViewerProps) { onClick={() => setCurrentSlide(index)} className={`rounded-full transition-all ${ index === currentSlide - ? 'bg-gradient-to-r from-yellow-400 to-orange-500 w-8 sm:w-10 h-2 sm:h-2.5 shadow-lg' - : 'bg-gray-600 hover:bg-gray-500 w-2 sm:w-2.5 h-2 sm:h-2.5' + ? "bg-gradient-to-r from-yellow-400 to-orange-500 w-8 sm:w-10 h-2 sm:h-2.5 shadow-lg" + : "bg-gray-600 hover:bg-gray-500 w-2 sm:w-2.5 h-2 sm:h-2.5" }`} aria-label={`Go to slide ${index + 1}`} /> diff --git a/components/presentation/presentation-preview.tsx b/components/presentation/presentation-preview.tsx index 14a4c864..f17375ac 100644 --- a/components/presentation/presentation-preview.tsx +++ b/components/presentation/presentation-preview.tsx @@ -3,13 +3,24 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { ChevronLeft, ChevronRight, Maximize2, Minimize2, Play, Pause, RotateCcw, Image as ImageIcon, Edit3 } from "lucide-react"; +import { + ChevronLeft, + ChevronRight, + Maximize2, + Minimize2, + Play, + Pause, + RotateCcw, + Image as ImageIcon, + Edit3, +} from "lucide-react"; import Image from "next/image"; import { useTheme } from "next-themes"; import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { PostGenerationImageEditor } from "./post-generation-image-editor"; import { DiagramPreview } from "@/components/diagram/diagram-preview"; import { PresentationVisualFrame } from "./visual-frame"; +import { sanitizeMarkup } from "@/lib/sanitize-markup"; import { getSlideMotionTransition, getSlideMotionVariants, @@ -33,28 +44,23 @@ import { Area, ScatterChart, Scatter, - Legend + Legend, } from "recharts"; -const CODE_VISUAL_TYPES = new Set(["svg_code", "mermaid", "html_tailwind", "chart_data"]); - -function sanitizeMarkup(markup: string): string { - return markup - .replace(/[\s\S]*?<\/script>/gi, "") - .replace(/[\s\S]*?<\/iframe>/gi, "") - .replace(/[\s\S]*?<\/object>/gi, "") - .replace(//gi, "") - .replace(/\son[a-z]+=(["']).*?\1/gi, "") - .replace(/\son[a-z]+={[^}]*}/gi, "") - .replace(/javascript:/gi, ""); -} +const CODE_VISUAL_TYPES = new Set([ + "svg_code", + "mermaid", + "html_tailwind", + "chart_data", +]); function normalizeVisualType(value: unknown): string { if (typeof value !== "string") return ""; const visualType = value.trim().toLowerCase(); if (["svg", "svg_code", "svgcode"].includes(visualType)) return "svg_code"; if (["mermaid", "diagram"].includes(visualType)) return "mermaid"; - if (["html", "html_tailwind", "tailwind", "mockup"].includes(visualType)) return "html_tailwind"; + if (["html", "html_tailwind", "tailwind", "mockup"].includes(visualType)) + return "html_tailwind"; if (["chart", "chart_data", "data"].includes(visualType)) return "chart_data"; return visualType; } @@ -84,15 +90,30 @@ function resolveSlideThemeTokens(slide: any, isDark: boolean) { const accent = getSlideAccentColor(slide) || (isDark ? "#38bdf8" : "#2563eb"); const palette = slide?.theme?.palette || {}; return { - bg: (typeof palette.secondary === "string" && /^#[0-9A-Fa-f]{6}$/.test(palette.secondary)) ? palette.secondary : isDark ? "#0f172a" : "#f8fafc", - card: (typeof palette.primary === "string" && /^#[0-9A-Fa-f]{6}$/.test(palette.primary)) ? palette.primary : isDark ? "#1e293b" : "#ffffff", + bg: + typeof palette.secondary === "string" && + /^#[0-9A-Fa-f]{6}$/.test(palette.secondary) + ? palette.secondary + : isDark + ? "#0f172a" + : "#f8fafc", + card: + typeof palette.primary === "string" && + /^#[0-9A-Fa-f]{6}$/.test(palette.primary) + ? palette.primary + : isDark + ? "#1e293b" + : "#ffffff", fg: isDark ? "#e2e8f0" : "#0f172a", accent, border: isDark ? "#334155" : "#cbd5e1", }; } -function wrapMarkupWithThemeTokens(markup: string, tokens: ReturnType): string { +function wrapMarkupWithThemeTokens( + markup: string, + tokens: ReturnType, +): string { return `