Skip to content

[Solutions]: AI Slides Generator #158

@kstij1

Description

@kstij1

AI Slides Generator – Complete Build and Integration Guide

Part 1: Building the Standalone AI Slides Generator

What You're Building

The AI Slides Generator is a web application that transforms text prompts into professional presentations. Users enter a topic or description, and the app generates structured slide decks with titles, content, and layouts. The app includes an interactive editor for reordering slides, editing content, and applying themes, plus export functionality for PDF and PowerPoint formats.

Core Features

  • Prompt-to-Slides Generation: Convert natural language descriptions into structured presentations
  • Interactive Editor: Drag-and-drop slide reordering, inline content editing, theme customization
  • Multiple Export Formats: PDF, PPTX, and shareable web links
  • Template System: Pre-built layouts for different presentation types
  • Real-time Preview: Live updates as users edit content

Tech Stack

Frontend:

  • Next.js 14 with App Router
  • TypeScript for type safety
  • Tailwind CSS for styling
  • React DnD for drag-and-drop functionality
  • React Hook Form for form management

Backend:

  • Next.js API routes
  • OpenAI GPT-4 for content generation
  • jsPDF for PDF export
  • PptxGenJS for PowerPoint export
  • MongoDB for data persistence

Development Tools:

  • ESLint and Prettier for code quality
  • Jest for testing
  • Docker for containerization

Project Structure

slides-generator/
├── app/
│   ├── (auth)/
│   │   └── login/
│   ├── (dashboard)/
│   │   ├── slides/
│   │   │   ├── page.tsx
│   │   │   ├── new/
│   │   │   │   └── page.tsx
│   │   │   └── [id]/
│   │   │       └── page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   ├── slides/
│   │   │   ├── route.ts
│   │   │   ├── [id]/
│   │   │   │   └── route.ts
│   │   │   └── generate/
│   │   │       └── route.ts
│   │   └── export/
│   │       └── route.ts
│   ├── globals.css
│   └── layout.tsx
├── components/
│   ├── slides/
│   │   ├── SlideCanvas.tsx
│   │   ├── SlideOutline.tsx
│   │   ├── ContentEditor.tsx
│   │   ├── ThemeSelector.tsx
│   │   └── ExportModal.tsx
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Textarea.tsx
│   │   └── Modal.tsx
│   └── layout/
│       ├── Header.tsx
│       └── Sidebar.tsx
├── lib/
│   ├── ai/
│   │   └── openai.ts
│   ├── export/
│   │   ├── pdf.ts
│   │   └── pptx.ts
│   ├── database/
│   │   └── mongodb.ts
│   └── utils.ts
├── types/
│   └── slides.ts
├── public/
│   └── icons/
├── package.json
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
└── .env.local

Data Models

// types/slides.ts
export interface Slide {
  id: string;
  title: string;
  content: string[];
  layout: 'title' | 'content' | 'image' | 'chart' | 'comparison';
  order: number;
  notes?: string;
}

export interface Presentation {
  id: string;
  title: string;
  description?: string;
  slides: Slide[];
  theme: {
    primaryColor: string;
    secondaryColor: string;
    fontFamily: string;
    fontSize: 'small' | 'medium' | 'large';
  };
  createdAt: Date;
  updatedAt: Date;
  userId: string;
}

export interface GenerationRequest {
  prompt: string;
  slideCount?: number;
  style?: 'business' | 'academic' | 'creative' | 'minimal';
  includeImages?: boolean;
}

API Endpoints

// app/api/slides/route.ts
POST   /api/slides           // Create new presentation
GET    /api/slides           // List user's presentations

// app/api/slides/[id]/route.ts
GET    /api/slides/[id]      // Get specific presentation
PUT    /api/slides/[id]      // Update presentation
DELETE /api/slides/[id]      // Delete presentation

// app/api/slides/generate/route.ts
POST   /api/slides/generate  // Generate slides from prompt

// app/api/export/route.ts
POST   /api/export           // Export presentation to PDF/PPTX

Core Components

1. Prompt Input Component

// components/slides/PromptInput.tsx
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Textarea } from '@/components/ui/Textarea';

interface PromptInputProps {
  onSubmit: (prompt: string, options: GenerationOptions) => void;
  loading?: boolean;
}

