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
This commit is contained in:
@@ -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 <ActionPlanGenerator />;
|
||||
case "uxdesigner":
|
||||
return <UXDesignerPrompt />;
|
||||
case "slides":
|
||||
return <SlidesGenerator />;
|
||||
case "history":
|
||||
return <HistoryPanel />;
|
||||
case "settings":
|
||||
@@ -49,3 +52,4 @@ export default function Home() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
727
components/SlidesGenerator.tsx
Normal file
727
components/SlidesGenerator.tsx
Normal file
@@ -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<HTMLDivElement>(null);
|
||||
const autoPlayRef = useRef<NodeJS.Timeout | null>(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 `
|
||||
<div style="
|
||||
min-height: 100%;
|
||||
padding: 3rem;
|
||||
background: linear-gradient(135deg, ${bg} 0%, ${accent}22 100%);
|
||||
color: ${theme === 'minimal' ? '#1f2937' : text};
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
">
|
||||
<h2 style="
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
background: linear-gradient(90deg, ${accent}, ${accent}cc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
">${slide.title || `Slide ${index + 1}`}</h2>
|
||||
<div style="font-size: 1.25rem; line-height: 1.8; opacity: 0.9;">
|
||||
${slide.content || "Content goes here..."}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
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: `
|
||||
<div style="padding: 2rem; font-family: system-ui;">
|
||||
<pre style="white-space: pre-wrap; font-size: 0.875rem;">${result.data}</pre>
|
||||
</div>
|
||||
`,
|
||||
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 = `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${slidesPresentation.title}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Inter', system-ui, sans-serif; background: ${bg}; color: ${text}; }
|
||||
.slides-container { width: 100vw; height: 100vh; overflow: hidden; position: relative; }
|
||||
.slide { width: 100%; height: 100%; display: none; animation: fadeIn 0.5s ease; }
|
||||
.slide.active { display: block; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.controls { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
||||
display: flex; gap: 1rem; background: rgba(0,0,0,0.8); padding: 0.75rem 1.5rem; border-radius: 2rem; }
|
||||
.controls button { background: ${accent}; color: white; border: none; padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem; cursor: pointer; font-weight: 500; transition: all 0.2s; }
|
||||
.controls button:hover { transform: scale(1.05); }
|
||||
.slide-counter { position: fixed; bottom: 2rem; right: 2rem; background: rgba(0,0,0,0.6);
|
||||
padding: 0.5rem 1rem; border-radius: 1rem; font-size: 0.875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slides-container">
|
||||
${slidesPresentation.slides.map((slide, i) => `
|
||||
<div class="slide${i === 0 ? ' active' : ''}" data-slide="${i}">
|
||||
${slide.htmlContent}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button onclick="prevSlide()">← Previous</button>
|
||||
<button onclick="nextSlide()">Next →</button>
|
||||
</div>
|
||||
<div class="slide-counter"><span id="current">1</span> / ${slidesPresentation.slides.length}</div>
|
||||
<script>
|
||||
let current = 0;
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
const counter = document.getElementById('current');
|
||||
|
||||
function showSlide(n) {
|
||||
slides.forEach(s => s.classList.remove('active'));
|
||||
current = (n + slides.length) % slides.length;
|
||||
slides[current].classList.add('active');
|
||||
counter.textContent = current + 1;
|
||||
}
|
||||
|
||||
function nextSlide() { showSlide(current + 1); }
|
||||
function prevSlide() { showSlide(current - 1); }
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') nextSlide();
|
||||
if (e.key === 'ArrowLeft') prevSlide();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
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 (
|
||||
<div className="mx-auto grid max-w-7xl gap-4 lg:gap-6 grid-cols-1 xl:grid-cols-2">
|
||||
{/* Input Panel */}
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="p-4 lg:p-6">
|
||||
<CardTitle className="flex items-center gap-2 text-base lg:text-lg">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 text-white">
|
||||
<Presentation className="h-4 w-4" />
|
||||
</div>
|
||||
Slides Generator
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
Generate stunning HTML5 presentation slides with multi-language support
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 lg:space-y-5 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
{/* AI Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">AI Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
variant={selectedProvider === provider ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
className="capitalize text-xs lg:text-sm h-8 lg:h-9 px-2.5 lg:px-3"
|
||||
>
|
||||
{provider === "qwen" ? "Qwen" : provider === "ollama" ? "Ollama" : "Z.AI"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Model</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{models.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Topic Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium">Presentation Topic</label>
|
||||
<Textarea
|
||||
placeholder="e.g., Q4 2024 Company Performance Review, AI in Healthcare: Transforming Patient Care, Product Launch Strategy for Global Markets..."
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
className="min-h-[100px] lg:min-h-[120px] resize-y text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Language & Theme Row */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||
<Globe className="h-3.5 w-3.5 text-blue-500" />
|
||||
Language
|
||||
</label>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.nativeName} ({lang.name})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs lg:text-sm font-medium flex items-center gap-1.5">
|
||||
<Palette className="h-3.5 w-3.5 text-purple-500" />
|
||||
Theme
|
||||
</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs lg:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{THEMES.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.icon} {t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
<button
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{showAdvanced ? "Hide" : "Show"} Advanced Options
|
||||
</button>
|
||||
|
||||
{/* Advanced Options */}
|
||||
{showAdvanced && (
|
||||
<div className="space-y-3 p-3 rounded-lg bg-muted/30 border">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||
<Users className="h-3.5 w-3.5 text-green-500" />
|
||||
Target Audience
|
||||
</label>
|
||||
<select
|
||||
value={audience}
|
||||
onChange={(e) => setAudience(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{AUDIENCES.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.icon} {a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||
<Hash className="h-3.5 w-3.5 text-orange-500" />
|
||||
Number of Slides
|
||||
</label>
|
||||
<select
|
||||
value={slideCount}
|
||||
onChange={(e) => setSlideCount(parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{[5, 8, 10, 12, 15, 20].map((n) => (
|
||||
<option key={n} value={n}>{n} slides</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium flex items-center gap-1.5">
|
||||
<Building2 className="h-3.5 w-3.5 text-cyan-500" />
|
||||
Organization Name (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., Acme Corporation"
|
||||
value={organization}
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-2.5 lg:p-3 text-xs lg:text-sm text-destructive">
|
||||
{error}
|
||||
{!apiKeys[selectedProvider] && (
|
||||
<div className="mt-1.5 lg:mt-2 flex items-center gap-2">
|
||||
<Settings className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
<span className="text-[10px] lg:text-xs">Configure API key in Settings</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate Button */}
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={isProcessing || !topic.trim()}
|
||||
className="w-full h-10 lg:h-11 text-sm lg:text-base font-medium bg-gradient-to-r from-violet-600 to-purple-600 hover:from-violet-700 hover:to-purple-700"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating Slides...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Presentation className="mr-2 h-4 w-4" />
|
||||
Generate Presentation
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preview Panel */}
|
||||
<Card className={cn("overflow-hidden", !slidesPresentation && "opacity-60")}>
|
||||
<CardHeader className="p-4 lg:p-6 pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-base lg:text-lg">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckCircle2 className={cn("h-4 w-4 lg:h-5 lg:w-5", slidesPresentation ? "text-green-500" : "text-muted-foreground")} />
|
||||
Slide Preview
|
||||
</span>
|
||||
{slidesPresentation && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsAutoPlaying(!isAutoPlaying)} className="h-8 w-8">
|
||||
{isAutoPlaying ? <Pause className="h-3.5 w-3.5" /> : <Play className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8">
|
||||
{isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} className="h-8 w-8">
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
|
||||
{copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
{slidesPresentation && (
|
||||
<CardDescription className="text-xs lg:text-sm">
|
||||
{slidesPresentation.title} • {slidesPresentation.slides.length} slides • {slidesPresentation.language}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 lg:p-6 pt-0">
|
||||
{slidesPresentation ? (
|
||||
<div className="space-y-4">
|
||||
{/* Slide Display */}
|
||||
<div
|
||||
ref={slideContainerRef}
|
||||
className="relative aspect-video rounded-lg overflow-hidden border bg-slate-900 shadow-2xl"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slidesPresentation.slides[currentSlide]?.htmlContent || ""
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Navigation Arrows */}
|
||||
<button
|
||||
onClick={() => goToSlide(currentSlide - 1)}
|
||||
disabled={currentSlide === 0}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => goToSlide(currentSlide + 1)}
|
||||
disabled={currentSlide >= slidesPresentation.slides.length - 1}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* Slide Counter */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-black/60 text-white text-xs font-medium">
|
||||
{currentSlide + 1} / {slidesPresentation.slides.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slide Thumbnails */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{slidesPresentation.slides.map((slide, index) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
onClick={() => setCurrentSlide(index)}
|
||||
className={cn(
|
||||
"flex-shrink-0 w-20 h-12 rounded border-2 overflow-hidden transition-all",
|
||||
currentSlide === index
|
||||
? "border-violet-500 ring-2 ring-violet-500/30"
|
||||
: "border-muted hover:border-violet-300"
|
||||
)}
|
||||
>
|
||||
<div className="w-full h-full bg-slate-800 flex items-center justify-center text-[8px] text-white/70 font-medium">
|
||||
{index + 1}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Current Slide Info */}
|
||||
<div className="p-3 rounded-lg bg-muted/30 border">
|
||||
<h4 className="font-medium text-sm mb-1">
|
||||
{slidesPresentation.slides[currentSlide]?.title}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{slidesPresentation.slides[currentSlide]?.content}
|
||||
</p>
|
||||
{slidesPresentation.slides[currentSlide]?.notes && (
|
||||
<p className="text-xs text-blue-500 mt-2 italic">
|
||||
Notes: {slidesPresentation.slides[currentSlide]?.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[300px] lg:h-[400px] items-center justify-center text-center">
|
||||
<div className="space-y-3">
|
||||
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500/20 to-purple-500/20 flex items-center justify-center">
|
||||
<Presentation className="h-8 w-8 text-violet-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">No presentation yet</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
Enter a topic and generate your slides
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -186,6 +186,23 @@ export class ModelAdapter {
|
||||
return this.callWithFallback((service) => service.generateUXDesignerPrompt(appDescription, model), providers);
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
} = {},
|
||||
provider?: ModelProvider,
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||
return this.callWithFallback((service) => service.generateSlides(topic, options, model), providers);
|
||||
}
|
||||
|
||||
async chatCompletion(
|
||||
messages: ChatMessage[],
|
||||
model: string,
|
||||
@@ -222,7 +239,7 @@ export class ModelAdapter {
|
||||
zai: ["glm-4.7", "glm-4.5", "glm-4.5-air", "glm-4-flash", "glm-4-flashx"],
|
||||
};
|
||||
const models: Record<ModelProvider, string[]> = { ...fallbackModels };
|
||||
|
||||
|
||||
if (provider === "ollama" || !provider) {
|
||||
try {
|
||||
const ollamaModels = await this.ollamaService.listModels();
|
||||
|
||||
@@ -303,6 +303,98 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const { language = "English", theme = "modern", slideCount = 10, audience = "general", organization = "" } = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class presentation designer and content strategist specializing in creating stunning, impactful slide decks for organizations and professionals.
|
||||
|
||||
Your task is to generate a complete presentation with HTML5-ready slide content. Each slide should be designed with modern, visually impressive aesthetics.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return a JSON object with the following structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Presentation Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Main content text",
|
||||
"htmlContent": "<div class='slide'>Complete HTML content for the slide</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|quote|statistics|timeline|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN GUIDELINES:
|
||||
1. Create ${slideCount} slides maximum
|
||||
2. Use the "${theme}" design theme
|
||||
3. All content MUST be in ${language}
|
||||
4. Target audience: ${audience}
|
||||
${organization ? `5. Organization: ${organization}` : ""}
|
||||
|
||||
SLIDE TYPES TO INCLUDE:
|
||||
- Title slide with compelling headline
|
||||
- Problem/Opportunity statement
|
||||
- Key insights with data visualizations
|
||||
- Solution/Strategy overview
|
||||
- Timeline or roadmap if applicable
|
||||
- Key metrics or statistics
|
||||
- Call-to-action or next steps
|
||||
- Summary/Conclusion slide
|
||||
|
||||
HTML CONTENT REQUIREMENTS:
|
||||
- Use semantic HTML5 elements
|
||||
- Include inline CSS for styling
|
||||
- Use modern gradients, shadows, and animations
|
||||
- Incorporate icons using Unicode or SVG
|
||||
- Ensure responsive design considerations
|
||||
- Use professional typography (specify font-family)
|
||||
- Include color schemes matching the theme
|
||||
|
||||
CONTENT QUALITY:
|
||||
- Concise, impactful headlines (max 8 words)
|
||||
- Bullet points with 3-5 items maximum
|
||||
- Relevant statistics with compelling visuals
|
||||
- Professional, business-appropriate language
|
||||
- Clear narrative flow between slides`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a stunning presentation about: ${topic}
|
||||
|
||||
Requirements:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Number of slides: ${slideCount}
|
||||
- Target audience: ${audience}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
|
||||
Generate the complete presentation with HTML5 content for each slide. Make it visually impressive and content-rich.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
||||
}
|
||||
}
|
||||
|
||||
export default OllamaCloudService;
|
||||
|
||||
@@ -631,6 +631,98 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const { language = "English", theme = "modern", slideCount = 10, audience = "general", organization = "" } = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class presentation designer and content strategist specializing in creating stunning, impactful slide decks for organizations and professionals.
|
||||
|
||||
Your task is to generate a complete presentation with HTML5-ready slide content. Each slide should be designed with modern, visually impressive aesthetics.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return a JSON object with the following structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Presentation Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Main content text",
|
||||
"htmlContent": "<div class='slide'>Complete HTML content for the slide</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|quote|statistics|timeline|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN GUIDELINES:
|
||||
1. Create ${slideCount} slides maximum
|
||||
2. Use the "${theme}" design theme
|
||||
3. All content MUST be in ${language}
|
||||
4. Target audience: ${audience}
|
||||
${organization ? `5. Organization: ${organization}` : ""}
|
||||
|
||||
SLIDE TYPES TO INCLUDE:
|
||||
- Title slide with compelling headline
|
||||
- Problem/Opportunity statement
|
||||
- Key insights with data visualizations
|
||||
- Solution/Strategy overview
|
||||
- Timeline or roadmap if applicable
|
||||
- Key metrics or statistics
|
||||
- Call-to-action or next steps
|
||||
- Summary/Conclusion slide
|
||||
|
||||
HTML CONTENT REQUIREMENTS:
|
||||
- Use semantic HTML5 elements
|
||||
- Include inline CSS for styling
|
||||
- Use modern gradients, shadows, and animations
|
||||
- Incorporate icons using Unicode or SVG
|
||||
- Ensure responsive design considerations
|
||||
- Use professional typography (specify font-family)
|
||||
- Include color schemes matching the theme
|
||||
|
||||
CONTENT QUALITY:
|
||||
- Concise, impactful headlines (max 8 words)
|
||||
- Bullet points with 3-5 items maximum
|
||||
- Relevant statistics with compelling visuals
|
||||
- Professional, business-appropriate language
|
||||
- Clear narrative flow between slides`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a stunning presentation about: ${topic}
|
||||
|
||||
Requirements:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Number of slides: ${slideCount}
|
||||
- Target audience: ${audience}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
|
||||
Generate the complete presentation with HTML5 content for each slide. Make it visually impressive and content-rich.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "coder-model");
|
||||
}
|
||||
|
||||
async listModels(): Promise<APIResponse<string[]>> {
|
||||
const models = [
|
||||
"coder-model",
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ZaiPlanService {
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Z.AI] Response data:", data);
|
||||
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message) {
|
||||
return { success: true, data: data.choices[0].message.content };
|
||||
} else if (data.output && data.output.choices && data.output.choices[0]) {
|
||||
@@ -168,7 +168,7 @@ Include specific recommendations for:
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data?.map((m: any) => m.id) || [];
|
||||
|
||||
|
||||
return { success: true, data: models };
|
||||
} else {
|
||||
console.log("[Z.AI] No API key, using fallback models");
|
||||
@@ -251,6 +251,98 @@ Make the prompt specific, inspiring, and comprehensive. Use professional UX term
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
|
||||
async generateSlides(
|
||||
topic: string,
|
||||
options: {
|
||||
language?: string;
|
||||
theme?: string;
|
||||
slideCount?: number;
|
||||
audience?: string;
|
||||
organization?: string;
|
||||
} = {},
|
||||
model?: string
|
||||
): Promise<APIResponse<string>> {
|
||||
const { language = "English", theme = "modern", slideCount = 10, audience = "general", organization = "" } = options;
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
role: "system",
|
||||
content: `You are a world-class presentation designer and content strategist specializing in creating stunning, impactful slide decks for organizations and professionals.
|
||||
|
||||
Your task is to generate a complete presentation with HTML5-ready slide content. Each slide should be designed with modern, visually impressive aesthetics.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Return a JSON object with the following structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"title": "Presentation Title",
|
||||
"subtitle": "Presentation Subtitle",
|
||||
"theme": "${theme}",
|
||||
"language": "${language}",
|
||||
"slides": [
|
||||
{
|
||||
"id": "slide-1",
|
||||
"title": "Slide Title",
|
||||
"content": "Main content text",
|
||||
"htmlContent": "<div class='slide'>Complete HTML content for the slide</div>",
|
||||
"notes": "Speaker notes",
|
||||
"layout": "title|content|two-column|quote|statistics|timeline|comparison",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
DESIGN GUIDELINES:
|
||||
1. Create ${slideCount} slides maximum
|
||||
2. Use the "${theme}" design theme
|
||||
3. All content MUST be in ${language}
|
||||
4. Target audience: ${audience}
|
||||
${organization ? `5. Organization: ${organization}` : ""}
|
||||
|
||||
SLIDE TYPES TO INCLUDE:
|
||||
- Title slide with compelling headline
|
||||
- Problem/Opportunity statement
|
||||
- Key insights with data visualizations
|
||||
- Solution/Strategy overview
|
||||
- Timeline or roadmap if applicable
|
||||
- Key metrics or statistics
|
||||
- Call-to-action or next steps
|
||||
- Summary/Conclusion slide
|
||||
|
||||
HTML CONTENT REQUIREMENTS:
|
||||
- Use semantic HTML5 elements
|
||||
- Include inline CSS for styling
|
||||
- Use modern gradients, shadows, and animations
|
||||
- Incorporate icons using Unicode or SVG
|
||||
- Ensure responsive design considerations
|
||||
- Use professional typography (specify font-family)
|
||||
- Include color schemes matching the theme
|
||||
|
||||
CONTENT QUALITY:
|
||||
- Concise, impactful headlines (max 8 words)
|
||||
- Bullet points with 3-5 items maximum
|
||||
- Relevant statistics with compelling visuals
|
||||
- Professional, business-appropriate language
|
||||
- Clear narrative flow between slides`,
|
||||
};
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: `Create a stunning presentation about: ${topic}
|
||||
|
||||
Requirements:
|
||||
- Language: ${language}
|
||||
- Theme: ${theme}
|
||||
- Number of slides: ${slideCount}
|
||||
- Target audience: ${audience}
|
||||
${organization ? `- Organization: ${organization}` : ""}
|
||||
|
||||
Generate the complete presentation with HTML5 content for each slide. Make it visually impressive and content-rich.`,
|
||||
};
|
||||
|
||||
return this.chatCompletion([systemMessage, userMessage], model || "glm-4.7", true);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZaiPlanService;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { create } from "zustand";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation } from "@/types";
|
||||
|
||||
interface AppState {
|
||||
currentPrompt: string;
|
||||
enhancedPrompt: string | null;
|
||||
prd: PRD | null;
|
||||
actionPlan: ActionPlan | null;
|
||||
slidesPresentation: SlidesPresentation | null;
|
||||
selectedProvider: ModelProvider;
|
||||
selectedModels: Record<ModelProvider, string>;
|
||||
availableModels: Record<ModelProvider, string[]>;
|
||||
@@ -27,6 +28,7 @@ interface AppState {
|
||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||
setPRD: (prd: PRD) => void;
|
||||
setActionPlan: (plan: ActionPlan) => void;
|
||||
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||
setSelectedProvider: (provider: ModelProvider) => void;
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||
@@ -44,6 +46,7 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
selectedProvider: "qwen",
|
||||
selectedModels: {
|
||||
qwen: "coder-model",
|
||||
@@ -68,6 +71,7 @@ const useStore = create<AppState>((set) => ({
|
||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||
setPRD: (prd) => set({ prd }),
|
||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||
setSelectedModel: (provider, model) =>
|
||||
set((state) => ({
|
||||
@@ -102,6 +106,7 @@ const useStore = create<AppState>((set) => ({
|
||||
enhancedPrompt: null,
|
||||
prd: null,
|
||||
actionPlan: null,
|
||||
slidesPresentation: null,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -91,3 +91,27 @@ export interface ChatMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Slide {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
htmlContent: string;
|
||||
notes?: string;
|
||||
layout: "title" | "content" | "two-column" | "image-left" | "image-right" | "quote" | "statistics" | "timeline" | "comparison";
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface SlidesPresentation {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
author?: string;
|
||||
organization?: string;
|
||||
theme: "corporate" | "modern" | "minimal" | "dark" | "vibrant" | "gradient";
|
||||
language: string;
|
||||
slides: Slide[];
|
||||
rawContent: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user