21 |
setIsActive(false)}
26 | />
27 |
34 |
40 |
41 | {isActive && }
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Header;
54 |
--------------------------------------------------------------------------------
/src/components/modals/reset-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import axios from 'axios';
4 | import { useRouter } from 'next/navigation';
5 | import { useState } from 'react';
6 | import { toast } from 'sonner';
7 |
8 | import { useConversation } from '@/hooks/useConversation';
9 | import { useResetFormModal } from '@/hooks/useResetFormModal';
10 |
11 | import { Button } from '@/components/ui/button';
12 | import { Modal } from '@/components/ui/modal';
13 |
14 | const ResetFormModal = () => {
15 | const router = useRouter();
16 | const [isLoading, setIsLoading] = useState(false);
17 |
18 | const { resetConversation } = useConversation();
19 | const { isOpen, onClose, title, api } = useResetFormModal();
20 |
21 | const fixedTitle = title?.split(' ')[0];
22 |
23 | const onReset = async () => {
24 | try {
25 | setIsLoading(true);
26 | if (api) {
27 | const response = await axios.delete(api);
28 |
29 | if (response.status === 200) {
30 | toast.success(`${title} Reset`, { position: 'top-right' });
31 | resetConversation();
32 | } else {
33 | toast.error('Something went wrong.', { position: 'top-right' });
34 | }
35 | } else {
36 | throw new Error('No API provided.');
37 | }
38 | } catch (err: any) {
39 | toast.error(err.message, { position: 'top-right' });
40 | console.log(err);
41 | } finally {
42 | setIsLoading(false);
43 | onClose();
44 | router.refresh();
45 | }
46 | };
47 |
48 | return (
49 |
55 |
56 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default ResetFormModal;
65 |
--------------------------------------------------------------------------------
/src/app/(landing)/components/example-prompts.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { AnimatePresence, Variants } from 'framer-motion';
4 | import { useEffect, useState } from 'react';
5 | import { TypeAnimation } from 'react-type-animation';
6 |
7 | import { MotionDiv } from '@/components/ui/motion-div';
8 |
9 | import { LandingPrompts } from '@/constants';
10 |
11 | const ExamplePrompts = () => {
12 | const [index, setIndex] = useState(0);
13 | const [currentPrompt, setCurrentPrompt] = useState(LandingPrompts[0]);
14 | const [isDone, setIsDone] = useState(false);
15 |
16 | useEffect(() => {
17 | if (isDone) {
18 | setCurrentPrompt(LandingPrompts[index + 1]);
19 | if (index === 3) {
20 | setIndex(0);
21 | } else {
22 | setIndex(index + 1);
23 | }
24 | setIsDone(false);
25 | }
26 | }, [isDone, index]);
27 |
28 | const variants: Variants = {
29 | initial: {
30 | opacity: 0,
31 | y: -20,
32 | },
33 | enter: {
34 | transition: { duration: 0.5 },
35 | opacity: 1,
36 | y: 0,
37 | },
38 | exit: {
39 | transition: { duration: 0.5 },
40 | opacity: 0,
41 | y: 20,
42 | },
43 | };
44 |
45 | const currentQuestions = (
46 | currentPrompt.questions.flatMap(question => [question, 3000]) as any[]
47 | ).concat([() => setIsDone(true)]);
48 |
49 | return (
50 |
51 |
58 |
59 | {currentPrompt.title}
60 |
61 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default ExamplePrompts;
72 |
--------------------------------------------------------------------------------
/src/components/services/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ArrowRight } from 'lucide-react';
4 | import { useEffect, useState } from 'react';
5 |
6 | import { serviceVariants } from '@/components/services/animations';
7 | import { ServicesLinks } from '@/constants';
8 |
9 | import { Card, CardTitle } from '@/components/ui/card';
10 | import { MotionDiv } from '@/components/ui/motion-div';
11 | import { cn } from '@/lib/utils';
12 | import Link from 'next/link';
13 |
14 | const Services = () => {
15 | const [isMounted, setIsMounted] = useState(false);
16 |
17 | useEffect(() => {
18 | setIsMounted(true);
19 | }, []);
20 |
21 | if (!isMounted) return null;
22 |
23 | return (
24 |
25 | {ServicesLinks.map(
26 | ({ title, href, icon: Icon, textColor, bgColor }, index) => (
27 |
34 |
35 |
39 |
45 |
46 |
47 |
48 | {title}
49 |
50 |
53 |
54 |
55 |
56 | ),
57 | )}
58 |
59 | );
60 | };
61 |
62 | export default Services;
63 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from '@radix-ui/react-slot';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 | import * as React from 'react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | 'default-no-hover': 'bg-primary text-primary-foreground',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | },
23 | size: {
24 | default: 'h-10 px-4 py-2',
25 | sm: 'h-9 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-8 w-8 md:h-10 md:w-10',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | },
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes
,
39 | VariantProps {
40 | asChild?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button';
46 | return (
47 |
52 | );
53 | },
54 | );
55 | Button.displayName = 'Button';
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/src/app/(landing)/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { Button } from '@/components/ui/button';
4 |
5 | import ExamplePrompts from '@/app/(landing)/components/example-prompts';
6 | import Heading from '@/app/(landing)/components/heading';
7 | import LandingFooter from '@/app/(landing)/components/landing-footer';
8 |
9 | const LandingPage = () => {
10 | return (
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
Get Started
24 |
25 |
26 |
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default LandingPage;
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = 'CardTitle';
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = 'CardDescription';
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = 'CardContent';
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = 'CardFooter';
78 |
79 | export {
80 | Card,
81 | CardContent,
82 | CardDescription,
83 | CardFooter,
84 | CardHeader,
85 | CardTitle,
86 | };
87 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer utilities {
12 | @layer components {
13 | /* Hide scrollbar for Chrome, Safari and Opera */
14 | .no-scrollbar::-webkit-scrollbar {
15 | display: none;
16 | }
17 |
18 | /* Hide scrollbar for IE, Edge and Firefox */
19 | .no-scrollbar {
20 | -ms-overflow-style: none; /* Internet Explorer 10+ */
21 | scrollbar-width: none; /* Firefox */
22 | }
23 | }
24 | }
25 |
26 | @layer base {
27 | :root {
28 | --background: 0 0% 100%;
29 | --foreground: 224 71.4% 4.1%;
30 |
31 | --card: 0 0% 100%;
32 | --card-foreground: 224 71.4% 4.1%;
33 |
34 | --popover: 0 0% 100%;
35 | --popover-foreground: 224 71.4% 4.1%;
36 |
37 | --primary: 220.9 39.3% 11%;
38 | --primary-foreground: 210 20% 98%;
39 |
40 | --secondary: 220 14.3% 95.9%;
41 | --secondary-foreground: 220.9 39.3% 11%;
42 |
43 | --muted: 220 14.3% 95.9%;
44 | --muted-foreground: 220 8.9% 46.1%;
45 |
46 | --accent: 220 14.3% 95.9%;
47 | --accent-foreground: 220.9 39.3% 11%;
48 |
49 | --destructive: 0 84.2% 60.2%;
50 | --destructive-foreground: 210 20% 98%;
51 |
52 | --border: 220 13% 91%;
53 | --input: 220 13% 91%;
54 | --ring: 224 71.4% 4.1%;
55 |
56 | --radius: 0.5rem;
57 | }
58 |
59 | .dark {
60 | --background: 224 71.4% 4.1%;
61 | --foreground: 210 20% 98%;
62 |
63 | --card: 224 71.4% 4.1%;
64 | --card-foreground: 210 20% 98%;
65 |
66 | --popover: 224 71.4% 4.1%;
67 | --popover-foreground: 210 20% 98%;
68 |
69 | --primary: 210 20% 98%;
70 | --primary-foreground: 220.9 39.3% 11%;
71 |
72 | --secondary: 215 27.9% 16.9%;
73 | --secondary-foreground: 210 20% 98%;
74 |
75 | --muted: 215 27.9% 16.9%;
76 | --muted-foreground: 217.9 10.6% 64.9%;
77 |
78 | --accent: 215 27.9% 16.9%;
79 | --accent-foreground: 210 20% 98%;
80 |
81 | --destructive: 0 62.8% 30.6%;
82 | --destructive-foreground: 210 20% 98%;
83 |
84 | --border: 215 27.9% 16.9%;
85 | --input: 215 27.9% 16.9%;
86 | --ring: 216 12.2% 83.9%;
87 | }
88 | }
89 |
90 | @layer base {
91 | * {
92 | @apply border-border;
93 | }
94 | body {
95 | @apply bg-background text-foreground;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-saas",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"",
11 | "migration:generate": "npx drizzle-kit generate",
12 | "migration:push": "node -r esbuild-register ./src/db/migration.ts",
13 | "migrate": "npm run migration:generate && npm run migration:push"
14 | },
15 | "dependencies": {
16 | "@azure/storage-blob": "^12.23.0",
17 | "@clerk/nextjs": "^5.2.0",
18 | "@hookform/resolvers": "^3.7.0",
19 | "@radix-ui/react-avatar": "^1.1.0",
20 | "@radix-ui/react-dialog": "^1.1.1",
21 | "@radix-ui/react-label": "^2.1.0",
22 | "@radix-ui/react-select": "^2.1.1",
23 | "@radix-ui/react-separator": "^1.1.0",
24 | "@radix-ui/react-slot": "^1.1.0",
25 | "@radix-ui/react-tooltip": "^1.1.2",
26 | "axios": "^1.7.2",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.1.1",
29 | "dotenv": "^16.4.5",
30 | "drizzle-orm": "^0.31.2",
31 | "framer-motion": "^10.18.0",
32 | "lodash": "^4.17.21",
33 | "lucide-react": "^0.400.0",
34 | "next": "14.2.4",
35 | "next-themes": "^0.3.0",
36 | "openai": "^4.52.3",
37 | "pg": "^8.12.0",
38 | "react": "^18.3.1",
39 | "react-dom": "^18.3.1",
40 | "react-hook-form": "^7.52.1",
41 | "react-markdown": "^9.0.1",
42 | "react-type-animation": "^3.2.0",
43 | "replicate": "^0.30.2",
44 | "shadcn-ui": "^0.2.3",
45 | "sonner": "^1.5.0",
46 | "svix": "^1.24.0",
47 | "tailwind-merge": "^2.3.0",
48 | "tailwindcss-animate": "^1.0.7",
49 | "zod": "^3.23.8",
50 | "zustand": "^4.5.4"
51 | },
52 | "devDependencies": {
53 | "@kamona/tailwindcss-perspective": "^0.1.1",
54 | "@types/lodash": "^4.17.6",
55 | "@types/node": "^20.14.9",
56 | "@types/pg": "^8.11.6",
57 | "@types/react": "^18.3.3",
58 | "@types/react-dom": "^18.3.0",
59 | "autoprefixer": "^10.4.19",
60 | "drizzle-kit": "^0.22.8",
61 | "eslint": "^8.56.0",
62 | "eslint-config-next": "14.0.4",
63 | "postcss": "^8.4.39",
64 | "prettier": "^3.3.2",
65 | "prettier-plugin-organize-imports": "^4.0.0",
66 | "prettier-plugin-tailwindcss": "^0.6.5",
67 | "tailwindcss": "^3.4.4",
68 | "typescript": "^5.5.3"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/app/(routes)/image/components/image-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useRef, useState } from 'react';
4 |
5 | import { useImage } from '@/hooks/useImage';
6 | import useScroll from '@/hooks/useScroll';
7 | import { cn } from '@/lib/utils';
8 |
9 | import Empty from '@/components/empty';
10 | import ScrollToBottomArrow from '@/components/scroll-to-bottom-arrow';
11 | import ImagePromptSection from './image-prompt-section';
12 |
13 | const ImageContent = () => {
14 | const { imagePrompts } = useImage();
15 | const [isShowArrow, setIsShowArrow] = useState(false);
16 | const [isMounted, setIsMounted] = useState(false);
17 | const divRef = useRef(null!);
18 |
19 | const scrollToBottom = () => {
20 | if (divRef.current) {
21 | divRef.current.scrollTo({
22 | top: divRef.current.scrollHeight,
23 | behavior: 'smooth',
24 | });
25 | }
26 | };
27 |
28 | const handleScroll = () => {
29 | setIsShowArrow(
30 | divRef.current.scrollTop + divRef.current.clientHeight <
31 | divRef.current.scrollHeight - 200,
32 | );
33 | };
34 |
35 | useScroll(divRef, handleScroll);
36 |
37 | useEffect(() => {
38 | setIsMounted(true);
39 | }, []);
40 |
41 | useEffect(() => {
42 | if (isMounted) {
43 | if (divRef.current) {
44 | divRef.current.scrollTop = divRef.current.scrollHeight;
45 | }
46 | }
47 | }, [isMounted]);
48 |
49 | useEffect(() => {
50 | setIsShowArrow(false);
51 | scrollToBottom();
52 | }, [imagePrompts]);
53 |
54 | if (!isMounted) {
55 | return null;
56 | }
57 |
58 | return (
59 |
65 | {imagePrompts.length === 0 ? (
66 |
67 | ) : (
68 |
72 | {imagePrompts.map((imagePrompt, index) => (
73 |
74 | ))}
75 |
76 | )}
77 |
78 |
79 | );
80 | };
81 |
82 | export default ImageContent;
83 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CodeIcon,
3 | ImageIcon,
4 | MessageSquareIcon,
5 | MusicIcon,
6 | SettingsIcon,
7 | VideoIcon,
8 | } from 'lucide-react';
9 |
10 | const LandingPrompts = [
11 | {
12 | title: 'Write a thank-you note',
13 | questions: ['to my interviewer', 'to my babysitter'],
14 | },
15 | {
16 | title: 'Create a grocery list',
17 | questions: ['for a dinner party', 'for a week of meals'],
18 | },
19 | {
20 | title: 'Design a logo',
21 | questions: ['for a tech startup', 'for a fitness brand'],
22 | },
23 | {
24 | title: 'Plan a vacation',
25 | questions: ['to a tropical destination', 'to a historical city'],
26 | },
27 | {
28 | title: 'Bake a cake',
29 | questions: ['for a birthday celebration', 'for a special occasion'],
30 | },
31 | ];
32 |
33 | const ConversationGeneration = {
34 | title: 'Conversation',
35 | href: '/conversation',
36 | api: '/api/conversation',
37 | showReset: true,
38 | icon: MessageSquareIcon,
39 | bgColor: 'bg-orange-500/20',
40 | textColor: 'text-orange-500',
41 | };
42 |
43 | const ImageGeneration = {
44 | title: 'Image Generation',
45 | href: '/image',
46 | api: '/api/image',
47 | showReset: true,
48 | icon: ImageIcon,
49 | bgColor: 'bg-yellow-500/20',
50 | textColor: 'text-yellow-500',
51 | };
52 |
53 | const VideoGeneration = {
54 | title: 'Video Generation',
55 | href: '/video',
56 | api: '/api/video',
57 | showReset: false,
58 | icon: VideoIcon,
59 | bgColor: 'bg-green-500/20',
60 | textColor: 'text-green-500',
61 | };
62 |
63 | const MusicGeneration = {
64 | title: 'Music Generation',
65 | href: '/music',
66 | api: '/api/music',
67 | showReset: false,
68 | icon: MusicIcon,
69 | bgColor: 'bg-blue-500/20',
70 | textColor: 'text-blue-500',
71 | };
72 |
73 | const CodeGeneration = {
74 | title: 'Code Generation',
75 | href: '/code',
76 | api: '/api/code',
77 | showReset: true,
78 | icon: CodeIcon,
79 | bgColor: 'bg-indigo-500/20',
80 | textColor: 'text-indigo-500',
81 | };
82 |
83 | const Settings = {
84 | title: 'Settings',
85 | href: '/settings',
86 | showReset: false,
87 | icon: SettingsIcon,
88 | bgColor: 'bg-purple-500/20',
89 | textColor: 'text-purple-500',
90 | };
91 |
92 | const ServicesLinks = [
93 | ConversationGeneration,
94 | ImageGeneration,
95 | VideoGeneration,
96 | MusicGeneration,
97 | CodeGeneration,
98 | ];
99 |
100 | export {
101 | CodeGeneration,
102 | ConversationGeneration,
103 | ImageGeneration,
104 | LandingPrompts,
105 | MusicGeneration,
106 | ServicesLinks,
107 | Settings,
108 | VideoGeneration,
109 | };
110 |
--------------------------------------------------------------------------------
/src/app/api/webhooks/route.ts:
--------------------------------------------------------------------------------
1 | import { WebhookEvent } from '@clerk/nextjs/server';
2 | import { eq } from 'drizzle-orm';
3 | import { headers } from 'next/headers';
4 | import { NextResponse } from 'next/server';
5 | import { Webhook } from 'svix';
6 |
7 | import { db } from '@/db';
8 | import { users } from '@/db/schema';
9 |
10 | export const POST = async (req: Request) => {
11 | const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
12 |
13 | if (!WEBHOOK_SECRET) {
14 | throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env');
15 | }
16 |
17 | // Get the headers
18 | const headerPayload = headers();
19 | const svix_id = headerPayload.get('svix-id');
20 | const svix_timestamp = headerPayload.get('svix-timestamp');
21 | const svix_signature = headerPayload.get('svix-signature');
22 |
23 | if (!svix_id || !svix_timestamp || !svix_signature) {
24 | return new Response('Error occured -- no svix headers', {
25 | status: 400,
26 | });
27 | }
28 |
29 | // Get the body
30 | const payload = await req.json();
31 | const body = JSON.stringify(payload);
32 |
33 | // Create a new Svix instance with your secret.
34 | const wh = new Webhook(WEBHOOK_SECRET);
35 | let event: WebhookEvent;
36 |
37 | // Verify the payload with the headers
38 | try {
39 | event = wh.verify(body, {
40 | 'svix-id': svix_id,
41 | 'svix-timestamp': svix_timestamp,
42 | 'svix-signature': svix_signature,
43 | }) as WebhookEvent;
44 | } catch (err) {
45 | console.error('Error verifying webhook:', err);
46 | return new Response('Error occured', {
47 | status: 400,
48 | });
49 | }
50 |
51 | // Event Type
52 | const eventType = event.type;
53 |
54 | // Handle the event for type users
55 | switch (eventType) {
56 | case 'user.created': {
57 | const userId = event.data.id;
58 | const email = event.data.email_addresses[0].email_address;
59 | const name = `${event.data.first_name} ${event.data.last_name}`;
60 |
61 | await db
62 | .insert(users)
63 | .values({
64 | userId,
65 | email,
66 | name,
67 | })
68 | .execute();
69 |
70 | break;
71 | }
72 | case 'user.updated': {
73 | const userId = event.data.id;
74 | const email = event.data.email_addresses[0].email_address;
75 | const name = `${event.data.first_name} ${event.data.last_name}`;
76 |
77 | await db
78 | .update(users)
79 | .set({
80 | email,
81 | name,
82 | })
83 | .where(eq(users.userId, userId))
84 | .execute();
85 |
86 | break;
87 | }
88 | case 'user.deleted': {
89 | const userId = event.data.id!;
90 |
91 | await db.delete(users).where(eq(users.userId, userId)).execute();
92 |
93 | break;
94 | }
95 | }
96 |
97 | return NextResponse.json({ message: 'Webhook received' }, { status: 200 });
98 | };
99 |
--------------------------------------------------------------------------------
/src/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from 'drizzle-orm';
2 | import {
3 | integer,
4 | pgTable,
5 | serial,
6 | text,
7 | timestamp,
8 | varchar,
9 | } from 'drizzle-orm/pg-core';
10 |
11 | export const users = pgTable('users', {
12 | id: serial('id').primaryKey(),
13 | userId: text('user_id').notNull(),
14 | email: text('email'),
15 | name: text('name'),
16 | about: text('about').default('').notNull(),
17 | createdAt: timestamp('created_at').defaultNow().notNull(),
18 | });
19 |
20 | export const userRelations = relations(users, ({ many }) => ({
21 | conversations: many(conversation),
22 | code: many(code),
23 | imagePrompts: many(imagePrompt),
24 | }));
25 |
26 | export const conversation = pgTable('conversation', {
27 | id: serial('id').primaryKey(),
28 | authorId: integer('author_id')
29 | .notNull()
30 | .references(() => users.id, { onDelete: 'cascade' }),
31 | index: integer('index').notNull(),
32 | role: varchar('role', { length: 10 }).notNull(),
33 | content: text('content').notNull(),
34 | createdAt: timestamp('created_at').defaultNow().notNull(),
35 | });
36 |
37 | export const conversationRelations = relations(conversation, ({ one }) => ({
38 | author: one(users, {
39 | fields: [conversation.authorId],
40 | references: [users.id],
41 | }),
42 | }));
43 |
44 | export const code = pgTable('code', {
45 | id: serial('id').primaryKey(),
46 | authorId: integer('author_id')
47 | .notNull()
48 | .references(() => users.id, { onDelete: 'cascade' }),
49 | index: integer('index').notNull(),
50 | role: varchar('role', { length: 10 }).notNull(),
51 | content: text('content').notNull(),
52 | createdAt: timestamp('created_at').defaultNow().notNull(),
53 | });
54 |
55 | export const codeRelations = relations(code, ({ one }) => ({
56 | author: one(users, { fields: [code.authorId], references: [users.id] }),
57 | }));
58 |
59 | export const imagePrompt = pgTable('image_prompt', {
60 | id: serial('id').primaryKey(),
61 | authorId: integer('author_id')
62 | .notNull()
63 | .references(() => users.id, { onDelete: 'cascade' }),
64 | prompt: text('prompt').notNull(),
65 | amount: integer('amount').notNull(),
66 | resolution: varchar('resolution', { length: 10 }).notNull(),
67 | model: varchar('model', { length: 10 }).notNull(),
68 | createdAt: timestamp('created_at').defaultNow().notNull(),
69 | });
70 |
71 | export const imagePromptRelations = relations(imagePrompt, ({ one, many }) => ({
72 | author: one(users, {
73 | fields: [imagePrompt.authorId],
74 | references: [users.id],
75 | }),
76 | images: many(image),
77 | }));
78 |
79 | export const image = pgTable('image', {
80 | id: serial('id').primaryKey(),
81 | promptId: integer('prompt_id')
82 | .notNull()
83 | .references(() => imagePrompt.id, { onDelete: 'cascade' }),
84 | key: text('key').notNull(),
85 | url: text('url').notNull(),
86 | createdAt: timestamp('created_at').defaultNow().notNull(),
87 | });
88 |
89 | export const imageRelations = relations(image, ({ one }) => ({
90 | prompt: one(imagePrompt, {
91 | fields: [image.promptId],
92 | references: [imagePrompt.id],
93 | }),
94 | }));
95 |
--------------------------------------------------------------------------------
/src/components/navbar/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { BotIcon } from 'lucide-react';
4 | import { Lato } from 'next/font/google';
5 | import Link from 'next/link';
6 | import { usePathname, useRouter } from 'next/navigation';
7 | import { Dispatch, MouseEvent, SetStateAction } from 'react';
8 |
9 | import { cn } from '@/lib/utils';
10 |
11 | import {
12 | navBarEntryFooterVariants,
13 | navBarEntryVariants,
14 | } from '@/components/navbar/animations';
15 | import { links } from '@/components/navbar/data';
16 | import { MotionDiv } from '@/components/ui/motion-div';
17 |
18 | const lato = Lato({ style: 'normal', weight: '700', subsets: ['latin'] });
19 |
20 | interface NavbarProps {
21 | setIsActive: Dispatch>;
22 | }
23 |
24 | const Navbar = ({ setIsActive }: NavbarProps) => {
25 | const pathname = usePathname();
26 | const router = useRouter();
27 |
28 | const handleClick = (e: MouseEvent, href: string) => {
29 | e.preventDefault();
30 | router.prefetch(href);
31 | setIsActive(false);
32 | setTimeout(() => router.push(href), 800);
33 | };
34 |
35 | return (
36 |
86 | );
87 | };
88 |
89 | export default Navbar;
90 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | mode: 'jit',
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: '2rem',
15 | screens: {
16 | '2xl': '1400px',
17 | },
18 | },
19 | extend: {
20 | rotate3d: (theme: any) => ({
21 | // default values
22 | // https://tailwindcss.com/docs/rotate
23 | ...theme('rotate'),
24 | // new values
25 | ...{
26 | '-60': '-60deg',
27 | '-50': '-50deg',
28 | '-40': '-40deg',
29 | '-35': '-35deg',
30 | '-30': '-30deg',
31 | '-25': '-25deg',
32 | '-20': '-20deg',
33 | '-15': '-15deg',
34 | '-10': '-10deg',
35 | 10: '10deg',
36 | 15: '15deg',
37 | 20: '20deg',
38 | 25: '25deg',
39 | 30: '30deg',
40 | 35: '35deg',
41 | 40: '40deg',
42 | 50: '50deg',
43 | 60: '60deg',
44 | },
45 | }),
46 | screens: {
47 | 'sm-height': { raw: '(min-height: 640px)' },
48 | 'md-height': { raw: '(min-height: 800px)' },
49 | },
50 | height: {
51 | dynamic: '100dvh',
52 | },
53 | colors: {
54 | border: 'hsl(var(--border))',
55 | input: 'hsl(var(--input))',
56 | ring: 'hsl(var(--ring))',
57 | background: 'hsl(var(--background))',
58 | foreground: 'hsl(var(--foreground))',
59 | primary: {
60 | DEFAULT: 'hsl(var(--primary))',
61 | foreground: 'hsl(var(--primary-foreground))',
62 | },
63 | secondary: {
64 | DEFAULT: 'hsl(var(--secondary))',
65 | foreground: 'hsl(var(--secondary-foreground))',
66 | },
67 | destructive: {
68 | DEFAULT: 'hsl(var(--destructive))',
69 | foreground: 'hsl(var(--destructive-foreground))',
70 | },
71 | muted: {
72 | DEFAULT: 'hsl(var(--muted))',
73 | foreground: 'hsl(var(--muted-foreground))',
74 | },
75 | accent: {
76 | DEFAULT: 'hsl(var(--accent))',
77 | foreground: 'hsl(var(--accent-foreground))',
78 | },
79 | popover: {
80 | DEFAULT: 'hsl(var(--popover))',
81 | foreground: 'hsl(var(--popover-foreground))',
82 | },
83 | card: {
84 | DEFAULT: 'hsl(var(--card))',
85 | foreground: 'hsl(var(--card-foreground))',
86 | },
87 | },
88 | borderRadius: {
89 | lg: 'var(--radius)',
90 | md: 'calc(var(--radius) - 2px)',
91 | sm: 'calc(var(--radius) - 4px)',
92 | },
93 | keyframes: {
94 | 'accordion-down': {
95 | from: { height: 0 },
96 | to: { height: 'var(--radix-accordion-content-height)' },
97 | },
98 | 'accordion-up': {
99 | from: { height: 'var(--radix-accordion-content-height)' },
100 | to: { height: 0 },
101 | },
102 | },
103 | animation: {
104 | 'accordion-down': 'accordion-down 0.2s ease-out',
105 | 'accordion-up': 'accordion-up 0.2s ease-out',
106 | },
107 | },
108 | },
109 | plugins: [
110 | require('tailwindcss-animate'),
111 | require('@kamona/tailwindcss-perspective'),
112 | ],
113 | };
114 |
--------------------------------------------------------------------------------
/src/app/(routes)/conversation/components/conversation-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
4 |
5 | import { useConversation } from '@/hooks/useConversation';
6 | import useScroll from '@/hooks/useScroll';
7 | import { cn } from '@/lib/utils';
8 |
9 | import Avatar from '@/components/custom-avatar';
10 | import Empty from '@/components/empty';
11 | import ScrollToBottomArrow from '@/components/scroll-to-bottom-arrow';
12 |
13 | const ConversationContent = () => {
14 | const { conversation } = useConversation();
15 | const [isShowArrow, setIsShowArrow] = useState(false);
16 | const [isMounted, setIsMounted] = useState(false);
17 | const divRef = useRef(null!);
18 |
19 | const scrollToBottom = () => {
20 | if (divRef.current) {
21 | divRef.current.scrollTo({
22 | top: divRef.current.scrollHeight,
23 | behavior: 'smooth',
24 | });
25 | }
26 | };
27 |
28 | const handleScroll = () => {
29 | setIsShowArrow(
30 | divRef.current.scrollTop + divRef.current.clientHeight <
31 | divRef.current.scrollHeight - 200,
32 | );
33 | };
34 |
35 | useScroll(divRef, handleScroll);
36 |
37 | useEffect(() => {
38 | setIsMounted(true);
39 | }, []);
40 |
41 | useEffect(() => {
42 | if (isMounted) {
43 | if (divRef.current) {
44 | divRef.current.scrollTop = divRef.current.scrollHeight;
45 | }
46 | }
47 | }, [isMounted]);
48 |
49 | useEffect(() => {
50 | scrollToBottom();
51 | }, [conversation]);
52 |
53 | const filteredMessages = useMemo(
54 | () => conversation.filter(message => message.role !== 'system'),
55 | [conversation],
56 | );
57 |
58 | if (!isMounted) {
59 | return null;
60 | }
61 |
62 | return (
63 |
69 | {filteredMessages.length === 0 ? (
70 |
71 | ) : (
72 |
76 | {filteredMessages.map(({ content, role }, index) => {
77 | const message = (content as string)
78 | .split('\n')
79 | .map((line, index) => (
80 |
81 | {line}
82 |
83 |
84 | ));
85 |
86 | return (
87 |
96 | {role === 'user' ? (
97 |
98 | ) : (
99 |
100 | )}
101 |
{message}
102 |
103 | );
104 | })}
105 |
106 | )}
107 |
108 |
109 | );
110 | };
111 |
112 | export default ConversationContent;
113 |
--------------------------------------------------------------------------------
/src/app/(routes)/settings/components/settings-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { useRouter } from 'next/navigation';
6 | import { useForm } from 'react-hook-form';
7 | import { toast } from 'sonner';
8 |
9 | import { User } from '@/db/types';
10 | import { SettingsFormSchema, SettingsFormValues } from '../data';
11 |
12 | import { Button } from '@/components/ui/button';
13 | import {
14 | Form,
15 | FormControl,
16 | FormDescription,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from '@/components/ui/form';
22 | import { Input } from '@/components/ui/input';
23 | import { Textarea } from '@/components/ui/textarea';
24 |
25 | interface SettingsFormProps {
26 | user: User;
27 | }
28 |
29 | const SettingsForm = ({ user }: SettingsFormProps) => {
30 | const router = useRouter();
31 | const settingsForm = useForm({
32 | resolver: zodResolver(SettingsFormSchema),
33 | defaultValues: {
34 | name: user.name ?? '',
35 | about: user.about ?? '',
36 | },
37 | });
38 |
39 | // Loading State
40 | const isLoading = settingsForm.formState.isSubmitting;
41 |
42 | const onSubmit = async (values: SettingsFormValues) => {
43 | try {
44 | const response = await axios.post('/api/settings', values);
45 |
46 | if (response.status === 200) {
47 | toast.success('Settings updated!');
48 | } else {
49 | toast.error('Something went wrong.');
50 | }
51 | } catch (err: any) {
52 | toast.error(err.message);
53 | console.log(err);
54 | } finally {
55 | router.refresh();
56 | }
57 | };
58 |
59 | return (
60 |
108 |
109 | );
110 | };
111 |
112 | export default SettingsForm;
113 |
--------------------------------------------------------------------------------
/src/app/api/code/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from '@clerk/nextjs/server';
2 | import { eq } from 'drizzle-orm';
3 | import { NextResponse } from 'next/server';
4 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
5 |
6 | import { db } from '@/db';
7 | import { code, users } from '@/db/schema';
8 | import { openai } from '@/lib/open-ai';
9 |
10 | interface ConversationRequest {
11 | messages: ChatCompletionMessageParam[];
12 | }
13 |
14 | export const POST = async (req: Request) => {
15 | try {
16 | const { userId } = auth();
17 |
18 | if (!userId) {
19 | return new NextResponse('Unauthorized', { status: 401 });
20 | }
21 |
22 | if (!openai.apiKey) {
23 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
24 | }
25 |
26 | const body = await req.json();
27 | const { messages }: ConversationRequest = body;
28 |
29 | if (!messages) {
30 | return new NextResponse('Messages are required', { status: 400 });
31 | }
32 |
33 | const response = await openai.chat.completions.create({
34 | model: 'gpt-3.5-turbo',
35 | messages,
36 | });
37 |
38 | if (!response.choices) {
39 | return new NextResponse('No response from OpenAI', { status: 500 });
40 | }
41 |
42 | messages.push(response.choices[0].message);
43 |
44 | const user = await db.query.users.findFirst({
45 | where: eq(users.userId, userId),
46 | });
47 |
48 | if (!user) {
49 | return new NextResponse('Unauthorized', { status: 401 });
50 | }
51 |
52 | const existingCodeLength = await db.query.code
53 | .findMany({
54 | where: eq(code.authorId, user.id),
55 | })
56 | .then(codes => codes.length)
57 | .catch(() => -1);
58 |
59 | if (existingCodeLength === -1) {
60 | messages.forEach(async ({ role, content }, index) => {
61 | await db
62 | .insert(code)
63 | .values({
64 | authorId: user.id,
65 | index,
66 | role,
67 | content: content as string,
68 | })
69 | .execute();
70 | });
71 |
72 | return NextResponse.json(response.choices[0].message, { status: 200 });
73 | }
74 |
75 | messages.forEach(async ({ role, content }, index) => {
76 | index >= existingCodeLength &&
77 | (await db
78 | .insert(code)
79 | .values({
80 | authorId: user.id,
81 | index,
82 | role,
83 | content: content as string,
84 | })
85 | .execute());
86 | });
87 |
88 | return NextResponse.json(response.choices[0].message, { status: 200 });
89 | } catch (err) {
90 | console.log('CODE_POST_ERROR:', err);
91 | return new NextResponse('Internal Error', { status: 500 });
92 | }
93 | };
94 |
95 | export const DELETE = async (req: Request) => {
96 | try {
97 | const { userId } = auth();
98 |
99 | if (!userId) {
100 | return new NextResponse('Unauthorized', { status: 401 });
101 | }
102 |
103 | if (!openai.apiKey) {
104 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
105 | }
106 |
107 | const user = await db.query.users.findFirst({
108 | where: eq(users.userId, userId),
109 | });
110 |
111 | if (!user) {
112 | return new NextResponse('Unauthorized', { status: 401 });
113 | }
114 |
115 | await db.delete(code).where(eq(code.authorId, user.id)).execute();
116 |
117 | return new NextResponse('Deleted Code', { status: 200 });
118 | } catch (err: any) {
119 | console.log('CODE_DELETE_ERROR:', err);
120 | return new NextResponse('Internal Error', { status: 500 });
121 | }
122 | };
123 |
--------------------------------------------------------------------------------
/src/app/api/conversation/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from '@clerk/nextjs/server';
2 | import { eq } from 'drizzle-orm';
3 | import { NextResponse } from 'next/server';
4 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
5 |
6 | import { db } from '@/db';
7 | import { conversation, users } from '@/db/schema';
8 | import { openai } from '@/lib/open-ai';
9 |
10 | interface ConversationRequest {
11 | messages: ChatCompletionMessageParam[];
12 | }
13 |
14 | export const POST = async (req: Request) => {
15 | try {
16 | const { userId } = auth();
17 |
18 | if (!userId) {
19 | return new NextResponse('Unauthorized', { status: 401 });
20 | }
21 |
22 | if (!openai.apiKey) {
23 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
24 | }
25 |
26 | const body = await req.json();
27 | const { messages }: ConversationRequest = body;
28 |
29 | if (!messages) {
30 | return new NextResponse('Messages are required', { status: 400 });
31 | }
32 |
33 | const response = await openai.chat.completions.create({
34 | model: 'gpt-4',
35 | messages,
36 | });
37 |
38 | if (!response.choices) {
39 | return new NextResponse('No response from OpenAI', { status: 500 });
40 | }
41 |
42 | messages.push(response.choices[0].message);
43 |
44 | const user = await db.query.users.findFirst({
45 | where: eq(users.userId, userId),
46 | });
47 |
48 | if (!user) {
49 | return new NextResponse('Unauthorized', { status: 401 });
50 | }
51 |
52 | const existingConversationsLength = await db.query.conversation
53 | .findMany({
54 | where: eq(conversation.authorId, user.id),
55 | })
56 | .then(conversations => conversations.length)
57 | .catch(() => -1);
58 |
59 | if (existingConversationsLength === -1) {
60 | messages.forEach(async ({ role, content }, index) => {
61 | await db
62 | .insert(conversation)
63 | .values({
64 | authorId: user.id,
65 | index,
66 | role,
67 | content: content as string,
68 | })
69 | .execute();
70 | });
71 |
72 | return NextResponse.json(response.choices[0].message, { status: 200 });
73 | }
74 |
75 | messages.forEach(async ({ role, content }, index) => {
76 | index >= existingConversationsLength &&
77 | (await db
78 | .insert(conversation)
79 | .values({
80 | authorId: user.id,
81 | index,
82 | role,
83 | content: content as string,
84 | })
85 | .execute());
86 | });
87 |
88 | return NextResponse.json(response.choices[0].message, { status: 200 });
89 | } catch (err) {
90 | console.log('CONVERSATION_POST_ERROR:', err);
91 | return new NextResponse('Internal Error', { status: 500 });
92 | }
93 | };
94 |
95 | export const DELETE = async (req: Request) => {
96 | try {
97 | const { userId } = auth();
98 |
99 | if (!userId) {
100 | return new NextResponse('Unauthorized', { status: 401 });
101 | }
102 |
103 | if (!openai.apiKey) {
104 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
105 | }
106 |
107 | const user = await db.query.users.findFirst({
108 | where: eq(users.userId, userId),
109 | });
110 |
111 | if (!user) {
112 | return new NextResponse('Unauthorized', { status: 401 });
113 | }
114 |
115 | await db
116 | .delete(conversation)
117 | .where(eq(conversation.authorId, user.id))
118 | .execute();
119 |
120 | return new NextResponse('Deleted Conversation', { status: 200 });
121 | } catch (err: any) {
122 | console.log('CONVERSATION_DELETE_ERROR:', err);
123 | return new NextResponse('Internal Error', { status: 500 });
124 | }
125 | };
126 |
--------------------------------------------------------------------------------
/src/app/(routes)/code/components/code-content.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useMemo, useRef, useState } from 'react';
4 | import Markdown from 'react-markdown';
5 |
6 | import { useCode } from '@/hooks/useCode';
7 | import useScroll from '@/hooks/useScroll';
8 | import { cn } from '@/lib/utils';
9 |
10 | import CustomAvatar from '@/components/custom-avatar';
11 | import Empty from '@/components/empty';
12 | import ScrollToBottomArrow from '@/components/scroll-to-bottom-arrow';
13 |
14 | const CodeGenerationContent = () => {
15 | const { code } = useCode();
16 | const [isShowArrow, setIsShowArrow] = useState(false);
17 | const [isMounted, setIsMounted] = useState(false);
18 | const divRef = useRef(null!);
19 |
20 | const scrollToBottom = () => {
21 | if (divRef.current) {
22 | divRef.current.scrollTo({
23 | top: divRef.current.scrollHeight,
24 | behavior: 'smooth',
25 | });
26 | }
27 | };
28 |
29 | const handleScroll = () => {
30 | setIsShowArrow(
31 | divRef.current.scrollTop + divRef.current.clientHeight <
32 | divRef.current.scrollHeight - 200,
33 | );
34 | };
35 |
36 | useScroll(divRef, handleScroll);
37 |
38 | useEffect(() => {
39 | setIsMounted(true);
40 | }, []);
41 |
42 | useEffect(() => {
43 | if (isMounted) {
44 | if (divRef.current) {
45 | divRef.current.scrollTop = divRef.current.scrollHeight;
46 | }
47 | }
48 | }, [isMounted]);
49 |
50 | useEffect(() => {
51 | scrollToBottom();
52 | }, [code]);
53 |
54 | const filteredMessages = useMemo(
55 | () => code.filter(message => message.role !== 'system'),
56 | [code],
57 | );
58 |
59 | if (!isMounted) {
60 | return null;
61 | }
62 |
63 | return (
64 |
70 | {filteredMessages.length === 0 ? (
71 |
72 | ) : (
73 |
77 | {filteredMessages.map((message, index) => (
78 |
83 | {message.role === 'user' ? (
84 |
85 | ) : (
86 |
87 | )}
88 |
(
91 |
92 | ),
93 | li: ({ node, ...props }) => (
94 |
95 | ),
96 | pre: ({ node, ...props }) => (
97 |
103 | ),
104 | code: ({ node, ...props }) => (
105 |
106 | ),
107 | }}
108 | className={'overflow-auto text-sm leading-7'}
109 | >
110 | {message.content as string}
111 |
112 |
113 | ))}
114 |
115 | )}
116 |
117 |
118 | );
119 | };
120 |
121 | export default CodeGenerationContent;
122 |
--------------------------------------------------------------------------------
/src/app/(routes)/video/components/video-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { ArrowUpIcon } from 'lucide-react';
6 | import { useRouter } from 'next/navigation';
7 | import { Dispatch, SetStateAction, useEffect, useState } from 'react';
8 | import { useForm } from 'react-hook-form';
9 | import { toast } from 'sonner';
10 |
11 | import { VideoFormSchema, VideoFormValues } from '../data';
12 |
13 | import TooltipWrapper from '@/components/tooltip-wrapper';
14 | import { Button } from '@/components/ui/button';
15 | import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
16 | import { Input } from '@/components/ui/input';
17 |
18 | interface VideoFormProps {
19 | setVideo: Dispatch>;
20 | }
21 |
22 | const VideoForm = ({ setVideo }: VideoFormProps) => {
23 | const router = useRouter();
24 | const [isMounted, setIsMounted] = useState(false);
25 | const videoForm = useForm({
26 | resolver: zodResolver(VideoFormSchema),
27 | defaultValues: {
28 | prompt: '',
29 | },
30 | });
31 |
32 | // Loading State
33 | const isLoading = videoForm.formState.isSubmitting;
34 |
35 | const onSubmit = async (values: VideoFormValues) => {
36 | try {
37 | setVideo(undefined);
38 |
39 | const responsePromise = axios.post('/api/video', values);
40 |
41 | toast.promise(responsePromise, {
42 | id: 'Video',
43 | position: 'top-right',
44 | loading: 'Directing a movie...',
45 | error: 'Something went wrong.',
46 | });
47 |
48 | const response = await responsePromise;
49 |
50 | if (response.status === 200) {
51 | setVideo(response.data[0]);
52 | videoForm.reset();
53 | }
54 | } catch (err: any) {
55 | toast.error(err.message);
56 | console.log(err);
57 | } finally {
58 | router.refresh();
59 | }
60 | };
61 |
62 | useEffect(() => {
63 | setIsMounted(true);
64 | }, []);
65 |
66 | if (!isMounted) {
67 | return null;
68 | }
69 |
70 | return (
71 |
118 |
119 | );
120 | };
121 |
122 | export default VideoForm;
123 |
--------------------------------------------------------------------------------
/src/app/(routes)/music/components/music-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { useRouter } from 'next/navigation';
6 | import { Dispatch, SetStateAction } from 'react';
7 | import { useForm } from 'react-hook-form';
8 | import { toast } from 'sonner';
9 |
10 | import { MusicFormSchema, MusicFormValues, audioLengthOptions } from '../data';
11 |
12 | import { Button } from '@/components/ui/button';
13 | import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
14 | import { Input } from '@/components/ui/input';
15 | import {
16 | Select,
17 | SelectContent,
18 | SelectItem,
19 | SelectTrigger,
20 | SelectValue,
21 | } from '@/components/ui/select';
22 |
23 | interface MusicFormProps {
24 | setMusic: Dispatch>;
25 | }
26 |
27 | const MusicForm = ({ setMusic }: MusicFormProps) => {
28 | const router = useRouter();
29 | const musicForm = useForm({
30 | resolver: zodResolver(MusicFormSchema),
31 | defaultValues: {
32 | prompt: '',
33 | length: '8',
34 | },
35 | });
36 |
37 | // Loading State
38 | const isLoading = musicForm.formState.isSubmitting;
39 |
40 | const onSubmit = async (values: MusicFormValues) => {
41 | try {
42 | setMusic(undefined);
43 |
44 | const responsePromise = axios.post('/api/music', values);
45 |
46 | toast.promise(responsePromise, {
47 | id: 'Music',
48 | position: 'top-right',
49 | loading: 'Composing music...',
50 | error: 'Something went wrong.',
51 | });
52 |
53 | const response = await responsePromise;
54 |
55 | if (response.status === 200) {
56 | setMusic(response.data);
57 | musicForm.reset();
58 | }
59 | } catch (err: any) {
60 | toast.error(err.message);
61 | console.log(err);
62 | } finally {
63 | router.refresh();
64 | }
65 | };
66 |
67 | return (
68 |
125 |
126 | );
127 | };
128 |
129 | export default MusicForm;
130 |
--------------------------------------------------------------------------------
/src/app/(routes)/code/components/code-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { ArrowUpIcon } from 'lucide-react';
6 | import { useRouter } from 'next/navigation';
7 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
8 | import { useEffect, useState } from 'react';
9 | import { useForm } from 'react-hook-form';
10 | import { toast } from 'sonner';
11 |
12 | import { useCode } from '@/hooks/useCode';
13 | import { CodeFormSchema, CodeFormValues } from '../data';
14 |
15 | import TooltipWrapper from '@/components/tooltip-wrapper';
16 | import { Button } from '@/components/ui/button';
17 | import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
18 | import { Input } from '@/components/ui/input';
19 |
20 | const CodeForm = () => {
21 | const router = useRouter();
22 | const [isMounted, setIsMounted] = useState(false);
23 | const { code, setCode } = useCode();
24 | const codeForm = useForm({
25 | resolver: zodResolver(CodeFormSchema),
26 | defaultValues: {
27 | prompt: '',
28 | },
29 | });
30 |
31 | // Loading State
32 | const isLoading = codeForm.formState.isSubmitting;
33 |
34 | const onSubmit = async (values: CodeFormValues) => {
35 | try {
36 | const userMessage: ChatCompletionMessageParam = {
37 | role: 'user',
38 | content: values.prompt,
39 | };
40 |
41 | const newMessages = [...code, userMessage];
42 |
43 | const responsePromise = axios.post('/api/code', {
44 | messages: newMessages,
45 | });
46 |
47 | toast.promise(responsePromise, {
48 | id: 'Code',
49 | position: 'top-right',
50 | loading: 'Copying from Stack Overflow...',
51 | error: 'Something went wrong.',
52 | });
53 |
54 | const response = await responsePromise;
55 |
56 | if (response.status === 200) {
57 | setCode([...newMessages, response.data]);
58 | codeForm.reset();
59 | }
60 | } catch (err: any) {
61 | toast.error(err.message);
62 | console.log(err);
63 | } finally {
64 | router.refresh();
65 | }
66 | };
67 |
68 | useEffect(() => {
69 | setIsMounted(true);
70 | }, []);
71 |
72 | if (!isMounted) {
73 | return null;
74 | }
75 |
76 | return (
77 |
124 |
125 | );
126 | };
127 |
128 | export default CodeForm;
129 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as DialogPrimitive from '@radix-ui/react-dialog';
4 | import { X } from 'lucide-react';
5 | import * as React from 'react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = 'DialogHeader';
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = 'DialogFooter';
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogClose,
114 | DialogContent,
115 | DialogDescription,
116 | DialogFooter,
117 | DialogHeader,
118 | DialogOverlay,
119 | DialogPortal,
120 | DialogTitle,
121 | DialogTrigger,
122 | };
123 |
--------------------------------------------------------------------------------
/src/app/(routes)/conversation/components/conversation-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { ArrowUpIcon } from 'lucide-react';
6 | import { useRouter } from 'next/navigation';
7 | import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
8 | import { useEffect, useState } from 'react';
9 | import { useForm } from 'react-hook-form';
10 | import { toast } from 'sonner';
11 |
12 | import { useConversation } from '@/hooks/useConversation';
13 | import { ConversationFormSchema, ConversationFormValues } from '../data';
14 |
15 | import TooltipWrapper from '@/components/tooltip-wrapper';
16 | import { Button } from '@/components/ui/button';
17 | import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
18 | import { Input } from '@/components/ui/input';
19 |
20 | const ConversationForm = () => {
21 | const router = useRouter();
22 | const [isMounted, setIsMounted] = useState(false);
23 | const { conversation, setConversation } = useConversation();
24 | const conversationForm = useForm({
25 | resolver: zodResolver(ConversationFormSchema),
26 | defaultValues: {
27 | prompt: '',
28 | },
29 | });
30 |
31 | // Loading State
32 | const isLoading = conversationForm.formState.isSubmitting;
33 |
34 | const onSubmit = async (values: ConversationFormValues) => {
35 | try {
36 | const userMessage: ChatCompletionMessageParam = {
37 | role: 'user',
38 | content: values.prompt,
39 | };
40 |
41 | const newMessages = [...conversation, userMessage];
42 |
43 | const responsePromise = axios.post('/api/conversation', {
44 | messages: newMessages,
45 | });
46 |
47 | toast.promise(responsePromise, {
48 | id: 'Conversation',
49 | position: 'top-right',
50 | loading: 'ChatXYZ is thinking...',
51 | error: 'Something went wrong.',
52 | });
53 |
54 | const response = await responsePromise;
55 |
56 | if (response.status === 200) {
57 | setConversation([...newMessages, response.data]);
58 | conversationForm.reset();
59 | }
60 | } catch (err: any) {
61 | toast.error(err.message);
62 | console.log(err);
63 | } finally {
64 | router.refresh();
65 | }
66 | };
67 |
68 | useEffect(() => {
69 | setIsMounted(true);
70 | }, []);
71 |
72 | if (!isMounted) {
73 | return null;
74 | }
75 |
76 | return (
77 |
124 |
125 | );
126 | };
127 |
128 | export default ConversationForm;
129 |
--------------------------------------------------------------------------------
/src/app/api/image/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from '@clerk/nextjs/server';
2 | import { eq } from 'drizzle-orm';
3 | import { NextResponse } from 'next/server';
4 | import { Image, ImageGenerateParams } from 'openai/resources/images.mjs';
5 |
6 | import { Amount } from '@/app/(routes)/image/data';
7 | import { db } from '@/db';
8 | import { image, imagePrompt, users } from '@/db/schema';
9 | import { deleteImageFromAzureBlob, uploadImageToAzureBlob } from '@/lib/azure';
10 | import { openai } from '@/lib/open-ai';
11 |
12 | interface ImageRequest {
13 | prompt: string;
14 | amount?: Amount;
15 | resolution?: ImageGenerateParams['size'];
16 | model?: ImageGenerateParams['model'];
17 | }
18 |
19 | export const POST = async (req: Request) => {
20 | try {
21 | const { userId } = auth();
22 |
23 | if (!userId) {
24 | return new NextResponse('Unauthorized', { status: 401 });
25 | }
26 |
27 | if (!openai.apiKey) {
28 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
29 | }
30 |
31 | const body = await req.json();
32 | const {
33 | prompt,
34 | amount = '1',
35 | resolution = '512x512',
36 | model = 'dall-e-2',
37 | }: ImageRequest = body;
38 |
39 | const n = model === 'dall-e-3' ? 1 : parseInt(amount);
40 |
41 | if (!prompt) {
42 | return new NextResponse('Prompt is required', { status: 400 });
43 | }
44 |
45 | const response = await openai.images.generate({
46 | prompt,
47 | model,
48 | n,
49 | size: resolution,
50 | });
51 |
52 | if (!response || !response.data) {
53 | return new NextResponse('Internal Error', { status: 500 });
54 | }
55 |
56 | const user = await db.query.users.findFirst({
57 | where: eq(users.userId, userId),
58 | });
59 |
60 | if (!user) {
61 | return new NextResponse('Unauthorized', { status: 401 });
62 | }
63 |
64 | const promptId = await db
65 | .insert(imagePrompt)
66 | .values({
67 | authorId: user.id,
68 | prompt,
69 | amount: n,
70 | resolution: resolution || '',
71 | model: model || '',
72 | })
73 | .returning({ id: imagePrompt.id });
74 |
75 | await Promise.all(
76 | response.data.map(async (imageData: Image) => {
77 | const { url: imageUrl } = imageData;
78 |
79 | if (imageUrl) {
80 | try {
81 | const { key, url } = await uploadImageToAzureBlob(
82 | imageUrl,
83 | process.env.AZURE_STORAGE_CONTAINER,
84 | );
85 |
86 | await db.insert(image).values({
87 | promptId: promptId[0].id,
88 | key,
89 | url,
90 | });
91 | } catch (err) {
92 | console.error('Upload failed', err);
93 | }
94 | }
95 | }),
96 | );
97 |
98 | const imagePrompts = await db.query.imagePrompt.findMany({
99 | where: eq(imagePrompt.authorId, user.id),
100 | with: { images: true },
101 | });
102 |
103 | return NextResponse.json(imagePrompts, { status: 200 });
104 | } catch (err) {
105 | console.log('IMAGE_ERROR:', err);
106 | return new NextResponse('Internal Error', { status: 500 });
107 | }
108 | };
109 |
110 | export const DELETE = async (req: Request) => {
111 | try {
112 | const { userId } = auth();
113 |
114 | if (!userId) {
115 | return new NextResponse('Unauthorized', { status: 401 });
116 | }
117 |
118 | if (!openai.apiKey) {
119 | return new NextResponse('OpenAI API Key not configured', { status: 500 });
120 | }
121 |
122 | const user = await db.query.users.findFirst({
123 | where: eq(users.userId, userId),
124 | });
125 |
126 | if (!user) {
127 | return new NextResponse('Unauthorized', { status: 401 });
128 | }
129 |
130 | const imagePrompts = await db.query.imagePrompt.findMany({
131 | where: eq(imagePrompt.authorId, user.id),
132 | with: { images: true },
133 | columns: {},
134 | });
135 |
136 | await Promise.all(
137 | imagePrompts.map(
138 | async ({ images }) =>
139 | await Promise.all(
140 | images.map(async ({ key }) => {
141 | await deleteImageFromAzureBlob(
142 | key,
143 | process.env.AZURE_STORAGE_CONTAINER,
144 | );
145 | }),
146 | ),
147 | ),
148 | );
149 |
150 | await db
151 | .delete(imagePrompt)
152 | .where(eq(imagePrompt.authorId, user.id))
153 | .execute();
154 |
155 | return new NextResponse('Deleted Images', { status: 200 });
156 | } catch (err: any) {
157 | console.log('IMAGES_DELETE_ERROR:', err);
158 | return new NextResponse('Internal Error', { status: 500 });
159 | }
160 | };
161 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from '@radix-ui/react-label';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import * as React from 'react';
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from 'react-hook-form';
12 |
13 | import { Label } from '@/components/ui/label';
14 | import { cn } from '@/lib/utils';
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName;
23 | };
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | );
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error('useFormField should be used within ');
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | );
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | });
85 | FormItem.displayName = 'FormItem';
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField();
92 |
93 | return (
94 |
100 | );
101 | });
102 | FormLabel.displayName = 'FormLabel';
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | });
125 | FormControl.displayName = 'FormControl';
126 |
127 | const FormDescription = React.forwardRef<
128 | HTMLParagraphElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => {
131 | const { formDescriptionId } = useFormField();
132 |
133 | return (
134 |
140 | );
141 | });
142 | FormDescription.displayName = 'FormDescription';
143 |
144 | const FormMessage = React.forwardRef<
145 | HTMLParagraphElement,
146 | React.HTMLAttributes
147 | >(({ className, children, ...props }, ref) => {
148 | const { error, formMessageId } = useFormField();
149 | const body = error ? String(error?.message) : children;
150 |
151 | if (!body) {
152 | return null;
153 | }
154 |
155 | return (
156 |
162 | {body}
163 |
164 | );
165 | });
166 | FormMessage.displayName = 'FormMessage';
167 |
168 | export {
169 | Form,
170 | FormControl,
171 | FormDescription,
172 | FormField,
173 | FormItem,
174 | FormLabel,
175 | FormMessage,
176 | useFormField,
177 | };
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI SaaS Platform
2 |
3 | Welcome to our AI SaaS Platform, your all-in-one solution for generating dynamic content including conversations, images, videos, music, and code snippets. Our platform leverages cutting-edge artificial intelligence algorithms to provide seamless and high-quality content generation tailored to your needs.
4 |
5 | ## Key Features
6 |
7 | ### Content Generation
8 |
9 | - **Conversation**: Generate realistic and engaging conversations between virtual characters or users.
10 | - **Images**: Create stunning images tailored to your specifications using advanced image generation techniques.
11 | - **Videos**: Generate professional-quality videos with custom settings, themes, and styles.
12 | - **Music**: Compose original music tracks or remix existing ones with our AI-powered music generation tools.
13 | - **Code**: Automatically generate code snippets for various programming languages based on specific requirements.
14 |
15 | ### Customization Options
16 |
17 | - **Initial Context Prompt**: Provide an initial context prompt to guide the content generation process and ensure that the generated content aligns with your specific requirements and preferences.
18 | - **Parameters and Settings**: Fine-tune the generation process by adjusting parameters and settings according to your needs.
19 |
20 | ## Deployment URL
21 |
22 | The AI SaaS platform is deployed and accessible at: [https://d2czldtmzevope.cloudfront.net/](https://d2czldtmzevope.cloudfront.net/)
23 |
24 | This is coupled with a database run on an EC2 instance using docker and other AWS resources created with SST.
25 |
26 | ## Run AI SaaS (Yourself)
27 |
28 | - **`docker-compose up`**: This command orchestrates the startup of Docker containers as defined in the `compose.yaml` file. It's commonly used to start all the services and dependencies required for the AI SaaS platform, such as databases, message brokers, or other microservices.
29 |
30 | - **`npx drizzle-kit studio`**: This command starts the Drizzle Kit Studio, which is a graphical user interface (GUI) tool used for managing and developing applications built with Drizzle, a framework or toolset used in the project.
31 |
32 | - **`npx sst dev`**: This command starts the Serverless Stack (SST) framework in development mode. SST is a framework used for building serverless applications on AWS (Amazon Web Services). Running in development mode allows developers to test and iterate on their serverless application locally before deploying it to the cloud.
33 |
34 | - **`npm run dev`**: This command starts the development server for the Npm framework. Npm is likely the framework or toolset used for developing the AI SaaS platform. Running the development server enables developers to preview and test their changes in a local environment before deploying them to production.
35 |
36 | These commands are essential for running and developing the AI SaaS platform locally, providing a streamlined workflow for development and testing. Adjustments or additional commands may be necessary based on the specific setup and requirements of your project.
37 |
38 | ### Env Variables
39 |
40 | - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
41 | - CLERK_SECRET_KEY=
42 | - NEXT_PUBLIC_CLERK_SIGN_IN_URL=
43 | - NEXT_PUBLIC_CLERK_SIGN_UP_URL=
44 | - NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=
45 | - NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=
46 | - OPENAI_API_KEY=
47 | - REPLICATE_API_TOKEN=
48 | - DB_HOST=
49 | - DB_PORT=
50 | - DB_NAME=
51 | - DB_USER=
52 | - DB_PASSWORD=
53 |
54 | ## Deploy to production
55 |
56 | `npx sst deploy --stage prod`: This command deploys the AI SaaS platform to Amazon Web Services (AWS) using the Serverless Stack (SST) framework. The --stage prod flag specifies that the deployment is intended for the production stage, ensuring that the resources are provisioned in the production environment.
57 |
58 | ## Technologies Used
59 |
60 | - **AWS S3**: Utilized for scalable and secure storage of media files such as images, videos, and music generated by the AI SaaS platform.
61 | - **Docker**: Employed for containerization of the application, including services like Postgres for database management and Adminer for database administration.
62 | - **Drizzle**: Integrated for managing and developing applications, providing tools and frameworks for efficient development workflows.
63 | - **OpenAI / Replicate API**: Leveraged for accessing advanced AI capabilities, such as generating conversation, images, videos, music, and code snippets, through the Replicate API provided by OpenAI.
64 |
65 | ## Screenshots
66 |
67 | #### Conversation Page
68 |
69 | 
70 |
71 | #### Image Page
72 |
73 | 
74 |
75 | #### Video Page
76 |
77 | 
78 |
79 | #### Music Page
80 |
81 | 
82 |
83 | #### Code Page
84 |
85 | 
86 |
87 | ## License
88 |
89 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
90 |
91 | ## Contact
92 |
93 | If you have any questions or suggestions, please feel free to reach out:
94 |
95 | - Email: kpirabaharan3@gmail.com
96 | - LinkedIn: [https://linkedin.com/in/kpirabaharan/](https://linkedin.com/in/kpirabaharan/)
97 |
--------------------------------------------------------------------------------
/src/app/(routes)/image/components/image-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import axios from 'axios';
5 | import { useRouter } from 'next/navigation';
6 | import { useForm } from 'react-hook-form';
7 | import { toast } from 'sonner';
8 |
9 | import { useImage } from '@/hooks/useImage';
10 | import {
11 | ImageFormSchema,
12 | ImageFormValues,
13 | amountOptions,
14 | resolutionOptions,
15 | } from '../data';
16 |
17 | import { Button } from '@/components/ui/button';
18 | import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
19 | import { Input } from '@/components/ui/input';
20 | import {
21 | Select,
22 | SelectContent,
23 | SelectItem,
24 | SelectTrigger,
25 | SelectValue,
26 | } from '@/components/ui/select';
27 |
28 | const ImageForm = () => {
29 | const router = useRouter();
30 | const { setImagePrompts } = useImage();
31 | const imageForm = useForm({
32 | resolver: zodResolver(ImageFormSchema),
33 | defaultValues: {
34 | prompt: '',
35 | amount: '1',
36 | resolution: '512x512 dall-e-2',
37 | },
38 | });
39 |
40 | // Loading State
41 | const isLoading = imageForm.formState.isSubmitting;
42 |
43 | const onSubmit = async (values: ImageFormValues) => {
44 | try {
45 | const [resolution, model] = values.resolution.split(' ');
46 |
47 | const responsePromise = axios.post('/api/image', {
48 | ...values,
49 | resolution,
50 | model,
51 | });
52 |
53 | toast.promise(responsePromise, {
54 | id: 'Image',
55 | position: 'top-right',
56 | loading: 'ChatXYZ is drawing...',
57 | error: 'Something went wrong.',
58 | });
59 |
60 | const response = await responsePromise;
61 |
62 | if (response.status === 200) {
63 | setImagePrompts(response.data);
64 | imageForm.reset();
65 | }
66 | } catch (err: any) {
67 | toast.error(err.message);
68 | console.log(err);
69 | } finally {
70 | router.refresh();
71 | }
72 | };
73 |
74 | return (
75 |
168 |
169 | );
170 | };
171 |
172 | export default ImageForm;
173 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as SelectPrimitive from '@radix-ui/react-select';
4 | import { Check, ChevronDown, ChevronUp } from 'lucide-react';
5 | import * as React from 'react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1',
23 | className,
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ));
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ));
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName;
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = 'popper', ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ));
100 | SelectContent.displayName = SelectPrimitive.Content.displayName;
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | SelectItem.displayName = SelectPrimitive.Item.displayName;
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ));
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148 |
149 | export {
150 | Select,
151 | SelectContent,
152 | SelectGroup,
153 | SelectItem,
154 | SelectLabel,
155 | SelectScrollDownButton,
156 | SelectScrollUpButton,
157 | SelectSeparator,
158 | SelectTrigger,
159 | SelectValue,
160 | };
161 |
--------------------------------------------------------------------------------