From 571aa9f69404eda583ab88667e822115c4e680d2 Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Sat, 27 Dec 2025 20:51:59 +0400 Subject: [PATCH] feat: Add Slides Generator tool with multi-language support and HTML5 presentation design - Added SlidesPresentation and Slide types - Implemented generateSlides method in all services (Qwen, Ollama, Z.AI) - Created stunning SlidesGenerator component with: - 18 language support (English, Chinese, Spanish, French, etc.) - 6 theme options (Corporate, Modern, Minimal, Dark, Vibrant, Gradient) - 7 audience presets (Executives, Investors, Technical, etc.) - HTML5 slide preview with navigation - Fullscreen presentation mode - Auto-play functionality - Export to standalone HTML file - Slide thumbnails and speaker notes - Updated sidebar navigation with new Slides Generator menu item - Updated store with slidesPresentation state management --- app/page.tsx | 4 + components/Sidebar.tsx | 5 +- components/SlidesGenerator.tsx | 727 +++++++++++++++++++++++++++++++++ lib/services/model-adapter.ts | 19 +- lib/services/ollama-cloud.ts | 92 +++++ lib/services/qwen-oauth.ts | 92 +++++ lib/services/zai-plan.ts | 96 ++++- lib/store.ts | 7 +- types/index.ts | 24 ++ 9 files changed, 1060 insertions(+), 6 deletions(-) create mode 100644 components/SlidesGenerator.tsx diff --git a/app/page.tsx b/app/page.tsx index f2b1911..ade7bd4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,6 +7,7 @@ import PromptEnhancer from "@/components/PromptEnhancer"; import PRDGenerator from "@/components/PRDGenerator"; import ActionPlanGenerator from "@/components/ActionPlanGenerator"; import UXDesignerPrompt from "@/components/UXDesignerPrompt"; +import SlidesGenerator from "@/components/SlidesGenerator"; import HistoryPanel from "@/components/HistoryPanel"; import SettingsPanel from "@/components/SettingsPanel"; import modelAdapter from "@/lib/services/adapter-instance"; @@ -29,6 +30,8 @@ export default function Home() { return ; case "uxdesigner": return ; + case "slides": + return ; case "history": return ; case "settings": @@ -49,3 +52,4 @@ export default function Home() { ); } + diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 78dd639..69193a9 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -3,10 +3,10 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import useStore from "@/lib/store"; -import { Sparkles, FileText, ListTodo, Palette, History, Settings, Github, Menu, X } from "lucide-react"; +import { Sparkles, FileText, ListTodo, Palette, Presentation, History, Settings, Github, Menu, X } from "lucide-react"; import { cn } from "@/lib/utils"; -export type View = "enhance" | "prd" | "action" | "uxdesigner" | "history" | "settings"; +export type View = "enhance" | "prd" | "action" | "uxdesigner" | "slides" | "history" | "settings"; interface SidebarProps { currentView: View; @@ -22,6 +22,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) { { id: "prd" as View, label: "PRD Generator", icon: FileText }, { id: "action" as View, label: "Action Plan", icon: ListTodo }, { id: "uxdesigner" as View, label: "UX Designer", icon: Palette }, + { id: "slides" as View, label: "Slides Generator", icon: Presentation }, { id: "history" as View, label: "History", icon: History, count: history.length }, { id: "settings" as View, label: "Settings", icon: Settings }, ]; diff --git a/components/SlidesGenerator.tsx b/components/SlidesGenerator.tsx new file mode 100644 index 0000000..df87d1e --- /dev/null +++ b/components/SlidesGenerator.tsx @@ -0,0 +1,727 @@ +"use client"; + +import { useState, useEffect, useRef } 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, + RotateCcw, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +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", name: "Corporate", colors: ["#1e3a5f", "#2563eb", "#ffffff"], icon: "🏢" }, + { id: "modern", name: "Modern", colors: ["#0f172a", "#6366f1", "#f8fafc"], icon: "✨" }, + { id: "minimal", name: "Minimal", colors: ["#ffffff", "#374151", "#f3f4f6"], icon: "◻️" }, + { id: "dark", name: "Dark Mode", colors: ["#0a0a0a", "#a855f7", "#fafafa"], icon: "🌙" }, + { id: "vibrant", name: "Vibrant", colors: ["#7c3aed", "#ec4899", "#fef3c7"], icon: "🎨" }, + { id: "gradient", name: "Gradient", colors: ["#667eea", "#764ba2", "#ffffff"], icon: "🌈" }, +]; + +const AUDIENCES = [ + { id: "executives", name: "Executives & C-Suite", icon: "👔" }, + { id: "investors", name: "Investors & Stakeholders", icon: "💼" }, + { id: "technical", name: "Technical Team", icon: "💻" }, + { id: "marketing", name: "Marketing & Sales", icon: "📈" }, + { id: "general", name: "General Audience", icon: "👥" }, + { id: "students", name: "Students & Educators", icon: "🎓" }, + { id: "customers", name: "Customers & Clients", icon: "🤝" }, +]; + +export default function SlidesGenerator() { + const { + selectedProvider, + selectedModels, + availableModels, + apiKeys, + isProcessing, + error, + slidesPresentation, + setSelectedProvider, + setSlidesPresentation, + setProcessing, + setError, + setAvailableModels, + setSelectedModel, + } = useStore(); + + const [topic, setTopic] = useState(""); + const [language, setLanguage] = useState("en"); + const [theme, setTheme] = useState("modern"); + const [audience, setAudience] = useState("general"); + const [organization, setOrganization] = useState(""); + const [slideCount, setSlideCount] = useState(8); + 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 slideContainerRef = useRef(null); + const autoPlayRef = 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); + } + }; + + const parseSlides = (content: string): SlidesPresentation | null => { + try { + // Try to extract JSON from markdown code blocks + 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 || generateDefaultHtml(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 generateDefaultHtml = (slide: any, index: number): string => { + const themeConfig = THEMES.find(t => t.id === theme) || THEMES[1]; + const [bg, accent, text] = themeConfig.colors; + + 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 slides generation...", { + selectedProvider, + selectedModel, + topic, + language, + theme + }); + + try { + const languageName = LANGUAGES.find(l => l.code === language)?.name || "English"; + const audienceName = AUDIENCES.find(a => a.id === audience)?.name || "General Audience"; + + const result = await modelAdapter.generateSlides( + topic, + { + language: languageName, + theme, + slideCount, + audience: audienceName, + organization, + }, + selectedProvider, + selectedModel + ); + + console.log("[SlidesGenerator] Generation result:", result); + + if (result.success && result.data) { + const presentation = parseSlides(result.data); + if (presentation) { + setSlidesPresentation(presentation); + } else { + // Fallback: create a simple presentation with the raw content + 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, text] = themeConfig.colors; + + const html = ` + + + + + ${slidesPresentation.title} + + + + +
+ ${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, '_')}_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))); + } + }; + + return ( +
+ {/* Input Panel */} + + + +
+ +
+ Slides Generator +
+ + Generate stunning HTML5 presentation slides with multi-language support + +
+ + {/* AI Provider Selection */} +
+ +
+ {(["qwen", "ollama", "zai"] as const).map((provider) => ( + + ))} +
+
+ + {/* Model Selection */} +
+ + +
+ + {/* Topic Input */} +
+ +