"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; import useStore from "@/lib/store"; import modelAdapter from "@/lib/services/adapter-instance"; import type { Slide, SlidesPresentation } from "@/types"; import { Presentation, Copy, Loader2, CheckCircle2, ChevronLeft, ChevronRight, Download, Maximize2, Minimize2, Settings, Globe, Palette, Users, Building2, Hash, Play, Pause, Upload, X, FileText, Image as ImageIcon, File, Sparkles, BarChart3, TrendingUp, Zap, } from "lucide-react"; import { cn } from "@/lib/utils"; import { translations } from "@/lib/i18n/translations"; const LANGUAGES = [ { code: "en", name: "English", nativeName: "English" }, { code: "zh", name: "Chinese", nativeName: "中文" }, { code: "es", name: "Spanish", nativeName: "Español" }, { code: "fr", name: "French", nativeName: "Français" }, { code: "de", name: "German", nativeName: "Deutsch" }, { code: "ja", name: "Japanese", nativeName: "日本語" }, { code: "ko", name: "Korean", nativeName: "한국어" }, { code: "ru", name: "Russian", nativeName: "Русский" }, { code: "ar", name: "Arabic", nativeName: "العربية" }, { code: "pt", name: "Portuguese", nativeName: "Português" }, { code: "it", name: "Italian", nativeName: "Italiano" }, { code: "hi", name: "Hindi", nativeName: "हिन्दी" }, { code: "tr", name: "Turkish", nativeName: "Türkçe" }, { code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt" }, { code: "th", name: "Thai", nativeName: "ไทย" }, { code: "nl", name: "Dutch", nativeName: "Nederlands" }, { code: "pl", name: "Polish", nativeName: "Polski" }, { code: "uk", name: "Ukrainian", nativeName: "Українська" }, ]; const THEMES = [ { id: "corporate-blue", name: "Corporate Blue", colors: ["#0f172a", "#3b82f6", "#60a5fa", "#ffffff"], icon: "🏢", gradient: "linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #1e40af 100%)" }, { id: "executive-dark", name: "Executive Dark", colors: ["#09090b", "#6366f1", "#a855f7", "#fafafa"], icon: "👔", gradient: "linear-gradient(135deg, #09090b 0%, #18181b 50%, #27272a 100%)" }, { id: "modern-gradient", name: "Modern Gradient", colors: ["#0c0a09", "#f97316", "#eab308", "#fafaf9"], icon: "✨", gradient: "linear-gradient(135deg, #0c0a09 0%, #1c1917 50%, #292524 100%)" }, { id: "tech-neon", name: "Tech Neon", colors: ["#020617", "#22d3ee", "#a3e635", "#f8fafc"], icon: "⚡", gradient: "linear-gradient(135deg, #020617 0%, #0c1929 50%, #172554 100%)" }, { id: "minimal-light", name: "Minimal Light", colors: ["#ffffff", "#18181b", "#71717a", "#f4f4f5"], icon: "◻️", gradient: "linear-gradient(135deg, #ffffff 0%, #f4f4f5 50%, #e4e4e7 100%)" }, { id: "premium-gold", name: "Premium Gold", colors: ["#1a1a2e", "#d4af37", "#ffd700", "#f5f5dc"], icon: "👑", gradient: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)" }, { id: "nature-green", name: "Nature Green", colors: ["#14532d", "#22c55e", "#86efac", "#f0fdf4"], icon: "🌿", gradient: "linear-gradient(135deg, #14532d 0%, #166534 50%, #15803d 100%)" }, { id: "sunset-warm", name: "Sunset Warm", colors: ["#1f1f1f", "#f43f5e", "#fb923c", "#fef2f2"], icon: "🌅", gradient: "linear-gradient(135deg, #1f1f1f 0%, #2d1b1b 50%, #3d2424 100%)" }, ]; const AUDIENCES = [ { id: "executives", name: "Executives & C-Suite", icon: "👔", style: "Sophisticated, data-driven, strategic focus" }, { id: "investors", name: "Investors & Board", icon: "💼", style: "ROI-focused, metrics-heavy, growth narrative" }, { id: "technical", name: "Technical Team", icon: "💻", style: "Detailed, architecture diagrams, code snippets" }, { id: "marketing", name: "Marketing & Sales", icon: "📈", style: "Persuasive, visual storytelling, emotional appeal" }, { id: "general", name: "General Audience", icon: "👥", style: "Clear, engaging, accessible language" }, { id: "stakeholders", name: "Stakeholders", icon: "🤝", style: "Project updates, milestones, risk mitigation" }, { id: "clients", name: "Clients & Customers", icon: "⭐", style: "Benefits-focused, testimonials, case studies" }, ]; const ANIMATION_STYLES = [ { id: "professional", name: "Professional", description: "Subtle fade & slide transitions" }, { id: "dynamic", name: "Dynamic", description: "Engaging animations with emphasis effects" }, { id: "minimal", name: "Minimal", description: "Clean, simple transitions only" }, { id: "impressive", name: "Impressive", description: "Bold animations, parallax, morphing effects" }, ]; interface AttachedFile { id: string; name: string; type: string; size: number; content?: string; preview?: string; colors?: string[]; } const ACCEPTED_FILE_TYPES = { documents: [".pdf", ".doc", ".docx", ".txt", ".rtf", ".md"], presentations: [".pptx", ".ppt", ".key", ".odp"], images: [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"], data: [".json", ".csv", ".xlsx", ".xls"], design: [".ase", ".aco", ".gpl", ".css"], // Color palette formats }; const ALL_ACCEPTED = Object.values(ACCEPTED_FILE_TYPES).flat().join(","); export default function SlidesGenerator() { const { selectedProvider, selectedModels, availableModels, apiKeys, isProcessing, error, slidesPresentation, setSelectedProvider, setSlidesPresentation, setProcessing, setError, setAvailableModels, setSelectedModel, language: uiLanguage, } = useStore(); const t = translations[uiLanguage].slidesGen; const common = translations[uiLanguage].common; const [topic, setTopic] = useState(""); const [language, setLanguage] = useState("en"); const [theme, setTheme] = useState("executive-dark"); const [audience, setAudience] = useState("executives"); const [organization, setOrganization] = useState(""); const [slideCount, setSlideCount] = useState(10); const [animationStyle, setAnimationStyle] = useState("professional"); const [copied, setCopied] = useState(false); const [currentSlide, setCurrentSlide] = useState(0); const [isFullscreen, setIsFullscreen] = useState(false); const [isAutoPlaying, setIsAutoPlaying] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [attachedFiles, setAttachedFiles] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const [uploadProgress, setUploadProgress] = useState(null); const slideContainerRef = useRef(null); const autoPlayRef = useRef(null); const fileInputRef = useRef(null); const slideFrameRef = useRef(null); const selectedModel = selectedModels[selectedProvider]; const models = availableModels[selectedProvider] || modelAdapter.getAvailableModels(selectedProvider); useEffect(() => { if (typeof window !== "undefined") { loadAvailableModels(); const saved = localStorage.getItem("promptarch-api-keys"); if (saved) { try { const keys = JSON.parse(saved); if (keys.qwen) modelAdapter.updateQwenApiKey(keys.qwen); if (keys.ollama) modelAdapter.updateOllamaApiKey(keys.ollama); if (keys.zai) modelAdapter.updateZaiApiKey(keys.zai); } catch (e) { console.error("Failed to load API keys:", e); } } } }, [selectedProvider]); useEffect(() => { if (isAutoPlaying && slidesPresentation?.slides) { autoPlayRef.current = setInterval(() => { setCurrentSlide((prev) => prev >= (slidesPresentation.slides.length - 1) ? 0 : prev + 1 ); }, 5000); } return () => { if (autoPlayRef.current) { clearInterval(autoPlayRef.current); } }; }, [isAutoPlaying, slidesPresentation?.slides?.length]); const loadAvailableModels = async () => { const fallbackModels = modelAdapter.getAvailableModels(selectedProvider); setAvailableModels(selectedProvider, fallbackModels); try { const result = await modelAdapter.listModels(selectedProvider); if (result.success && result.data) { setAvailableModels(selectedProvider, result.data[selectedProvider] || fallbackModels); } } catch (error) { console.error("Failed to load models:", error); } }; // Extract colors from image const extractColorsFromImage = (file: File): Promise => { return new Promise((resolve) => { const img = document.createElement("img"); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); img.onload = () => { canvas.width = 50; canvas.height = 50; ctx?.drawImage(img, 0, 0, 50, 50); const imageData = ctx?.getImageData(0, 0, 50, 50); if (!imageData) { resolve([]); return; } const colorCounts: Record = {}; for (let i = 0; i < imageData.data.length; i += 4) { const r = Math.round(imageData.data[i] / 32) * 32; const g = Math.round(imageData.data[i + 1] / 32) * 32; const b = Math.round(imageData.data[i + 2] / 32) * 32; const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; colorCounts[hex] = (colorCounts[hex] || 0) + 1; } const sortedColors = Object.entries(colorCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([color]) => color); resolve(sortedColors); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(file); }); }; // Process uploaded file const processFile = async (file: File): Promise => { const id = Math.random().toString(36).substr(2, 9); const ext = file.name.split(".").pop()?.toLowerCase() || ""; const attachedFile: AttachedFile = { id, name: file.name, type: file.type || ext, size: file.size, }; try { // Handle text-based files if ([...ACCEPTED_FILE_TYPES.documents, ".json", ".csv", ".css", ".md"].some(e => file.name.endsWith(e))) { const text = await file.text(); attachedFile.content = text.slice(0, 50000); // Limit to 50KB of text // Extract colors from CSS files if (file.name.endsWith(".css")) { const colorMatches = text.match(/#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|hsl\([^)]+\)/g); if (colorMatches) { attachedFile.colors = [...new Set(colorMatches)].slice(0, 10); } } // Parse JSON color palettes if (file.name.endsWith(".json")) { try { const json = JSON.parse(text); if (json.colors || json.palette) { attachedFile.colors = (json.colors || json.palette).slice(0, 10); } } catch { } } } // Handle images if (ACCEPTED_FILE_TYPES.images.some(e => file.name.toLowerCase().endsWith(e))) { attachedFile.preview = URL.createObjectURL(file); attachedFile.colors = await extractColorsFromImage(file); } // Handle presentations (extract text content if possible) if (ACCEPTED_FILE_TYPES.presentations.some(e => file.name.toLowerCase().endsWith(e))) { attachedFile.content = `[Presentation file: ${file.name}] - Analyze structure and content for redesign.`; } return attachedFile; } catch (err) { console.error("Error processing file:", err); return attachedFile; } }; const handleFileDrop = useCallback(async (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = Array.from(e.dataTransfer.files); await handleFileUpload(files); }, []); const handleFileUpload = async (files: File[]) => { setUploadProgress("Processing files..."); const newFiles: AttachedFile[] = []; for (const file of files) { setUploadProgress(`Processing ${file.name}...`); const processed = await processFile(file); if (processed) { newFiles.push(processed); } } setAttachedFiles(prev => [...prev, ...newFiles]); setUploadProgress(null); }; const removeFile = (id: string) => { setAttachedFiles(prev => { const file = prev.find(f => f.id === id); if (file?.preview) { URL.revokeObjectURL(file.preview); } return prev.filter(f => f.id !== id); }); }; const getFileIcon = (type: string, name: string) => { if (name.match(/\.(png|jpg|jpeg|svg|webp|gif)$/i)) return ; if (name.match(/\.(pdf|doc|docx|txt|md)$/i)) return ; if (name.match(/\.(pptx|ppt|key)$/i)) return ; return ; }; const buildFileContext = (): string => { if (attachedFiles.length === 0) return ""; let context = "\n\n## ATTACHED FILES CONTEXT:\n"; for (const file of attachedFiles) { context += `\n### File: ${file.name}\n`; if (file.colors && file.colors.length > 0) { context += `Brand Colors Extracted: ${file.colors.join(", ")}\n`; context += "USE THESE EXACT COLORS in the presentation design.\n"; } if (file.content) { context += `Content:\n\`\`\`\n${file.content.slice(0, 10000)}\n\`\`\`\n`; } if (file.name.match(/\.(pptx|ppt|key)$/i)) { context += "This is an existing presentation - analyze its structure and REDESIGN with modern aesthetics while preserving the content flow.\n"; } } return context; }; const parseSlides = (content: string): SlidesPresentation | null => { try { const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim(); const parsed = JSON.parse(jsonStr); if (parsed.slides && Array.isArray(parsed.slides)) { return { id: Math.random().toString(36).substr(2, 9), title: parsed.title || "Untitled Presentation", subtitle: parsed.subtitle || "", author: parsed.author || "", organization: organization, theme: parsed.theme || theme, language: parsed.language || LANGUAGES.find(l => l.code === language)?.name || "English", slides: parsed.slides.map((slide: any, index: number) => ({ id: slide.id || `slide-${index + 1}`, title: slide.title || `Slide ${index + 1}`, content: slide.content || "", htmlContent: slide.htmlContent || generateAnimatedHtml(slide, index), notes: slide.notes || "", layout: slide.layout || "content", order: slide.order || index + 1, })), rawContent: content, createdAt: new Date(), updatedAt: new Date(), }; } } catch (e) { console.error("Failed to parse slides:", e); } return null; }; const buildSlideDoc = (html: string): string => { const normalized = (html || "").trim(); if (!normalized) return ""; const isFullDoc = /^ ${normalized} `; }; const generateAnimatedHtml = (slide: any, index: number): string => { const themeConfig = THEMES.find(t => t.id === theme) || THEMES[1]; const [bg, accent, secondary, text] = themeConfig.colors; const gradient = themeConfig.gradient; // Get brand colors from attached files if available const brandColors = attachedFiles.flatMap(f => f.colors || []).slice(0, 3); const primaryColor = brandColors[0] || accent; const secondaryColor = brandColors[1] || secondary; return `

${slide.title || `Slide ${index + 1}`}

${slide.content || "Content goes here..."}
`; }; const handleGenerate = async () => { if (!topic.trim()) { setError("Please enter a topic for your presentation"); return; } const apiKey = apiKeys[selectedProvider]; const isQwenOAuth = selectedProvider === "qwen" && modelAdapter.hasQwenAuth(); if (!isQwenOAuth && (!apiKey || !apiKey.trim())) { setError(`Please configure your ${selectedProvider.toUpperCase()} API key in Settings`); return; } setProcessing(true); setError(null); setCurrentSlide(0); console.log("[SlidesGenerator] Starting animated slides generation...", { selectedProvider, selectedModel, topic, language, theme, animationStyle, attachedFilesCount: attachedFiles.length }); try { const languageName = LANGUAGES.find(l => l.code === language)?.name || "English"; const audienceConfig = AUDIENCES.find(a => a.id === audience); const animConfig = ANIMATION_STYLES.find(a => a.id === animationStyle); const themeConfig = THEMES.find(t => t.id === theme); const fileContext = buildFileContext(); // Build enhanced topic with file context const enhancedTopic = `${topic}${fileContext}`; const result = await modelAdapter.generateSlides( enhancedTopic, { language: languageName, theme, slideCount, audience: audienceConfig?.name || "General Audience", organization, animationStyle: animConfig?.name, audienceStyle: audienceConfig?.style, themeColors: themeConfig?.colors, brandColors: attachedFiles.flatMap(f => f.colors || []).slice(0, 5), }, selectedProvider, selectedModel ); console.log("[SlidesGenerator] Generation result:", result); if (result.success && result.data) { const presentation = parseSlides(result.data); if (presentation) { setSlidesPresentation(presentation); } else { setSlidesPresentation({ id: Math.random().toString(36).substr(2, 9), title: topic.slice(0, 50), subtitle: "", organization, theme: theme as any, language: languageName, slides: [{ id: "slide-1", title: "Generated Content", content: result.data, htmlContent: `
${result.data}
`, layout: "content", order: 1, }], rawContent: result.data, createdAt: new Date(), updatedAt: new Date(), }); } } else { console.error("[SlidesGenerator] Generation failed:", result.error); setError(result.error || "Failed to generate slides"); } } catch (err) { console.error("[SlidesGenerator] Generation error:", err); setError(err instanceof Error ? err.message : "An error occurred"); } finally { setProcessing(false); } }; const handleCopy = async () => { if (slidesPresentation?.rawContent) { await navigator.clipboard.writeText(slidesPresentation.rawContent); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; const handleDownloadHtml = () => { if (!slidesPresentation) return; const themeConfig = THEMES.find(t => t.id === slidesPresentation.theme) || THEMES[1]; const [bg, accent, secondary, text] = themeConfig.colors; const brandColors = attachedFiles.flatMap(f => f.colors || []); const primaryColor = brandColors[0] || accent; const secondaryColor = brandColors[1] || secondary; const html = ` ${slidesPresentation.title}
${organization ? `` : ''}
${slidesPresentation.slides.map((slide, i) => `
${slide.htmlContent}
`).join('')}
1 / ${slidesPresentation.slides.length}
`; const blob = new Blob([html], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${slidesPresentation.title.replace(/[^a-z0-9]/gi, '_')}_animated_presentation.html`; a.click(); URL.revokeObjectURL(url); }; const toggleFullscreen = () => { if (!slideContainerRef.current) return; if (!document.fullscreenElement) { slideContainerRef.current.requestFullscreen().catch(console.error); setIsFullscreen(true); } else { document.exitFullscreen(); setIsFullscreen(false); } }; const goToSlide = (index: number) => { if (slidesPresentation?.slides) { setCurrentSlide(Math.max(0, Math.min(index, slidesPresentation.slides.length - 1))); } }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; return (bytes / (1024 * 1024)).toFixed(1) + " MB"; }; return (
{/* Input Panel */}
{t.title} PRO
{t.description}
{/* AI Provider Selection */}
{(["qwen", "ollama", "zai"] as const).map((provider) => ( ))}
{/* Model Selection */}
{/* Topic Input */}