export default function PromptInput({ onSubmit, loading }: PromptInputProps) {
  const [prompt, setPrompt] = useState('');
  const [slideCount, setSlideCount] = useState(8);
  const [style, setStyle] = useState<'business' | 'academic' | 'creative' | 'minimal'>('business');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!prompt.trim()) return;
    
    onSubmit(prompt, {
      slideCount,
      style,
      includeImages: false
    });
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      <div>
        <label className="block text-sm font-medium mb-2">
          What would you like to present?
        </label>
        <Textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Describe your presentation topic... (e.g., 'Quarterly business review for Q3 2024')"
          rows={4}
          className="w-full"
        />
      </div>
      
      <div className="grid grid-cols-2 gap-4">
        <div>
          <label className="block text-sm font-medium mb-2">Number of slides</label>
          <select
            value={slideCount}
            onChange={(e) => setSlideCount(Number(e.target.value))}
            className="w-full p-2 border rounded-md"
          >
            <option value={6}>6 slides</option>
            <option value={8}>8 slides</option>
            <option value={10}>10 slides</option>
            <option value={12}>12 slides</option>
          </select>
        </div>
        
        <div>
          <label className="block text-sm font-medium mb-2">Style</label>
          <select
            value={style}
            onChange={(e) => setStyle(e.target.value as any)}
            className="w-full p-2 border rounded-md"
          >
            <option value="business">Business</option>
            <option value="academic">Academic</option>
            <option value="creative">Creative</option>
            <option value="minimal">Minimal</option>
          </select>
        </div>
      </div>
      
      <Button type="submit" disabled={loading || !prompt.trim()}>
        {loading ? 'Generating...' : 'Generate Slides'}
      </Button>
    </form>
  );
}

2. Slide Canvas Component

// components/slides/SlideCanvas.tsx
'use client';

import { useState } from 'react';
import { Slide } from '@/types/slides';
import { ContentEditor } from './ContentEditor';

interface SlideCanvasProps {
  slides: Slide[];
  onUpdateSlide: (slideId: string, updates: Partial<Slide>) => void;
  onReorderSlides: (fromIndex: number, toIndex: number) => void;
}

export default function SlideCanvas({ slides, onUpdateSlide, onReorderSlides }: SlideCanvasProps) {
  const [selectedSlide, setSelectedSlide] = useState<string | null>(null);

  return (
    <div className="flex-1 p-6 bg-gray-50">
      <div className="max-w-4xl mx-auto space-y-6">
        {slides.map((slide, index) => (
          <div
            key={slide.id}
            className={`bg-white rounded-lg shadow-md p-6 cursor-pointer transition-all ${
              selectedSlide === slide.id ? 'ring-2 ring-blue-500' : 'hover:shadow-lg'
            }`}
            onClick={() => setSelectedSlide(slide.id)}
          >
            <div className="flex items-center justify-between mb-4">
              <span className="text-sm text-gray-500">Slide {index + 1}</span>
              <span className="text-xs bg-gray-100 px-2 py-1 rounded">
                {slide.layout}
              </span>
            </div>
            
            <ContentEditor
              slide={slide}
              onUpdate={(updates) => onUpdateSlide(slide.id, updates)}
              isSelected={selectedSlide === slide.id}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

3. AI Generation Service

// lib/ai/openai.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export async function generateSlides(prompt: string, options: GenerationOptions): Promise<Slide[]> {
  const systemPrompt = `You are a presentation expert. Generate ${options.slideCount} slides based on the user's prompt.

Return a JSON array of slides with this exact structure:
[
  {
    "id": "unique-id",
    "title": "Slide Title",
    "content": ["Bullet point 1", "Bullet point 2"],
    "layout": "title|content|image|chart|comparison",
    "order": 1
  }
]

Guidelines:
- Use the ${options.style} style
- Make titles concise and engaging
- Keep bullet points short and impactful
- Choose appropriate layouts for each slide
- Ensure logical flow between slides`;

  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: prompt }
    ],
    temperature: 0.7,
  });

  const content = response.choices[0].message.content;
  return JSON.parse(content || '[]');
}

4. Export Services

// lib/export/pdf.ts
import jsPDF from 'jspdf';

export async function exportToPDF(presentation: Presentation): Promise<Buffer> {
  const pdf = new jsPDF();
  
  presentation.slides.forEach((slide, index) => {
    if (index > 0) pdf.addPage();
    
    // Add title
    pdf.setFontSize(24);
    pdf.text(slide.title, 20, 30);
    
    // Add content
    pdf.setFontSize(12);
    slide.content.forEach((item, i) => {
      pdf.text(`• ${item}`, 20, 50 + (i * 10));
    });
  });
  
  return Buffer.from(pdf.output('arraybuffer'));
}

// lib/export/pptx.ts
import PptxGenJS from 'pptxgenjs';

