├── .eslintrc.json ├── app ├── favicon.ico ├── provider.tsx ├── api │ ├── limit │ │ └── route.ts │ ├── convertd │ │ └── route.ts │ └── chat │ │ └── route.ts ├── layout.tsx ├── globals.css └── page.tsx ├── public ├── demo.mp4 └── logo.svg ├── next.config.mjs ├── postcss.config.mjs ├── lib ├── utils.ts ├── supabase.ts ├── schema.ts ├── template.ts ├── ratelimit.ts ├── messages.ts └── auth.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── switch.tsx │ ├── avatar.tsx │ ├── alert.tsx │ ├── button.tsx │ ├── tabs.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── toast.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── price-dialog.tsx ├── share-dialog.tsx ├── auth-dialog.tsx ├── code-view.tsx ├── artifact-view.tsx ├── auth-form.tsx ├── user.tsx ├── navbar.tsx ├── share.tsx ├── chat.tsx ├── welcome.tsx ├── side-view.tsx └── price.tsx ├── components.json ├── .gitignore ├── sandbox-templates ├── e2b.Dockerfile └── e2b.toml ├── debug └── apitest.http ├── tsconfig.json ├── LICENSE ├── README_CN.md ├── README.md ├── tailwind.config.ts ├── package.json └── hooks └── use-toast.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOYZHANG/ai-ppt/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YOYZHANG/ai-ppt/HEAD/public/demo.mp4 -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | export const supabase = createClient( 4 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 5 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 6 | ) 7 | 8 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/price-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogTitle, 5 | } from "@/components/ui/dialog" 6 | import Price from "./price" 7 | 8 | export function PriceDialog({ open, setOpen }: { open: boolean, setOpen: (open: boolean) => void}) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/share-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogTitle, 5 | } from "@/components/ui/dialog" 6 | import { ShareLink } from "./share" 7 | 8 | export function ShareDialog({ open, setOpen, url }: { open: boolean, setOpen: (open: boolean) => void, url?: string }) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /lib/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const artifactSchema = z.object({ 4 | commentary: z.string().describe(`Describe what you're about to do and the steps you want to take for generating the code in great detail.`), 5 | title: z.string().describe('Short title of the code. Max 3 words.'), 6 | description: z.string().describe('Short description of the code. Max 1 sentence.'), 7 | code: z.string().describe('code generated. Only runnable code is allowed.'), 8 | }) 9 | 10 | export type ArtifactSchema = z.infer 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | public/presentations 39 | -------------------------------------------------------------------------------- /app/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import posthog from 'posthog-js' 3 | import { PostHogProvider as PostHogProviderJS } from 'posthog-js/react' 4 | 5 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? '', { 6 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 7 | person_profiles: 'identified_only', 8 | session_recording: { 9 | recordCrossOriginIframes: true, 10 | } 11 | }) 12 | 13 | export function PostHogProvider({ children }: { children: React.ReactNode }) { 14 | return ( 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /sandbox-templates/e2b.Dockerfile: -------------------------------------------------------------------------------- 1 | # You can use most Debian-based base images 2 | FROM node:21-slim 3 | 4 | # Install dependencies and customize sandbox 5 | WORKDIR /home/user/slidev 6 | Run npm install -g npm 7 | RUN npm install @slidev/cli @slidev/theme-default @slidev/theme-seriph 8 | 9 | RUN touch slides.md 10 | RUN apt-get update && apt-get install -y xdg-utils && apt-get clean && rm -rf /var/lib/apt/lists/* 11 | RUN mv /home/user/slidev/* /home/user/ && rm -rf /home/user/slidev 12 | 13 | EXPOSE 3030 14 | # Move the Vue app to the home directory and remove the Vue directory 15 | CMD ["npx", "slidev", "--remote"] 16 | -------------------------------------------------------------------------------- /debug/apitest.http: -------------------------------------------------------------------------------- 1 | @baseUri = http://127.0.0.1:3000/api 2 | 3 | GET {{baseUri}}/limit 4 | Content-Type: application/json 5 | 6 | {} 7 | 8 | POST {{baseUri}}/sandbox 9 | Content-Type: application/json 10 | 11 | { 12 | "artifact": { 13 | "code": "#title" 14 | } 15 | } 16 | 17 | POST {{baseUri}}/chat 18 | Content-Type: application/json 19 | 20 | { 21 | "messages": [{ 22 | "role": "user", 23 | "content": [{ "type": "text", "text": "introduce slidev" }] 24 | }] 25 | } 26 | 27 | POST {{baseUri}}/convertd 28 | 29 | { 30 | "artifact": { 31 | "code": "#title" 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /sandbox-templates/e2b.toml: -------------------------------------------------------------------------------- 1 | # This is a config for E2B sandbox template. 2 | # You can use 'template_id' (toyw6mhmdw42n4wyhdcb) or 'template_name (my-slidev-developer) from this config to spawn a sandbox: 3 | 4 | # Python SDK 5 | # from e2b import Sandbox 6 | # sandbox = Sandbox(template='my-slidev-developer') 7 | 8 | # JS SDK 9 | # import { Sandbox } from 'e2b' 10 | # const sandbox = await Sandbox.create({ template: 'my-slidev-developer' }) 11 | 12 | team_id = "df0c5b73-b47c-431e-a90c-9a32db676fb3" 13 | start_cmd = "cd /home/user && npx slidev" 14 | dockerfile = "e2b.Dockerfile" 15 | template_name = "my-slidev-developer" 16 | template_id = "toyw6mhmdw42n4wyhdcb" 17 | -------------------------------------------------------------------------------- /components/auth-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogTitle, 5 | } from "@/components/ui/dialog" 6 | import AuthForm from "./auth-form" 7 | import { SupabaseClient } from "@supabase/supabase-js" 8 | import { AuthViewType } from "@/lib/auth" 9 | 10 | export function AuthDialog({ open, setOpen, supabase, view }: { open: boolean, setOpen: (open: boolean) => void, supabase: SupabaseClient, view: AuthViewType }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /lib/template.ts: -------------------------------------------------------------------------------- 1 | export const htmlTemplate = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | // generate slides here 12 |
13 |
14 | 15 | 18 | 19 | 20 | `; 21 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/code-view.tsx: -------------------------------------------------------------------------------- 1 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 2 | import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism'; 3 | 4 | export function CodeView({ content }: { content: string }) { 5 | return ( 6 |
15 |        
21 |         {content}
22 |       
23 |     
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/api/limit/route.ts: -------------------------------------------------------------------------------- 1 | import ratelimit from '@/lib/ratelimit' 2 | 3 | export const maxDuration = 60 4 | 5 | const rateLimitMaxRequests = 10 6 | const ratelimitWindow = '1d' 7 | 8 | 9 | export async function GET(req: Request) { 10 | const limit = await ratelimit(req.headers.get('x-forwarded-for'), rateLimitMaxRequests, ratelimitWindow) 11 | 12 | if (limit && !limit?.success) { 13 | return new Response('You have reached your request limit for the day.', { 14 | status: 429, 15 | headers: { 16 | 'X-RateLimit-Limit': limit.amount.toString(), 17 | 'X-RateLimit-Remaining': limit.remaining.toString(), 18 | 'X-RateLimit-Reset': limit.reset.toString() 19 | } 20 | }) 21 | } 22 | 23 | return new Response(JSON.stringify(limit)) 24 | } 25 | -------------------------------------------------------------------------------- /lib/ratelimit.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv' 2 | import { Ratelimit } from '@upstash/ratelimit' 3 | 4 | export type Unit = "ms" | "s" | "m" | "h" | "d" 5 | export type Duration = `${number} ${Unit}` | `${number}${Unit}` 6 | 7 | export default async function ratelimit (key: string | null, maxRequests: number, window: Duration) { 8 | if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { 9 | const ratelimit = new Ratelimit({ 10 | redis: kv, 11 | limiter: Ratelimit.slidingWindow(maxRequests, window) 12 | }) 13 | 14 | const { success, limit, reset, remaining } = await ratelimit.limit( 15 | `ratelimit_${key}` 16 | ) 17 | 18 | return { 19 | amount: limit, 20 | reset, 21 | remaining, 22 | success 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { Inter } from 'next/font/google' 4 | import { PostHogProvider } from './provider' 5 | import { ToastContainer } from "react-toastify"; 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: "AI RevealJS", 11 | description: "Generate RevealJS PPT by AI", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |