Repository: YOYZHANG/ai-ppt
Branch: master
Commit: 3443269f5776
Files: 58
Total size: 90.6 KB
Directory structure:
gitextract_bom1etdj/
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── app/
│ ├── api/
│ │ ├── chat/
│ │ │ └── route.ts
│ │ ├── convertd/
│ │ │ └── route.ts
│ │ └── limit/
│ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── provider.tsx
├── components/
│ ├── artifact-view.tsx
│ ├── auth-dialog.tsx
│ ├── auth-form.tsx
│ ├── chat.tsx
│ ├── code-view.tsx
│ ├── navbar.tsx
│ ├── price-dialog.tsx
│ ├── price.tsx
│ ├── share-dialog.tsx
│ ├── share.tsx
│ ├── side-view.tsx
│ ├── ui/
│ │ ├── alert.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ └── toaster.tsx
│ ├── user.tsx
│ └── welcome.tsx
├── components.json
├── debug/
│ └── apitest.http
├── hooks/
│ └── use-toast.ts
├── lib/
│ ├── auth.ts
│ ├── messages.ts
│ ├── ratelimit.ts
│ ├── schema.ts
│ ├── supabase.ts
│ ├── template.ts
│ └── utils.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── sandbox-templates/
│ ├── e2b.Dockerfile
│ └── e2b.toml
├── tailwind.config.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": ["next/core-web-vitals"]
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
public/presentations
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Xiaoqian Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Welcome to RevealJS AI 👋
[中文说明](/README_CN.md)
## ✨ Demo
[/public/demo.mp4](https://github.com/user-attachments/assets/d5a4b37a-553b-41b4-ba33-ad457d118311)
Try it Online ⚡️: [Revealjs AI](https://ppt.revealjs.online)
## 🚀 Getting Started
### install
```sh
pnpm install
```
### set environmental values
set .env.local under root dir with values list below
```sh
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
KV_REST_API_URL=
KV_REST_API_TOKEN=
GEMINI_API_KEY=
```
### usege
```sh
pnpm run dev
```
## 💻 TechStack
- [Nextjs](https://nextjs.org/docs) - Full Stack Development
- [Tailwindcss](https://tailwindcss.com/) - CSS Engine
- [Supabase](https://supabase.com/) - User OAuth
- [Stripe](https://stripe.com/docs/development) - Payment
## 💗 Credit
- [Gemini API](https://gemini.google.com/app) - AI Powered
- [ai-artifacts](https://github.com/e2b-dev/ai-artifacts) - Reference
## 👤 Author
**YOYZHANG**
- Twitter: [@alexu19049062](https://twitter.com/alexuzhang19049062)
- Github: [@YOYZHANG](https://github.com/YOYZHANG)
- Wechat: whdxzxq
## 🤝 Contributing
Contributions, issues and feature requests are welcome. 😄
Feel free to check [issues page](https://github.com/YOYZHANG/ai-ppt/issues) if you want to contribute.
## 📝 License
MIT License © 2024 YOYZHANG
## Others
Please ⭐️ this repository if this project helped you!
Your appreciation is my greatest strength in updating content!
---
================================================
FILE: README_CN.md
================================================
欢迎来到 RevealJS AI 👋
## ✨ 示例
[/public/demo.mp4](https://github.com/user-attachments/assets/d5a4b37a-553b-41b4-ba33-ad457d118311)
在线地址: ⚡️ [Revealjs AI](https://ppt.revealjs.online)
## 🚀 快速开始
### 安装依赖
```sh
pnpm install
```
### 设置环境变量
set .env.local under root dir with values list below
```sh
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
KV_REST_API_URL=
KV_REST_API_TOKEN=
GEMINI_API_KEY=
```
### 本地开发
```sh
pnpm run dev
```
## 💻 技术栈
- [Nextjs](https://nextjs.org/docs) - Full Stack Development
- [Tailwindcss](https://tailwindcss.com/) - CSS Engine
- [Supabase](https://supabase.com/) - User OAuth
- [Stripe](https://stripe.com/docs/development) - Payment
## 💗 感谢以下项目
- [ai-artifacts](https://github.com/e2b-dev/ai-artifacts) - Reference
- [Gemini API](https://gemini.google.com/app) - AI Powered
## 👤作者
如果有任何疑问或技术上的交流,可以在 Twitter 或微信上联系我。
**YOYZHANG**
- twitter: [@alexu19049062](https://twitter.com/alexuzhang19049062)
- 微信: whdxzxq
## 🤝 贡献
欢迎贡献 [issues](https://github.com/YOYZHANG/ai-ppt/issues).
如果这个项目对你有帮助,欢迎 ⭐️ 或 Fork.
## 📝 License
MIT License © 2024 YOYZHANG
## 👀 其他
你的赞赏是我更新内容最大的功力:
---
================================================
FILE: app/api/chat/route.ts
================================================
import {
streamObject,
LanguageModel,
CoreMessage,
} from 'ai'
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import {htmlTemplate} from '@/lib/template'
import ratelimit from '@/lib/ratelimit'
import { artifactSchema } from '@/lib/schema'
export type LLMModel = {
id: string
name: string
provider: string
providerId: string
}
export type LLMModelConfig = {
model?: string
apiKey?: string
baseURL?: string
temperature?: number
topP?: number
topK?: number
frequencyPenalty?: number
presencePenalty?: number
maxTokens?: number
}
interface Req {
messages: CoreMessage[],
userID: string,
}
export async function POST(req: Request) {
const { messages } = await req.json() as Req
const client = createGoogleGenerativeAI({ apiKey: process.env.GEMINI_API_KEY})('models/gemini-1.5-flash-latest')
const stream = await streamObject({
model: client as LanguageModel,
schema: artifactSchema,
system: `
Generate a visually appealing reveal.js presentation in HTML.
The presentation should include the following slides: appealing cover, bullet points with links, conclusion and end page.
more than 6 slides.
use the template: ${htmlTemplate}
`,
messages
})
return stream.toTextStreamResponse()
}
================================================
FILE: app/api/convertd/route.ts
================================================
import { supabase } from '@/lib/supabase';
async function uploadFileContent(fileContent: string, fileName: string) {
const { data, error } = await supabase.storage
.from('ppt')
.upload(`public/${fileName}`, fileContent, {
contentType: 'text/html',
})
if (error) {
console.error('covert file failed', error)
return null
}
const { publicUrl } = supabase.storage.from('ppt').getPublicUrl(data.path).data
return publicUrl
}
export async function POST(req: Request) {
const { artifact} = await req.json()
const url = await uploadFileContent(artifact.code, `ppt_${Date.now()}.html`)
if (!url) {
return new Response('upload ppt html failed.', {
status: 403
})
}
// Send file URL back to client
return new Response(JSON.stringify({
url
}))
}
================================================
FILE: app/api/limit/route.ts
================================================
import ratelimit from '@/lib/ratelimit'
export const maxDuration = 60
const rateLimitMaxRequests = 10
const ratelimitWindow = '1d'
export async function GET(req: Request) {
const limit = await ratelimit(req.headers.get('x-forwarded-for'), rateLimitMaxRequests, ratelimitWindow)
if (limit && !limit?.success) {
return new Response('You have reached your request limit for the day.', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.amount.toString(),
'X-RateLimit-Remaining': limit.remaining.toString(),
'X-RateLimit-Reset': limit.reset.toString()
}
})
}
return new Response(JSON.stringify(limit))
}
================================================
FILE: app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 240, 6%, 10%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240, 5%, 13%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 270, 2%, 19%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
================================================
FILE: app/layout.tsx
================================================
import type { Metadata } from "next";
import "./globals.css";
import { Inter } from 'next/font/google'
import { PostHogProvider } from './provider'
import { ToastContainer } from "react-toastify";
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: "AI RevealJS",
description: "Generate RevealJS PPT by AI",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
{children}
);
}
================================================
FILE: app/page.tsx
================================================
'use client'
import Chat from '@/components/chat'
import SideView from '@/components/side-view'
import NavBar from '@/components/navbar'
import { AuthViewType, useAuth } from '@/lib/auth'
import { useEffect, useState } from 'react'
import { useLocalStorage } from 'usehooks-ts'
import { ChatMessage, toAISDKMessages } from '@/lib/messages'
import { experimental_useObject as useObject } from 'ai/react'
import {ArtifactSchema, artifactSchema } from '@/lib/schema'
import { usePostHog } from 'posthog-js/react'
import { supabase } from '@/lib/supabase'
import { AuthDialog } from '@/components/auth-dialog'
import { PriceDialog } from '@/components/price-dialog'
import { toast } from 'react-toastify'
export default function Home() {
const posthog = usePostHog()
const [currentTab, setCurrentTab] = useState<'code' | 'artifact'>('code')
const [isPreviewLoading, setIsPreviewLoading] = useState(false)
const [artifact, setArtifact] = useState | undefined>()
const [authView, setAuthView] = useState('sign_in')
const [isAuthDialogOpen, setAuthDialog] = useState(false)
const [isPriceDialogOpen, setPriceDialogOpen] = useState(false)
const { session, apiKey } = useAuth(setAuthDialog, setAuthView)
const { object, submit, isLoading, stop } = useObject({
api: '/api/chat',
schema: artifactSchema,
onFinish: async ({ object: artifact, error }) => {
if (error) {
return
}
setCurrentTab('artifact')
setIsPreviewLoading(false)
}
})
useEffect(() => {
if (object) {
setArtifact(object as ArtifactSchema)
const lastAssistantMessage = messages.findLast(message => message.role === 'assistant')
if (lastAssistantMessage) {
lastAssistantMessage.content = [{ type: 'text', text: object.commentary || '' }, { type: 'code', text: object.code || '' }]
lastAssistantMessage.meta = {
title: object.title,
description: object.description
}
}
}
}, [object])
const logout = () => {
supabase.auth.signOut()
}
const checkLimit = async (): Promise => {
const res = await fetch('/api/limit', {
method: 'GET',
});
if (res.status === 429) {
toast.error('You have reached your request limit for the day')
return true
}
return false
}
const [chatInput, setChatInput] = useLocalStorage('chat', '')
const handleSaveInputChange = (e: React.ChangeEvent) => {
setChatInput(e.target.value)
}
const [messages, setMessages] = useState([])
const addMessage = (message: ChatMessage) => {
setMessages(previousMessages => [...previousMessages, message])
return [...messages, message]
}
const handleSubmitAuth = async (e?: React.FormEvent) => {
e?.preventDefault()
if (!session) {
return setAuthDialog(true)
}
if (isLoading) {
stop()
}
const limited = await checkLimit()
if (limited) {
return
}
const content: ChatMessage['content'] = [{ type: 'text', text: chatInput }]
submit({
userID: session?.user?.id,
messages: toAISDKMessages(addMessage({role: 'user', content})),
})
addMessage({
role: 'assistant',
content: [{ type: 'text', text: 'Generating RevealJS ppt...' }],
})
setChatInput('')
setCurrentTab('code')
setIsPreviewLoading(true)
posthog.capture('chat_submit')
}
return (
{<>
>
}
setAuthDialog(true)}
signOut={logout}
showPrice={() => setPriceDialogOpen(true)}
/>
)
}
================================================
FILE: app/provider.tsx
================================================
'use client'
import posthog from 'posthog-js'
import { PostHogProvider as PostHogProviderJS } from 'posthog-js/react'
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? '', {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
session_recording: {
recordCrossOriginIframes: true,
}
})
export function PostHogProvider({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
================================================
FILE: components/artifact-view.tsx
================================================
'use client'
import { useEffect, useState } from "react"
interface ArtifactViewProps {
result: string
}
export function ArtifactView({
result,
}: ArtifactViewProps) {
const [iframeKey, setIframeKey] = useState(0);
useEffect(() => {
setIframeKey(prevKey => prevKey + 1);
}, [result]);
if (!result) return null
const encodedHTML = encodeURIComponent(result);
const dataURI = `data:text/html;charset=utf-8,${encodedHTML}`;
return (
)
}
================================================
FILE: components/auth-dialog.tsx
================================================
import {
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog"
import AuthForm from "./auth-form"
import { SupabaseClient } from "@supabase/supabase-js"
import { AuthViewType } from "@/lib/auth"
export function AuthDialog({ open, setOpen, supabase, view }: { open: boolean, setOpen: (open: boolean) => void, supabase: SupabaseClient, view: AuthViewType }) {
return (
)
}
================================================
FILE: components/auth-form.tsx
================================================
import { AuthViewType } from '@/lib/auth'
import { Auth } from '@supabase/auth-ui-react'
import {
ThemeSupa
} from '@supabase/auth-ui-shared'
import { SupabaseClient } from '@supabase/supabase-js'
function AuthForm({ supabase, view = 'sign_in' }: { supabase: SupabaseClient, view: AuthViewType }) {
return (
)
}
export default AuthForm
================================================
FILE: components/chat.tsx
================================================
import { ChangeEvent, FormEvent, useEffect } from 'react'
import { ArrowUp, Square, Sparkles,Terminal, BadgeDollarSign} from 'lucide-react'
import { ChatMessage } from '@/lib/messages'
import { Button } from '@/components/ui/button'
import Welcome from './welcome'
import { Input } from './ui/input'
interface ChatProps {
isLoading: boolean,
handleSubmit: (e?: FormEvent) => void,
setChatInput: (input: string) => void
input: string
handleInputChange: (e: ChangeEvent) => void,
messages: ChatMessage[]
}
export default function Chat({
isLoading,
input,
setChatInput,
messages,
handleInputChange,
handleSubmit,
}: ChatProps) {
useEffect(() => {
const chatContainer = document.getElementById('chat-container')
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight
}
}, [JSON.stringify(messages)])
return (
{!!messages.length && (
<>
{messages.map((message: ChatMessage, index: number) => (
{message.content.map((content, id) => {
if (content.type === 'text') {
return
{content.text}
}
if (content.type === 'image') {
return

}
})}
{message.meta &&
{message.meta.title}
{message.meta.description}
}
))}
>
)}
{!messages.length &&
}
)
}
================================================
FILE: components/code-view.tsx
================================================
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
export function CodeView({ content }: { content: string }) {
return (
{content}
);
}
================================================
FILE: components/navbar.tsx
================================================
import Link from 'next/link'
import Image from 'next/image'
import { Separator } from '@/components/ui/separator'
import { Button } from './ui/button'
import { Session } from '@supabase/supabase-js'
import { User } from '@/components/user'
import { BsGithub, BsTwitterX } from "react-icons/bs";
interface NavBarProps {
session: Session | null,
showLogin: () => void,
signOut: () => void,
showPrice: () => void
}
export default function NavBar({session, signOut, showLogin, showPrice}: NavBarProps) {
return (
)
}
================================================
FILE: components/price-dialog.tsx
================================================
import {
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog"
import Price from "./price"
export function PriceDialog({ open, setOpen }: { open: boolean, setOpen: (open: boolean) => void}) {
return (
)
}
================================================
FILE: components/price.tsx
================================================
import React, { useState } from 'react';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Check, X } from 'lucide-react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export interface Plan {
name: string
description: string
price: number
credits: number
features: {
name: string
included: boolean
}[]
}
const PricingPlans = () => {
const [isYearly, setIsYearly] = useState(false);
const [loading, setLoading] = useState(false);
const plans: Plan[] = [
{
name: "Free",
description: "No credit card needed",
price: 0,
credits: 20,
features: [
{ name: "20 credits per month", included: true },
{ name: "1 task waiting in queue", included: true },
{ name: "Limited queue priority", included: true },
{ name: "Assets are under CC BY 4.0 license", included: true },
],
},
{
name: "Pro",
description: "Best for individual creators",
price: 20,
credits: 100,
features: [
{ name: "1,000 credits per month", included: true },
{ name: "10 tasks waiting in queue", included: true },
{ name: "Standard queue priority", included: true },
{ name: "Assets are private & customer owned", included: true },
],
},
];
const handleSubscribe = async (plan: Plan) => {
try {
if (!process.env.STRIPE_PRIVATE_KEY) {
console.error('xxxxxx')
toast("👷 Feature Coming Soon....");
return
}
setLoading(true);
const response = await fetch("/api/stripe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(plan),
});
const { message, data } = await response.json();
if (!data) {
setLoading(false);
toast.error(message);
return;
}
} catch (e) {
setLoading(false);
toast.error("checkout failed");
}
};
return (
Upgrade Your Plan
Monthly
Yearly Save 20%
{plans.map((plan) => (
{plan.name}
{plan.description}
${isYearly ? plan.price * 12 * 0.8 : plan.price}/{isYearly ? 'year' : 'month'}
{plan.credits && ${(plan.price / plan.credits).toFixed(2)} / 100 credits
}
{plan.features.map((feature, index) => (
-
{feature.included ? (
) : (
)}
{feature.name}
))}
))}
);
};
export default PricingPlans;
================================================
FILE: components/share-dialog.tsx
================================================
import {
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog"
import { ShareLink } from "./share"
export function ShareDialog({ open, setOpen, url }: { open: boolean, setOpen: (open: boolean) => void, url?: string }) {
return (
)
}
================================================
FILE: components/share.tsx
================================================
import React from 'react';
import {
FacebookShareButton,
LinkedinShareButton,
RedditShareButton,
TelegramShareButton,
TwitterShareButton,
} from "react-share";
import {BsX, BsFacebook, BsReddit, BsLinkedin, BsTelegram} from 'react-icons/bs'
import { toast } from 'react-toastify';
import { Button } from './ui/button';
import { Input } from './ui/input';
const Title = `Just published a presentation generated by @alexu19049062 to the community.
#RevealJS #AI #createdwithai `
export function ShareLink({url}: {url?: string}) {
if (!url) {
return
}
function copy(url: string) {
navigator.clipboard.writeText(url)
.then(() => {
toast('Copied to clipboard')
})
.catch(err => {
toast.error('Failed to copy: ' + url)
})
}
return (
);
};
================================================
FILE: components/side-view.tsx
================================================
import { Dispatch, SetStateAction, useState } from 'react'
import { Download, LoaderCircle, Share2, Copy } from 'lucide-react'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs'
import { Button } from "@/components/ui/button"
import { CodeView } from './code-view'
import { ArtifactView } from './artifact-view'
import { ArtifactSchema } from '@/lib/schema'
import { toast } from 'react-toastify'
interface SideViewProps {
isLoading:boolean,
selectedTab: 'code' | 'artifact'
onSelectedTabChange: Dispatch>
artifact?: Partial
}
export default function SideView({
isLoading,
selectedTab,
onSelectedTabChange,
artifact
}: SideViewProps) {
const [isShareDialogOpen, setShareDialogOpen] = useState(false)
if (!artifact) {
return null
}
// function share() {
// setShareDialogOpen(true)
// }
function copy (content: string) {
navigator.clipboard.writeText(content)
.then(() => {
toast('Copied to clipboard')
})
.catch(err => {
toast.error('Failed to copy: ' + content)
})
}
function download (content: string) {
const blob = new Blob([content], { type: 'text/plain' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = "revealjs.html"
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
}
return (
{/*
*/}
onSelectedTabChange(value as 'code' | 'artifact')}
className="h-full max-h-full overflow-hidden flex flex-col items-start justify-start"
>
{isLoading && }
code
Preview
{
artifact && (
<>
{/* */}
>
)
}
{artifact && (
<>
{artifact.code &&
}
{artifact &&
}
>
)}
)
}
================================================
FILE: components/ui/alert.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes & VariantProps
>(({ className, variant, ...props }, ref) => (
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }
================================================
FILE: components/ui/avatar.tsx
================================================
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
================================================
FILE: components/ui/button.tsx
================================================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
================================================
FILE: components/ui/card.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
================================================
FILE: components/ui/dialog.tsx
================================================
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
================================================
FILE: components/ui/dropdown-menu.tsx
================================================
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
{children}
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, checked, ...props }, ref) => (
{children}
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
================================================
FILE: components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes {}
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
return (
)
}
)
Input.displayName = "Input"
export { Input }
================================================
FILE: components/ui/label.tsx
================================================
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
================================================
FILE: components/ui/select.tsx
================================================
"use client"
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
span]:line-clamp-1",
className
)}
{...props}
>
{children}
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
================================================
FILE: components/ui/separator.tsx
================================================
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
================================================
FILE: components/ui/skeleton.tsx
================================================
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes) {
return (
)
}
export { Skeleton }
================================================
FILE: components/ui/switch.tsx
================================================
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
================================================
FILE: components/ui/tabs.tsx
================================================
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
================================================
FILE: components/ui/textarea.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes {}
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
return (
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
================================================
FILE: components/ui/toast.tsx
================================================
"use client"
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, variant, ...props }, ref) => {
return (
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef
type ToastActionElement = React.ReactElement
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
================================================
FILE: components/ui/toaster.tsx
================================================
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
{title && {title}}
{description && (
{description}
)}
{action}
)
})}
)
}
================================================
FILE: components/user.tsx
================================================
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Session } from '@supabase/supabase-js'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "./ui/button";
import { LogOut } from "lucide-react";
interface Props {
session: Session | null,
signOut: () => void,
showLogin: () => void,
}
export function User({ session, signOut, showLogin }: Props) {
return (
<>
{session && (
{session.user.user_metadata.name}
{session.user.user_metadata.name}
)}
{!session && (
)}
>
);
}
================================================
FILE: components/welcome.tsx
================================================
'use client'
import React, { FormEvent, ChangeEvent, useEffect } from 'react';
import { Sparkles, Presentation } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from './ui/textarea';
const Lint = [
"Create a PPT on how ChatGPT works.",
"Generate a PPT explaining the API integration of ChatGPT in web applications, including key steps and practical examples.",
"Compares ChatGPT with other conversational AI models, such as Gemini, in terms of architecture and performance."
]
interface Props {
onSubmit: (e?: FormEvent) => void
onChange: (e: ChangeEvent) => void
setChatInput: (input: string) => void
value: string
}
export default function Welcome({onSubmit, onChange, value, setChatInput}: Props) {
const handleClick = (lint: string) => {
setChatInput(lint)
}
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onSubmit();
}
};
return (
Type the Topic for your Presentation,
It is Free!
Get started with an example:
{Lint.map((example, index) => (
))}
);
};
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
================================================
FILE: debug/apitest.http
================================================
@baseUri = http://127.0.0.1:3000/api
GET {{baseUri}}/limit
Content-Type: application/json
{}
POST {{baseUri}}/sandbox
Content-Type: application/json
{
"artifact": {
"code": "#title"
}
}
POST {{baseUri}}/chat
Content-Type: application/json
{
"messages": [{
"role": "user",
"content": [{ "type": "text", "text": "introduce slidev" }]
}]
}
POST {{baseUri}}/convertd
{
"artifact": {
"code": "#title"
}
}
================================================
FILE: hooks/use-toast.ts
================================================
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
================================================
FILE: lib/auth.ts
================================================
import { useState, useEffect } from 'react'
import { Session } from '@supabase/supabase-js'
import { supabase } from './supabase'
import { usePostHog } from 'posthog-js/react'
interface UserTeam {
id: string;
name: string;
is_default: boolean;
tier: string;
email: string;
team_api_keys: { api_key: string; }[];
}
export type AuthViewType = "sign_in" | "sign_up" | "magic_link" | "forgotten_password" | "update_password" | "verify_otp"
export async function getUserAPIKey (session: Session) {
const { data: userTeams } = await supabase
.from('users_teams')
.select('teams (id, name, is_default, tier, email, team_api_keys (api_key))')
.eq('user_id', session?.user.id)
const teams = userTeams?.map((userTeam: any) => userTeam.teams).map((team: UserTeam) => {
return {
...team,
apiKeys: team.team_api_keys.map(apiKey => apiKey.api_key)
}
})
const defaultTeam = teams?.find(team => team.is_default)
return defaultTeam?.apiKeys[0]
}
export function useAuth (setAuthDialog: (value: boolean) => void, setAuthView: (value: AuthViewType) => void) {
const [session, setSession] = useState(null)
const [apiKey, setApiKey] = useState(undefined)
const posthog = usePostHog()
let recovery = false
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
getUserAPIKey(session as Session).then(setApiKey)
setSession(session)
})
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
if (_event === 'PASSWORD_RECOVERY') {
recovery = true
setAuthView('update_password')
setAuthDialog(true)
}
if (_event === 'USER_UPDATED' && recovery) {
recovery = false
}
if (_event === 'SIGNED_IN' && !recovery) {
setAuthDialog(false)
getUserAPIKey(session as Session).then(setApiKey)
posthog.identify(session?.user.id, { email: session?.user.email })
posthog.capture('sign_in')
}
if (_event === 'SIGNED_OUT') {
setApiKey(undefined)
setAuthView('sign_in')
posthog.capture('sign_out')
posthog.reset()
}
})
return () => subscription.unsubscribe()
}, [])
return {
session,
apiKey
}
}
================================================
FILE: lib/messages.ts
================================================
export type MessageText = {
type: 'text'
text: string
}
export type MessageCode = {
type: 'code'
text: string
}
export type MessageImage = {
type: 'image'
image: string
}
export type ChatMessage = {
role: 'assistant' | 'user'
content: Array
meta?: {
title?: string
description?: string
}
}
export function toAISDKMessages(messages: ChatMessage[]) {
return messages.map(message => ({
role: message.role,
content: message.content.map(content => {
if (content.type === 'code') {
return {
type: 'text',
text: content.text
}
}
return content
})
}))
}
export async function toMessageImage(files: FileList | null) {
if (!files || files.length === 0) {
return []
}
return Promise.all(Array.from(files).map(async file => {
const base64 = Buffer.from(await file.arrayBuffer()).toString('base64')
return `data:${file.type};base64,${base64}`
}))
}
================================================
FILE: lib/ratelimit.ts
================================================
import { kv } from '@vercel/kv'
import { Ratelimit } from '@upstash/ratelimit'
export type Unit = "ms" | "s" | "m" | "h" | "d"
export type Duration = `${number} ${Unit}` | `${number}${Unit}`
export default async function ratelimit (key: string | null, maxRequests: number, window: Duration) {
if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) {
const ratelimit = new Ratelimit({
redis: kv,
limiter: Ratelimit.slidingWindow(maxRequests, window)
})
const { success, limit, reset, remaining } = await ratelimit.limit(
`ratelimit_${key}`
)
return {
amount: limit,
reset,
remaining,
success
}
}
}
================================================
FILE: lib/schema.ts
================================================
import { z } from 'zod'
export const artifactSchema = z.object({
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.`),
title: z.string().describe('Short title of the code. Max 3 words.'),
description: z.string().describe('Short description of the code. Max 1 sentence.'),
code: z.string().describe('code generated. Only runnable code is allowed.'),
})
export type ArtifactSchema = z.infer
================================================
FILE: lib/supabase.ts
================================================
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
================================================
FILE: lib/template.ts
================================================
export const htmlTemplate = `
`;
================================================
FILE: lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
================================================
FILE: next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
================================================
FILE: package.json
================================================
{
"name": "revealjs-ai",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.50",
"@ai-sdk/google": "^0.0.51",
"@e2b/code-interpreter": "0.0.9-beta.3",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@stripe/stripe-js": "^4.5.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.45.4",
"@upstash/ratelimit": "^2.0.3",
"@vercel/kv": "^2.0.0",
"ai": "^3.3.42",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.441.0",
"next": "14.2.12",
"posthog-js": "^1.161.6",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0",
"react-share": "^5.1.0",
"react-syntax-highlighter": "^15.5.0",
"react-toastify": "^10.0.5",
"stripe": "^16.12.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^8",
"eslint-config-next": "14.2.12",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
================================================
FILE: postcss.config.mjs
================================================
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
================================================
FILE: sandbox-templates/e2b.Dockerfile
================================================
# You can use most Debian-based base images
FROM node:21-slim
# Install dependencies and customize sandbox
WORKDIR /home/user/slidev
Run npm install -g npm
RUN npm install @slidev/cli @slidev/theme-default @slidev/theme-seriph
RUN touch slides.md
RUN apt-get update && apt-get install -y xdg-utils && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mv /home/user/slidev/* /home/user/ && rm -rf /home/user/slidev
EXPOSE 3030
# Move the Vue app to the home directory and remove the Vue directory
CMD ["npx", "slidev", "--remote"]
================================================
FILE: sandbox-templates/e2b.toml
================================================
# This is a config for E2B sandbox template.
# You can use 'template_id' (toyw6mhmdw42n4wyhdcb) or 'template_name (my-slidev-developer) from this config to spawn a sandbox:
# Python SDK
# from e2b import Sandbox
# sandbox = Sandbox(template='my-slidev-developer')
# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create({ template: 'my-slidev-developer' })
team_id = "df0c5b73-b47c-431e-a90c-9a32db676fb3"
start_cmd = "cd /home/user && npx slidev"
dockerfile = "e2b.Dockerfile"
template_name = "my-slidev-developer"
template_id = "toyw6mhmdw42n4wyhdcb"
================================================
FILE: tailwind.config.ts
================================================
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
};
export default config;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}