export async function exportToPPTX(presentation: Presentation): Promise<Buffer> {
  const pptx = new PptxGenJS();
  
  presentation.slides.forEach(slide => {
    const slideObj = pptx.addSlide();
    
    // Add title
    slideObj.addText(slide.title, {
      x: 1, y: 1, w: 8, h: 1,
      fontSize: 24,
      bold: true
    });
    
    // Add content
    slide.content.forEach((item, i) => {
      slideObj.addText(`• ${item}`, {
        x: 1, y: 2 + (i * 0.5), w: 8, h: 0.5,
        fontSize: 14
      });
    });
  });
  
  return pptx.write('nodebuffer');
}

Database Schema

// lib/database/mongodb.ts
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI!);
export const db = client.db('slides_generator');

export const presentations = db.collection('presentations');
export const users = db.collection('users');

// Indexes
await presentations.createIndex({ userId: 1, createdAt: -1 });
await presentations.createIndex({ id: 1 }, { unique: true });

Main Application Page

// app/(dashboard)/slides/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { Presentation } from '@/types/slides';
import PromptInput from '@/components/slides/PromptInput';
import SlideCanvas from '@/components/slides/SlideCanvas';
import SlideOutline from '@/components/slides/SlideOutline';
import ExportModal from '@/components/slides/ExportModal';

export default function SlidesPage() {
  const [presentations, setPresentations] = useState<Presentation[]>([]);
  const [currentPresentation, setCurrentPresentation] = useState<Presentation | null>(null);
  const [loading, setLoading] = useState(false);
  const [showExport, setShowExport] = useState(false);

  const handleGenerateSlides = async (prompt: string, options: GenerationOptions) => {
    setLoading(true);
    try {
      const response = await fetch('/api/slides/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt, ...options }),
      });
      
      const slides = await response.json();
      const newPresentation: Presentation = {
        id: crypto.randomUUID(),
        title: 'New Presentation',
        slides,
        theme: {
          primaryColor: '#3B82F6',
          secondaryColor: '#1E40AF',
          fontFamily: 'Inter',
          fontSize: 'medium'
        },
        createdAt: new Date(),
        updatedAt: new Date(),
        userId: 'current-user-id'
      };
      
      setCurrentPresentation(newPresentation);
    } catch (error) {
      console.error('Failed to generate slides:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleUpdateSlide = (slideId: string, updates: Partial<Slide>) => {
    if (!currentPresentation) return;
    
    const updatedSlides = currentPresentation.slides.map(slide =>
      slide.id === slideId ? { ...slide, ...updates } : slide
    );
    
    setCurrentPresentation({
      ...currentPresentation,
      slides: updatedSlides,
      updatedAt: new Date()
    });
  };

  const handleSavePresentation = async () => {
    if (!currentPresentation) return;
    
    try {
      await fetch('/api/slides', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(currentPresentation),
      });
    } catch (error) {
      console.error('Failed to save presentation:', error);
    }
  };

  return (
    <div className="min-h-screen bg-gray-50">
      <div className="flex">
        {/* Sidebar */}
        <div className="w-64 bg-white shadow-sm">
          <SlideOutline
            slides={currentPresentation?.slides || []}
            onReorder={(from, to) => {
              // Handle reordering logic
            }}
          />
        </div>
        
        {/* Main Content */}
        <div className="flex-1">
          {!currentPresentation ? (
            <div className="max-w-2xl mx-auto p-8">
              <h1 className="text-3xl font-bold mb-8">AI Slides Generator</h1>
              <PromptInput onSubmit={handleGenerateSlides} loading={loading} />
            </div>
          ) : (
            <SlideCanvas
              slides={currentPresentation.slides}
              onUpdateSlide={handleUpdateSlide}
              onReorderSlides={() => {}}
            />
          )}
        </div>
      </div>
      
      {/* Export Modal */}
      {showExport && currentPresentation && (
        <ExportModal
          presentation={currentPresentation}
          onClose={() => setShowExport(false)}
        />
      )}
    </div>
  );
}

Environment Configuration

# .env.local
MONGODB_URI=mongodb://localhost:27017/slides_generator
OPENAI_API_KEY=your_openai_api_key_here
NEXTAUTH_SECRET=your_nextauth_secret_here
NEXTAUTH_URL=http://localhost:3000

Package Dependencies

