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
================================================
<h1 align="center">Welcome to RevealJS AI 👋</h1>
[中文说明](/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. 😄<br />
Feel free to check [issues page](https://github.com/YOYZHANG/ai-ppt/issues) if you want to contribute.<br />
## 📝 License
MIT License © 2024 YOYZHANG
## Others
Please ⭐️ this repository if this project helped you!
Your appreciation is my greatest strength in updating content!
<a href="https://www.buymeacoffee.com/zhangxiaoqian" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
---
================================================
FILE: README_CN.md
================================================
<h1 align="center">欢迎来到 RevealJS AI 👋</h1>
## ✨ 示例
[/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
## 👀 其他
你的赞赏是我更新内容最大的功力:
<a href="https://www.buymeacoffee.com/zhangxiaoqian" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
---
================================================
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 (
<html lang="en">
<PostHogProvider>
<body className={inter.className}>
<ToastContainer theme="dark"/>
{children}
</body>
</PostHogProvider>
</html>
);
}
================================================
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<Partial<ArtifactSchema> | undefined>()
const [authView, setAuthView] = useState<AuthViewType>('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<boolean> => {
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<HTMLTextAreaElement | HTMLInputElement>) => {
setChatInput(e.target.value)
}
const [messages, setMessages] = useState<ChatMessage[]>([])
const addMessage = (message: ChatMessage) => {
setMessages(previousMessages => [...previousMessages, message])
return [...messages, message]
}
const handleSubmitAuth = async (e?: React.FormEvent<HTMLFormElement>) => {
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 (
<main className="flex min-h-screen max-h-screen">
{<>
<AuthDialog open={isAuthDialogOpen} setOpen={setAuthDialog} view={authView} supabase={supabase} />
<PriceDialog open={isPriceDialogOpen} setOpen={setPriceDialogOpen}></PriceDialog>
</>
}
<NavBar
session={session}
showLogin={() => setAuthDialog(true)}
signOut={logout}
showPrice={() => setPriceDialogOpen(true)}
/>
<div className="flex-1 flex space-x-8 w-full pt-16 pb-8 px-4">
<Chat
isLoading={isLoading}
handleSubmit={handleSubmitAuth}
input={chatInput}
setChatInput={setChatInput}
handleInputChange={handleSaveInputChange}
messages={messages}
/>
<SideView
isLoading={isPreviewLoading}
selectedTab={currentTab}
onSelectedTabChange={setCurrentTab}
artifact={artifact}
/>
</div>
</main>
)
}
================================================
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 (
<PostHogProviderJS client={posthog}>
{children}
</PostHogProviderJS>
)
}
================================================
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 (
<div className="w-full h-full">
<iframe
key={iframeKey}
className="h-full w-full"
sandbox="allow-forms allow-scripts allow-same-origin"
loading="lazy"
src= {dataURI}
/>
</div>
)
}
================================================
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle></DialogTitle>
<AuthForm supabase={supabase} view={view} />
</DialogContent>
</Dialog>
)
}
================================================
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 (
<div className="mx-auto flex flex-1 w-full justify-center items-center flex-col">
<h1 className="text-4xl font-bold mt-8 mb-4">
Sign in
</h1>
<div className="md:w-[420px] w-[240px]">
<Auth
supabaseClient={supabase}
appearance={{
theme: ThemeSupa,
variables: {
default: {
colors: {
brand: 'rgb(255, 136, 0)',
brandAccent: 'rgb(255, 136, 0)',
inputText: '#FFF',
},
radii: {
borderRadiusButton: '20px',
inputBorderRadius: '12px'
}
},
},
}}
localization={{
variables: {
sign_in: {
email_label: 'Email',
password_label: 'Password',
},
},
}}
view={view}
theme='default'
showLinks={true}
providers={['google', 'github']}
providerScopes={{
github: 'email',
google: 'email'
}}
/>
</div>
</div>
)
}
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<HTMLFormElement>) => void,
setChatInput: (input: string) => void
input: string
handleInputChange: (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => 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 (
<div className="flex-1 flex flex-col gap-4 max-h-full max-w-[800px] mx-auto justify-between">
{!!messages.length && (
<>
<div id="chat-container" className="flex flex-col gap-2 overflow-y-auto max-h-full px-4 rounded-lg">
{messages.map((message: ChatMessage, index: number) => (
<div className={`py-2 px-4 shadow-sm whitespace-pre-wrap ${message.role !== 'user' ? 'bg-white/5 border text-muted-foreground' : 'bg-white/20'} rounded-lg font-serif`} key={index}>
{message.content.map((content, id) => {
if (content.type === 'text') {
return <p key={content.text} className="flex-1">{content.text}</p>
}
if (content.type === 'image') {
return <img key={id} src={content.image} alt="artifact" className="mr-2 inline-block w-[50px] h-[50px] object-contain border border-[#FFE7CC] rounded-lg bg-white mt-2" />
}
})}
{message.meta &&
<div className="mt-4 flex justify-start items-start border rounded-md">
<div className="p-2 self-stretch border-r w-14 flex items-center justify-center">
<Terminal strokeWidth={2} className="text-[#FF8800]"/>
</div>
<div className="p-2 flex flex-col space-y-1 justify-start items-start min-w-[100px]">
<span className="font-bold font-sans text-sm text-primary">{message.meta.title}</span>
<span className="font-sans text-sm">{message.meta.description}</span>
</div>
</div>
}
</div>
))}
</div>
<div className="flex flex-col gap-2">
<form onSubmit={handleSubmit} className="flex flex-row gap-2 items-center">
<Input
className="focus:outline-none resize-none"
required={true}
placeholder="Describe your ppt..."
value={input}
onChange={handleInputChange}
/>
{ !isLoading ? (
<Button variant="secondary" size="icon" className='rounded-full h-10 w-11'>
<Sparkles className="h-5 w-5 text-[#c5f955]" />
</Button>
) : (
<Button variant="secondary" size="icon" className='rounded-full h-10 w-11' onClick={(e) => { e.preventDefault(); stop() }}>
<Square className="h-5 w-5 text-[#c5f955]" />
</Button>
)
}
</form>
</div>
</>
)}
{!messages.length &&
<Welcome
onSubmit={handleSubmit}
onChange={handleInputChange}
setChatInput={setChatInput}
value={input}
/>}
</div>
)
}
================================================
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 (
<pre
className="p-4"
style={{
fontSize: 12,
backgroundColor: "transparent",
borderRadius: 0,
margin: 0,
}}
>
<SyntaxHighlighter
language="html"
style={tomorrow}
showLineNumbers={false}
wrapLines={true}
>
{content}
</SyntaxHighlighter>
</pre>
);
}
================================================
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 (
<nav className="fixed top-0 left-0 right-0 bg-background">
<div className="flex px-4 py-2">
<div className="flex flex-1 items-center">
<Link href="/" className="flex items-center gap-2" target="_blank">
<Image src="/logo.svg" alt="logo" width={24} height={24}/>
<h1 className="whitespace-pre ml-2 font-bold">RevealJS AI </h1>
</Link>
</div>
<div className="flex justify-end space-x-4">
<Button variant="secondary" className='text-[#c5f955]' onClick={showPrice}>
<span className="flex items-center h-5 w-5">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-5 w-5"><path fillRule="evenodd" clipRule="evenodd" d="M13.232 2.287A.75.75 0 0 1 13.75 3v6.25H19a.75.75 0 0 1 .607 1.191l-8 11a.75.75 0 0 1-1.357-.44v-6.25H5a.75.75 0 0 1-.607-1.192l8-11a.75.75 0 0 1 .839-.272Z" fill="currentColor"></path></svg>
</span>
<span>Subscribe to Pro</span>
</Button>
<Separator orientation="vertical" />
<Button variant="ghost" className='rounded-md px-0 mx-0 text-muted-foreground h-full'>
<a href="https://github.com/YOYZHANG/ai-ppt" target="_blank"><BsGithub className="text-sm w-5 h-5" /></a>
</Button>
<Button variant="ghost" className='rounded-md px-0 mx-0 text-muted-foreground h-full'>
<a href="https://x.com/alexu19049062" target="_blank"><BsTwitterX className="text-sm w-5 h-5" /></a>
</Button>
<Separator orientation="vertical" />
<User session={session} signOut={signOut} showLogin={showLogin}></User>
</div>
</div>
</nav>
)
}
================================================
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTitle></DialogTitle>
<DialogContent style={{'maxWidth': "800px"}}>
<Price/>
</DialogContent>
</Dialog>
)
}
================================================
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 (
<div className=" text-white p-8 mx-auto">
<h2 className="text-3xl font-bold mb-8 text-center">Upgrade Your Plan</h2>
<div className="flex justify-center items-center mb-8 space-x-4">
<span className={`${!isYearly ? 'text-green-400' : 'text-gray-400'}`}>Monthly</span>
<Switch checked={isYearly} onCheckedChange={setIsYearly} />
<span className={`${isYearly ? 'text-green-400' : 'text-gray-400'}`}>Yearly <span className="text-green-400 text-sm">Save 20%</span></span>
</div>
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-8">
{plans.map((plan) => (
<Card key={plan.name} className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className={`text-2xl font-bold ${plan.name === 'Free' ? 'text-green-400' : plan.name === 'Pro' ? 'text-blue-400' : plan.name === 'Max' ? 'text-purple-400' : 'text-yellow-400'}`}>
{plan.name}
</CardTitle>
<p className="text-sm text-gray-400">{plan.description}</p>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold mb-4">${isYearly ? plan.price * 12 * 0.8 : plan.price}<span className="text-sm font-normal text-gray-400">/{isYearly ? 'year' : 'month'}</span></p>
{plan.credits && <p className="text-sm text-gray-400 mb-4">${(plan.price / plan.credits).toFixed(2)} / 100 credits</p>}
<ul className="space-y-2">
{plan.features.map((feature, index) => (
<li key={index} className="flex items-start">
{feature.included ? (
<Check className="mr-2 h-5 w-5 text-green-400" />
) : (
<X className="mr-2 h-5 w-5 text-red-400" />
)}
<span className={feature.included ? 'text-gray-200' : 'text-gray-500'}>{feature.name}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button disabled={loading || plan.name === 'Free'} className="w-full bg-green-500 hover:bg-green-600 text-white" onClick={() => handleSubscribe(plan)}>
{plan.name === 'Free' ? 'Current plan' : 'Subscribe Now'}
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
};
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle></DialogTitle>
<ShareLink url={url}/>
</DialogContent>
</Dialog>
)
}
================================================
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 (
<div className=" text-white rounded-lg mx-auto">
<div className="flex mb-6">
<div className="flex flex-col gap-4 justify-start">
<div className='flex flex-row gap-2 items-center justify-start'>
<h3 className="text-sm font-semibold mr3 w-[100px]">By Link: </h3>
<Input
type="text"
value={url}
readOnly
className="flex-grow bg-gray-800 rounded-l-md px-3 py-2 text-sm"
/>
<Button variant="secondary" onClick={() => copy(url)}>
Copy Link
</Button>
</div>
<div className="flex flex-row gap-2 items-center justify-start">
<h3 className="text-sm font-semibold mr3">Share To:</h3>
<div className="flex space-x-4">
<TwitterShareButton url={url} title={Title}>
<BsX color="gray" size={24} />
</TwitterShareButton>
<FacebookShareButton url={url} title={Title}>
<BsFacebook color="gray" size={24}/>
</FacebookShareButton>
<LinkedinShareButton url={url} title={Title}>
<BsLinkedin color="gray" size={24} />
</LinkedinShareButton>
<RedditShareButton url={url} title={Title}>
<BsReddit size={24} color="gray"/>
</RedditShareButton>
<TelegramShareButton url={url} title={Title}>
<BsTelegram size={24} color="gray"/>
</TelegramShareButton>
</div>
</div>
</div>
</div>
</div>
);
};
================================================
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<SetStateAction<"code" | "artifact">>
artifact?: Partial<ArtifactSchema>
}
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 (
<div className="flex-1 flex flex-col shadow-2xl rounded-lg border max-w-[800px] bg-popover">
{/* <ShareDialog open={isShareDialogOpen} setOpen={setShareDialogOpen} url={result?.url}></ShareDialog> */}
<Tabs
value={selectedTab}
onValueChange={(value) => onSelectedTabChange(value as 'code' | 'artifact')}
className="h-full max-h-full overflow-hidden flex flex-col items-start justify-start"
>
<div className="w-full p-2 grid grid-cols-3 items-center justify-end rounded-t-lg border-b">
<div className='flex justify-start'>
{isLoading && <LoaderCircle className="h-4 w-4 text-[#a1a1aa] animate-spin" />}
</div>
<div className='flex justify-center'>
<TabsList className="px-1 py-0 border h-8">
<TabsTrigger className="font-normal text-xs py-1 px-2" value="code">code</TabsTrigger>
<TabsTrigger disabled={!artifact} className="font-normal text-xs py-1 px-2" value="artifact">Preview</TabsTrigger>
</TabsList>
</div>
<div className='flex items-center justify-end space-x-2'>
{
artifact && (
<>
<Button variant="ghost" className='h-8 rounded-md px-3 text-muted-foreground' title='Download Artifact' onClick={() => download(artifact.code || '')}>
<Download className="h-4 w-4" />
</Button>
<Button variant="ghost" className='h-8 rounded-md px-3 text-muted-foreground' title='Copy URL' onClick={() => copy(artifact.code || '')}>
<Copy className="h-4 w-4" />
</Button>
{/* <Button variant="ghost" className='h-8 rounded-md px-3 text-muted-foreground' title='Share' onClick={() => share()}>
<Share2 className="h-4 w-4" />
</Button> */}
</>
)
}
</div>
</div>
<div className="w-full flex-1 flex flex-col items-start justify-start overflow-y-auto">
{artifact && (
<>
<TabsContent value="code" className="flex-1 w-full">
{artifact.code &&
<CodeView content={artifact.code}/>
}
</TabsContent>
<TabsContent value="artifact" className="flex-1 w-full flex flex-col items-start justify-start">
{artifact &&
<ArtifactView
result={artifact?.code || ''}
/>
}
</TabsContent>
</>
)}
</div>
</Tabs>
</div>
)
}
================================================
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<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
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<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
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<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
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<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
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<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
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<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
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<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
================================================
FILE: components/ui/skeleton.tsx
================================================
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
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<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
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<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
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<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
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<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
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<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
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 (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
================================================
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 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="cursor-pointer">
<AvatarImage src={session.user?.user_metadata?.avatar_url}/>
<AvatarFallback>{session.user.user_metadata.name}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="mx-4">
<DropdownMenuLabel className="text-center truncate">
{session.user.user_metadata.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-center truncate">
<Button variant="ghost" size="icon" className="w-full h-full text-sm font-medium justify-start hover:text-gray-400" onClick={signOut}>
Sign Out
</Button>
</DropdownMenuLabel>
</DropdownMenuContent>
</DropdownMenu>
)}
{!session && (
<Button variant="secondary" size="icon" className="text-sm font-medium px-8 py-2" onClick={showLogin}>
Sign in
</Button>
)}
</>
);
}
================================================
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<HTMLFormElement>) => void
onChange: (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => 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<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
onSubmit();
}
};
return (
<div className="max-w-2xl mx-auto p-8 rounded-xl shadow-lg transition-all duration-300 hover:shadow-xl mt-[150px]">
<h1 className="text-2xl font-serif mb-8 flex items-center text-gray-100 animate-fade-in">
<Presentation className="w-10 h-10 text-[#c5f955] mr-3 animate-pulse" />
<div>
<p>
<span>
Type the Topic for your Presentation,
</span>
<span className='relative'>
It is Free!
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-blue-300/70"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"></path>
</svg>
</span>
</p>
</div>
</h1>
<div>
<form className="relative" onSubmit={onSubmit}>
<Textarea
className="w-full p-[20px] pr-[50px] text-white bg-black border rounded-lg resize-none focus-visible:outline-none"
rows={7}
value={value}
onKeyDown={handleKeyDown}
onChange={onChange}
placeholder="Describe your topic..."
/>
{value &&
<Button variant="secondary" size="icon" className='absolute right-3 top-3 text-white p-2 rounded-full'>
<Sparkles className="h-5 w-5 text-[#c5f955]" />
</Button>}
<div className="absolute left-3 bottom-3 right-3 flex justify-between items-center text-sm text-gray-600">
<span>Gemini Flash</span>
</div>
</form>
</div>
<div className="mt-6">
<p className="text-sm text-gray-400 mb-3 font-medium">Get started with an example:</p>
<div className="flex flex-wrap gap-2">
{Lint.map((example, index) => (
<button
key={index}
className="px-4 py-2 bg-black rounded-full text-sm text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-all duration-300 hover:shadow-md text-left"
onClick={() => handleClick(example)}
>
{example}
</button>
))}
</div>
</div>
</div>
);
};
================================================
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<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
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<ToasterToast, "id">
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<State>(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<Session | null>(null)
const [apiKey, setApiKey] = useState<string | undefined>(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<MessageText | MessageCode | MessageImage>
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<typeof artifactSchema>
================================================
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 = `
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/theme/black.min.css">
</head>
<body>
<div class="reveal">
<div class="slides">
// generate slides here
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.3.1/reveal.min.js"></script>
<script>
Reveal.initialize();
</script>
</body>
</html>
`;
================================================
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"]
}
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
SYMBOL INDEX (62 symbols across 31 files)
FILE: app/api/chat/route.ts
type LLMModel (line 12) | type LLMModel = {
type LLMModelConfig (line 19) | type LLMModelConfig = {
type Req (line 31) | interface Req {
function POST (line 36) | async function POST(req: Request) {
FILE: app/api/convertd/route.ts
function uploadFileContent (line 3) | async function uploadFileContent(fileContent: string, fileName: string) {
function POST (line 20) | async function POST(req: Request) {
FILE: app/api/limit/route.ts
function GET (line 9) | async function GET(req: Request) {
FILE: app/layout.tsx
function RootLayout (line 14) | function RootLayout({
FILE: app/page.tsx
function Home (line 19) | function Home() {
FILE: app/provider.tsx
function PostHogProvider (line 13) | function PostHogProvider({ children }: { children: React.ReactNode }) {
FILE: components/artifact-view.tsx
type ArtifactViewProps (line 5) | interface ArtifactViewProps {
function ArtifactView (line 10) | function ArtifactView({
FILE: components/auth-dialog.tsx
function AuthDialog (line 10) | function AuthDialog({ open, setOpen, supabase, view }: { open: boolean, ...
FILE: components/auth-form.tsx
function AuthForm (line 8) | function AuthForm({ supabase, view = 'sign_in' }: { supabase: SupabaseCl...
FILE: components/chat.tsx
type ChatProps (line 8) | interface ChatProps {
function Chat (line 17) | function Chat({
FILE: components/code-view.tsx
function CodeView (line 4) | function CodeView({ content }: { content: string }) {
FILE: components/navbar.tsx
type NavBarProps (line 9) | interface NavBarProps {
function NavBar (line 16) | function NavBar({session, signOut, showLogin, showPrice}: NavBarProps) {
FILE: components/price-dialog.tsx
function PriceDialog (line 8) | function PriceDialog({ open, setOpen }: { open: boolean, setOpen: (open:...
FILE: components/price.tsx
type Plan (line 9) | interface Plan {
FILE: components/share-dialog.tsx
function ShareDialog (line 8) | function ShareDialog({ open, setOpen, url }: { open: boolean, setOpen: (...
FILE: components/share.tsx
function ShareLink (line 17) | function ShareLink({url}: {url?: string}) {
FILE: components/side-view.tsx
type SideViewProps (line 16) | interface SideViewProps {
function SideView (line 23) | function SideView({
FILE: components/ui/button.tsx
type ButtonProps (line 37) | interface ButtonProps
FILE: components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({
FILE: components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: components/ui/toast.tsx
type ToastProps (line 115) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 117) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: components/user.tsx
type Props (line 14) | interface Props {
function User (line 20) | function User({ session, signOut, showLogin }: Props) {
FILE: components/welcome.tsx
type Props (line 14) | interface Props {
function Welcome (line 20) | function Welcome({onSubmit, onChange, value, setChatInput}: Props) {
FILE: hooks/use-toast.ts
constant TOAST_LIMIT (line 11) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 12) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 14) | type ToasterToast = ToastProps & {
function genId (line 30) | function genId() {
type ActionType (line 35) | type ActionType = typeof actionTypes
type Action (line 37) | type Action =
type State (line 55) | interface State {
function dispatch (line 136) | function dispatch(action: Action) {
type Toast (line 143) | type Toast = Omit<ToasterToast, "id">
function toast (line 145) | function toast({ ...props }: Toast) {
function useToast (line 174) | function useToast() {
FILE: lib/auth.ts
type UserTeam (line 6) | interface UserTeam {
type AuthViewType (line 15) | type AuthViewType = "sign_in" | "sign_up" | "magic_link" | "forgotten_pa...
function getUserAPIKey (line 17) | async function getUserAPIKey (session: Session) {
function useAuth (line 35) | function useAuth (setAuthDialog: (value: boolean) => void, setAuthView: ...
FILE: lib/messages.ts
type MessageText (line 1) | type MessageText = {
type MessageCode (line 6) | type MessageCode = {
type MessageImage (line 11) | type MessageImage = {
type ChatMessage (line 16) | type ChatMessage = {
function toAISDKMessages (line 25) | function toAISDKMessages(messages: ChatMessage[]) {
function toMessageImage (line 41) | async function toMessageImage(files: FileList | null) {
FILE: lib/ratelimit.ts
type Unit (line 4) | type Unit = "ms" | "s" | "m" | "h" | "d"
type Duration (line 5) | type Duration = `${number} ${Unit}` | `${number}${Unit}`
function ratelimit (line 7) | async function ratelimit (key: string | null, maxRequests: number, windo...
FILE: lib/schema.ts
type ArtifactSchema (line 10) | type ArtifactSchema = z.infer<typeof artifactSchema>
FILE: lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
{
"path": ".eslintrc.json",
"chars": 42,
"preview": "{\n \"extends\": [\"next/core-web-vitals\"]\n}\n"
},
{
"path": ".gitignore",
"chars": 413,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2024 Xiaoqian Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 1643,
"preview": "<h1 align=\"center\">Welcome to RevealJS AI 👋</h1>\n\n[中文说明](/README_CN.md)\n\n## ✨ Demo\n[/public/demo.mp4](https://github.com"
},
{
"path": "README_CN.md",
"chars": 1340,
"preview": "<h1 align=\"center\">欢迎来到 RevealJS AI 👋</h1>\n\n## ✨ 示例\n[/public/demo.mp4](https://github.com/user-attachments/assets/d5a4b3"
},
{
"path": "app/api/chat/route.ts",
"chars": 1290,
"preview": "import {\n streamObject,\n LanguageModel,\n CoreMessage,\n} from 'ai'\nimport { createGoogleGenerativeAI } from '@ai-sdk/g"
},
{
"path": "app/api/convertd/route.ts",
"chars": 817,
"preview": "import { supabase } from '@/lib/supabase';\n\nasync function uploadFileContent(fileContent: string, fileName: string) {\n "
},
{
"path": "app/api/limit/route.ts",
"chars": 667,
"preview": "import ratelimit from '@/lib/ratelimit'\n\nexport const maxDuration = 60\n\nconst rateLimitMaxRequests = 10\nconst ratelimitW"
},
{
"path": "app/globals.css",
"chars": 971,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 240, 6%, 10%;\n "
},
{
"path": "app/layout.tsx",
"chars": 672,
"preview": "import type { Metadata } from \"next\";\nimport \"./globals.css\";\nimport { Inter } from 'next/font/google'\nimport { PostHogP"
},
{
"path": "app/page.tsx",
"chars": 4501,
"preview": "'use client'\n\nimport Chat from '@/components/chat'\nimport SideView from '@/components/side-view'\nimport NavBar from '@/c"
},
{
"path": "app/provider.tsx",
"chars": 511,
"preview": "'use client'\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PostHogProviderJS } from 'posthog-js/react'\n\np"
},
{
"path": "components/artifact-view.tsx",
"chars": 702,
"preview": "'use client'\n\nimport { useEffect, useState } from \"react\"\n\ninterface ArtifactViewProps {\n result: string\n}\n\n\nexport fun"
},
{
"path": "components/auth-dialog.tsx",
"chars": 592,
"preview": "import {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"@/components/ui/dialog\"\nimport AuthForm from \"./auth-form\"\nim"
},
{
"path": "components/auth-form.tsx",
"chars": 1521,
"preview": "import { AuthViewType } from '@/lib/auth'\nimport { Auth } from '@supabase/auth-ui-react'\nimport {\n ThemeSupa\n} from '@s"
},
{
"path": "components/chat.tsx",
"chars": 3870,
"preview": "import { ChangeEvent, FormEvent, useEffect } from 'react'\nimport { ArrowUp, Square, Sparkles,Terminal, BadgeDollarSign} "
},
{
"path": "components/code-view.tsx",
"chars": 589,
"preview": "import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';\nimport { tomorrow } from 'react-syntax-highlighte"
},
{
"path": "components/navbar.tsx",
"chars": 2264,
"preview": "import Link from 'next/link'\nimport Image from 'next/image'\nimport { Separator } from '@/components/ui/separator'\nimport"
},
{
"path": "components/price-dialog.tsx",
"chars": 418,
"preview": "import {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"@/components/ui/dialog\"\nimport Price from \"./price\"\n\nexport f"
},
{
"path": "components/price.tsx",
"chars": 4614,
"preview": "import React, { useState } from 'react';\nimport { Card, CardContent, CardFooter, CardHeader, CardTitle } from \"@/compone"
},
{
"path": "components/share-dialog.tsx",
"chars": 432,
"preview": "import {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { ShareLink } from \"./share\"\n\n"
},
{
"path": "components/share.tsx",
"chars": 2485,
"preview": "import React from 'react';\nimport {\n FacebookShareButton,\n LinkedinShareButton,\n RedditShareButton,\n TelegramShareBu"
},
{
"path": "components/side-view.tsx",
"chars": 4212,
"preview": "import { Dispatch, SetStateAction, useState } from 'react'\nimport { Download, LoaderCircle, Share2, Copy } from 'lucide-"
},
{
"path": "components/ui/alert.tsx",
"chars": 1598,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
},
{
"path": "components/ui/avatar.tsx",
"chars": 1419,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } fr"
},
{
"path": "components/ui/button.tsx",
"chars": 1836,
"preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
},
{
"path": "components/ui/card.tsx",
"chars": 1847,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n Rea"
},
{
"path": "components/ui/dialog.tsx",
"chars": 3876,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { Cross2Ic"
},
{
"path": "components/ui/dropdown-menu.tsx",
"chars": 7366,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
},
{
"path": "components/ui/input.tsx",
"chars": 822,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLA"
},
{
"path": "components/ui/label.tsx",
"chars": 724,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type "
},
{
"path": "components/ui/select.tsx",
"chars": 5651,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n CaretSortIcon,\n CheckIcon,\n ChevronDownIcon,\n ChevronUpIcon,\n"
},
{
"path": "components/ui/separator.tsx",
"chars": 770,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { c"
},
{
"path": "components/ui/skeleton.tsx",
"chars": 266,
"preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {"
},
{
"path": "components/ui/switch.tsx",
"chars": 1162,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
},
{
"path": "components/ui/tabs.tsx",
"chars": 1891,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
},
{
"path": "components/ui/textarea.tsx",
"chars": 732,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n extends React.Textare"
},
{
"path": "components/ui/toast.tsx",
"chars": 4859,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Cross2Icon } from \"@radix-ui/react-icons\"\nimport * as ToastPrimiti"
},
{
"path": "components/ui/toaster.tsx",
"chars": 786,
"preview": "\"use client\"\n\nimport { useToast } from \"@/hooks/use-toast\"\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastPr"
},
{
"path": "components/user.tsx",
"chars": 1665,
"preview": "import { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Session } from '@supabase/supabas"
},
{
"path": "components/welcome.tsx",
"chars": 4219,
"preview": "'use client'\n\nimport React, { FormEvent, ChangeEvent, useEffect } from 'react';\nimport { Sparkles, Presentation } from '"
},
{
"path": "components.json",
"chars": 417,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "debug/apitest.http",
"chars": 457,
"preview": "@baseUri = http://127.0.0.1:3000/api \n\nGET {{baseUri}}/limit\nContent-Type: application/json\n\n{}\n\nPOST {{baseUri}}/sandbo"
},
{
"path": "hooks/use-toast.ts",
"chars": 3948,
"preview": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n ToastActionElement,"
},
{
"path": "lib/auth.ts",
"chars": 2349,
"preview": "import { useState, useEffect } from 'react'\nimport { Session } from '@supabase/supabase-js'\nimport { supabase } from './"
},
{
"path": "lib/messages.ts",
"chars": 1003,
"preview": "export type MessageText = {\n type: 'text'\n text: string\n}\n\nexport type MessageCode = {\n type: 'code'\n text: string\n}"
},
{
"path": "lib/ratelimit.ts",
"chars": 682,
"preview": "import { kv } from '@vercel/kv'\nimport { Ratelimit } from '@upstash/ratelimit'\n\nexport type Unit = \"ms\" | \"s\" | \"m\" | \"h"
},
{
"path": "lib/schema.ts",
"chars": 509,
"preview": "import { z } from 'zod'\n\nexport const artifactSchema = z.object({\n commentary: z.string().describe(`Describe what you'r"
},
{
"path": "lib/supabase.ts",
"chars": 181,
"preview": "import { createClient } from '@supabase/supabase-js'\n\nexport const supabase = createClient(\n process.env.NEXT_PUBLIC_SU"
},
{
"path": "lib/template.ts",
"chars": 612,
"preview": "export const htmlTemplate = `\n <!doctype html>\n <html>\n <head>\n <link rel=\"stylesheet\" href=\"https://cdnjs.clo"
},
{
"path": "lib/utils.ts",
"chars": 166,
"preview": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: Cla"
},
{
"path": "next.config.mjs",
"chars": 92,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n"
},
{
"path": "package.json",
"chars": 1780,
"preview": "{\n \"name\": \"revealjs-ai\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \""
},
{
"path": "postcss.config.mjs",
"chars": 135,
"preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport d"
},
{
"path": "sandbox-templates/e2b.Dockerfile",
"chars": 532,
"preview": "# You can use most Debian-based base images\nFROM node:21-slim\n\n# Install dependencies and customize sandbox\nWORKDIR /hom"
},
{
"path": "sandbox-templates/e2b.toml",
"chars": 580,
"preview": "# This is a config for E2B sandbox template.\n# You can use 'template_id' (toyw6mhmdw42n4wyhdcb) or 'template_name (my-sl"
},
{
"path": "tailwind.config.ts",
"chars": 1656,
"preview": "import type { Config } from \"tailwindcss\";\n\nconst config: Config = {\n darkMode: [\"class\"],\n content: [\n \"./page"
},
{
"path": "tsconfig.json",
"chars": 574,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n "
}
]
About this extraction
This page contains the full source code of the YOYZHANG/ai-ppt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (90.6 KB), approximately 25.5k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.