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 PRDGenerator from "@/components/PRDGenerator";
|
||||||
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
import ActionPlanGenerator from "@/components/ActionPlanGenerator";
|
||||||
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
import UXDesignerPrompt from "@/components/UXDesignerPrompt";
|
||||||
|
import SlidesGenerator from "@/components/SlidesGenerator";
|
||||||
import HistoryPanel from "@/components/HistoryPanel";
|
import HistoryPanel from "@/components/HistoryPanel";
|
||||||
import SettingsPanel from "@/components/SettingsPanel";
|
import SettingsPanel from "@/components/SettingsPanel";
|
||||||
import modelAdapter from "@/lib/services/adapter-instance";
|
import modelAdapter from "@/lib/services/adapter-instance";
|
||||||
@@ -29,6 +30,8 @@ export default function Home() {
|
|||||||
return <ActionPlanGenerator />;
|
return <ActionPlanGenerator />;
|
||||||
case "uxdesigner":
|
case "uxdesigner":
|
||||||
return <UXDesignerPrompt />;
|
return <UXDesignerPrompt />;
|
||||||
|
case "slides":
|
||||||
|
return <SlidesGenerator />;
|
||||||
case "history":
|
case "history":
|
||||||
return <HistoryPanel />;
|
return <HistoryPanel />;
|
||||||
case "settings":
|
case "settings":
|
||||||
@@ -49,3 +52,4 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useStore from "@/lib/store";
|
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";
|
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 {
|
interface SidebarProps {
|
||||||
currentView: View;
|
currentView: View;
|
||||||
@@ -22,6 +22,7 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
|||||||
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
{ id: "prd" as View, label: "PRD Generator", icon: FileText },
|
||||||
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
{ id: "action" as View, label: "Action Plan", icon: ListTodo },
|
||||||
{ id: "uxdesigner" as View, label: "UX Designer", icon: Palette },
|
{ 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: "history" as View, label: "History", icon: History, count: history.length },
|
||||||
{ id: "settings" as View, label: "Settings", icon: Settings },
|
{ 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);
|
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(
|
async chatCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
model: string,
|
model: string,
|
||||||
|
|||||||
@@ -303,6 +303,98 @@ Make's prompt specific, inspiring, and comprehensive. Use professional UX termin
|
|||||||
|
|
||||||
return this.chatCompletion([systemMessage, userMessage], model || "gpt-oss:120b");
|
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;
|
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");
|
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[]>> {
|
async listModels(): Promise<APIResponse<string[]>> {
|
||||||
const models = [
|
const models = [
|
||||||
"coder-model",
|
"coder-model",
|
||||||
|
|||||||
@@ -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);
|
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;
|
export default ZaiPlanService;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan } from "@/types";
|
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation } from "@/types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPrompt: string;
|
currentPrompt: string;
|
||||||
enhancedPrompt: string | null;
|
enhancedPrompt: string | null;
|
||||||
prd: PRD | null;
|
prd: PRD | null;
|
||||||
actionPlan: ActionPlan | null;
|
actionPlan: ActionPlan | null;
|
||||||
|
slidesPresentation: SlidesPresentation | null;
|
||||||
selectedProvider: ModelProvider;
|
selectedProvider: ModelProvider;
|
||||||
selectedModels: Record<ModelProvider, string>;
|
selectedModels: Record<ModelProvider, string>;
|
||||||
availableModels: Record<ModelProvider, string[]>;
|
availableModels: Record<ModelProvider, string[]>;
|
||||||
@@ -27,6 +28,7 @@ interface AppState {
|
|||||||
setEnhancedPrompt: (enhanced: string | null) => void;
|
setEnhancedPrompt: (enhanced: string | null) => void;
|
||||||
setPRD: (prd: PRD) => void;
|
setPRD: (prd: PRD) => void;
|
||||||
setActionPlan: (plan: ActionPlan) => void;
|
setActionPlan: (plan: ActionPlan) => void;
|
||||||
|
setSlidesPresentation: (slides: SlidesPresentation | null) => void;
|
||||||
setSelectedProvider: (provider: ModelProvider) => void;
|
setSelectedProvider: (provider: ModelProvider) => void;
|
||||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||||
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
setAvailableModels: (provider: ModelProvider, models: string[]) => void;
|
||||||
@@ -44,6 +46,7 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
selectedProvider: "qwen",
|
selectedProvider: "qwen",
|
||||||
selectedModels: {
|
selectedModels: {
|
||||||
qwen: "coder-model",
|
qwen: "coder-model",
|
||||||
@@ -68,6 +71,7 @@ const useStore = create<AppState>((set) => ({
|
|||||||
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
setEnhancedPrompt: (enhanced) => set({ enhancedPrompt: enhanced }),
|
||||||
setPRD: (prd) => set({ prd }),
|
setPRD: (prd) => set({ prd }),
|
||||||
setActionPlan: (plan) => set({ actionPlan: plan }),
|
setActionPlan: (plan) => set({ actionPlan: plan }),
|
||||||
|
setSlidesPresentation: (slides) => set({ slidesPresentation: slides }),
|
||||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||||
setSelectedModel: (provider, model) =>
|
setSelectedModel: (provider, model) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -102,6 +106,7 @@ const useStore = create<AppState>((set) => ({
|
|||||||
enhancedPrompt: null,
|
enhancedPrompt: null,
|
||||||
prd: null,
|
prd: null,
|
||||||
actionPlan: null,
|
actionPlan: null,
|
||||||
|
slidesPresentation: null,
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -91,3 +91,27 @@ export interface ChatMessage {
|
|||||||
role: "system" | "user" | "assistant";
|
role: "system" | "user" | "assistant";
|
||||||
content: string;
|
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