{
  "dependencies": {
    "next": "14.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "5.0.0",
    "tailwindcss": "3.3.0",
    "openai": "4.0.0",
    "jspdf": "2.5.0",
    "pptxgenjs": "3.12.0",
    "mongodb": "6.0.0",
    "react-dnd": "16.0.0",
    "react-dnd-html5-backend": "16.0.0",
    "react-hook-form": "7.45.0",
    "@hookform/resolvers": "3.3.0",
    "zod": "3.22.0"
  },
  "devDependencies": {
    "@types/node": "20.0.0",
    "@types/react": "18.2.0",
    "@types/react-dom": "18.2.0",
    "eslint": "8.50.0",
    "eslint-config-next": "14.0.0",
    "prettier": "3.0.0",
    "jest": "29.7.0",
    "@testing-library/react": "13.4.0"
  }
}

Part 2: Now that your app is working fine, let's integrate it with Weam

Integration Overview

Once your AI Slides Generator is fully functional as a standalone application, you can integrate it into the Weam ecosystem. This integration involves:

  1. Authentication: Using Weam's session management
  2. Database: Migrating to Weam's MongoDB with proper naming conventions
  3. UI Theming: Matching Weam's design system
  4. Routing: Setting up base path configuration
  5. Deployment: Configuring NGINX and Docker

Weam Integration Steps

Step 1: Authentication Integration

Replace your standalone authentication with Weam's iron-session:

// lib/session.ts
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';

export interface SessionData {
  user?: {
    id: string;
    email: string;
    name: string;
  };
}

export const sessionOptions = {
  cookieName: 'weam_session',
  password: process.env.IRON_SESSION_PASSWORD!,
  cookieOptions: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'lax' as const,
    path: '/',
    domain: process.env.COOKIE_DOMAIN,
    maxAge: 60 * 60 * 24 * 7, // 7 days
  },
};

export async function getSession() {
  const cookieStore = await cookies();
  return getIronSession<SessionData>(cookieStore, sessionOptions);
}

Step 2: Database Migration

Update your database collections to follow Weam's naming convention:

// lib/database/mongodb.ts
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI!);
export const db = client.db('weam');

// Weam naming convention: solution_{solutionName}_{tableName}
export const presentations = db.collection('solution_slides_presentations');
export const exports = db.collection('solution_slides_exports');
export const templates = db.collection('solution_slides_templates');

Step 3: Base Path Configuration

Update your Next.js configuration for Weam routing:

// next.config.js
const nextConfig = {
  basePath: '/slides',
  assetPrefix: '/slides',
  async rewrites() {
    return [
      {
        source: '/slides/api/:path*',
        destination: '/api/:path*',
      },
    ];
  },
};

module.exports = nextConfig;

Step 4: Environment Variables

Update your environment configuration:

# .env.local
NEXT_PUBLIC_API_BASE_PATH=/slides
MONGODB_URI=mongodb://localhost:27017/weam
IRON_SESSION_PASSWORD=your-secure-session-password
COOKIE_DOMAIN=.weam.ai
OPENAI_API_KEY=your-openai-key

Step 5: Sidebar Integration

Add your solution to Weam's sidebar:

// Update src/seeders/superSolution.json
{
  "name": "AI Slides Generator",
  "path": "/slides",
  "icon": "PresentationChart",
  "description": "Create AI-powered presentations from text or prompts"
}

Step 6: NGINX Configuration

Add routing configuration to Weam's NGINX:

# Add to /etc/nginx/sites-available/weam
location /slides/ {
    proxy_pass http://localhost:3010/slides/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Step 7: Docker Configuration

Create Docker setup for deployment:

# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine AS production
WORKDIR /app

COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3010

CMD ["npm", "start"]
# docker-compose.yml
version: '3.8'

services:
  slides-generator:
    build: .
    ports:
      - "3010:3010"
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_BASE_PATH=/slides
      - MONGODB_URI=${MONGODB_URI}
      - IRON_SESSION_PASSWORD=${IRON_SESSION_PASSWORD}
      - COOKIE_DOMAIN=.weam.ai
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    restart: unless-stopped

Final Integration Checklist

✅ Standalone app fully functional
✅ Weam authentication integrated
✅ Database collections follow naming convention
✅ Base path configuration set
✅ Sidebar entry added
✅ NGINX routing configured
✅ Docker deployment ready
✅ Environment variables updated

Testing Integration

  1. Authentication Test: Verify users can access the app through Weam login
  2. Database Test: Confirm data is stored with proper user/company scoping
  3. Routing Test: Check that all routes work under /slides base path
  4. Export Test: Verify PDF/PPTX generation works in production
  5. UI Test: Ensure theming matches Weam's design system

Your AI Slides Generator is now fully integrated into the Weam ecosystem while maintaining all its standalone functionality!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions