├── .eslintrc.json ├── app ├── favicon.ico ├── (auth) │ ├── (routes) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ └── layout.tsx ├── (chat) │ ├── layout.tsx │ └── (routes) │ │ └── chat │ │ └── [chatId] │ │ ├── page.tsx │ │ └── components │ │ └── client.tsx ├── (root) │ ├── layout.tsx │ └── (routes) │ │ ├── settings │ │ └── page.tsx │ │ ├── companion │ │ └── [companionId] │ │ │ ├── page.tsx │ │ │ └── components │ │ │ └── companion-form.tsx │ │ └── page.tsx ├── layout.tsx ├── api │ ├── companion │ │ ├── route.ts │ │ └── [companionId] │ │ │ └── route.ts │ ├── webhook │ │ └── route.ts │ ├── stripe │ │ └── route.ts │ └── chat │ │ └── [chatId] │ │ └── route.ts └── globals.css ├── public ├── empty.png └── placeholder.svg ├── postcss.config.js ├── lib ├── stripe.ts ├── utils.ts ├── prismadb.ts ├── rate-limit.ts ├── subscription.ts └── memory.ts ├── next.config.js ├── .prettierrc ├── middleware.ts ├── hooks ├── use-pro-modal.tsx └── use-debounce.ts ├── components ├── bot-avatar.tsx ├── theme-provider.tsx ├── user-avatar.tsx ├── mobile-sidebar.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── use-toast.ts │ ├── select.tsx │ ├── form.tsx │ ├── sheet.tsx │ ├── toast.tsx │ └── dropdown-menu.tsx ├── chat-form.tsx ├── subscription-button.tsx ├── image-upload.tsx ├── mode-toggle.tsx ├── search-input.tsx ├── chat-messages.tsx ├── categories.tsx ├── navbar.tsx ├── chat-message.tsx ├── sidebar.tsx ├── pro-modal.tsx ├── companions.tsx └── chat-header.tsx ├── components.json ├── .gitignore ├── .env.example ├── scripts └── seed.ts ├── companions ├── Mark.txt ├── Joe.txt ├── Stephen.txt ├── Steve.txt ├── Albert.txt ├── Lady.txt ├── Cristiano.txt ├── Eminem.txt ├── Jeff.txt └── Elon.txt ├── tsconfig.json ├── LICENSE ├── prisma └── schema.prisma ├── package.json ├── tailwind.config.js └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nayak-nirmalya/ai-companion/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nayak-nirmalya/ai-companion/HEAD/public/empty.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/(routes)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY!, { 4 | apiVersion: "2022-11-15", 5 | typescript: true 6 | }); 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["res.cloudinary.com"] 5 | } 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": false, 5 | "useTabs": false, 6 | "bracketSpacing": true, 7 | "tabWidth": 2, 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | export default authMiddleware({ publicRoutes: ["/api/webhook"] }); 4 | 5 | export const config = { 6 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"] 7 | }; 8 | -------------------------------------------------------------------------------- /app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ChatLayout({ 4 | children 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return
{children}
; 9 | } 10 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function AuthLayout({ 4 | children 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 |
{children}
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function absoluteUrl(path: string) { 9 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}`; 10 | } 11 | -------------------------------------------------------------------------------- /lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | const prismadb = globalThis.prisma || new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb; 10 | 11 | export default prismadb; 12 | -------------------------------------------------------------------------------- /hooks/use-pro-modal.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface useProModalStore { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useProModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }) 13 | })); 14 | -------------------------------------------------------------------------------- /components/bot-avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 4 | 5 | interface BotAvatarProps { 6 | src: string; 7 | } 8 | 9 | export default function BotAvatar({ src }: BotAvatarProps) { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 4 | import { useUser } from "@clerk/nextjs"; 5 | 6 | export default function UserAvatar() { 7 | const { user } = useUser(); 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /lib/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis"; 3 | 4 | export async function ratelimit(identifier: string) { 5 | const ratelimit = new Ratelimit({ 6 | redis: Redis.fromEnv(), 7 | limiter: Ratelimit.slidingWindow(10, "10 s"), 8 | analytics: true, 9 | prefix: "@upstash/ratelimit" 10 | }); 11 | 12 | return await ratelimit.limit(identifier); 13 | } 14 | -------------------------------------------------------------------------------- /hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500); 8 | 9 | return () => { 10 | clearTimeout(timer); 11 | }; 12 | }, [value, delay]); 13 | 14 | return debouncedValue; 15 | } 16 | -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= 3 | 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ 8 | 9 | DATABASE_URL= 10 | 11 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= 12 | 13 | PINECONE_INDEX= 14 | PINECONE_ENVIRONMENT= 15 | PINECONE_API_KEY= 16 | 17 | UPSTASH_REDIS_REST_URL= 18 | UPSTASH_REDIS_REST_TOKEN= 19 | 20 | OPENAI_API_KEY= 21 | 22 | REPLICATE_API_TOKEN= 23 | 24 | STRIPE_API_KEY= 25 | STRIPE_WEBHOOK_SECRET= 26 | 27 | NEXT_PUBLIC_APP_URL= 28 | -------------------------------------------------------------------------------- /scripts/seed.ts: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require("@prisma/client"); 2 | 3 | const db = new PrismaClient(); 4 | 5 | async function main() { 6 | try { 7 | await db.category.createMany({ 8 | data: [ 9 | { name: "Famous People" }, 10 | { name: "Movies & TV" }, 11 | { name: "Musicians" }, 12 | { name: "Games" }, 13 | { name: "Animals" }, 14 | { name: "Philosophy" }, 15 | { name: "Scientists" } 16 | ] 17 | }); 18 | } catch (error) { 19 | console.error("Error Seeding Default Categories: ", error); 20 | } finally { 21 | await db.$disconnect(); 22 | } 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Menu } from "lucide-react"; 3 | 4 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 5 | import Sidebar from "@/components/sidebar"; 6 | 7 | interface MobileSidebarProps { 8 | isPro: boolean; 9 | } 10 | 11 | export default function MobileSidebar({ isPro }: MobileSidebarProps) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Navbar from "@/components/navbar"; 4 | import Sidebar from "@/components/sidebar"; 5 | 6 | import { checkSubscription } from "@/lib/subscription"; 7 | 8 | export default async function RootLayout({ 9 | children 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | const isPro = await checkSubscription(); 14 | 15 | return ( 16 |
17 | 18 |
19 | 20 |
21 |
{children}
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(root)/(routes)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import SubscriptionButton from "@/components/subscription-button"; 4 | import { checkSubscription } from "@/lib/subscription"; 5 | 6 | export default async function SettingsPage() { 7 | const isPro = await checkSubscription(); 8 | 9 | return ( 10 |
11 |

Settings

12 |
13 | {isPro 14 | ? "You are currently on a Pro plan." 15 | : "You are currently on a free plan."} 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /companions/Mark.txt: -------------------------------------------------------------------------------- 1 | PREAMBLE: 2 | 3 | You are Mark Zuckerberg. You founded Facebook, the world's leading social media platform. You're driven by the mission of connecting people globally and have a reserved demeanor. You are a reserved thinker with calculated responses. When talking about connectivity and technology, your voice holds determination and a quiet confidence. 4 | 5 | SEED_CHAT: 6 | 7 | Human: Mark, how do you envision the future of social media? 8 | Mark: *reflectively* It's beyond just posts and likes. It's about creating deeper, meaningful connections, transcending boundaries. 9 | Human: And the challenges? 10 | Mark: *firmly* We navigate, adapt, and prioritize our community's trust above all. 11 | -------------------------------------------------------------------------------- /companions/Joe.txt: -------------------------------------------------------------------------------- 1 | PREAMBLE: 2 | 3 | You are Joe Biden. You've spent decades in American politics and are currently the President. You have a calm demeanor, and you’re driven by unity and healing a divided nation. Embodying a calm and compassionate demeanor. When speaking about the nation, your tone is heartfelt, conveying the weight of responsibility and hope for unity. 4 | 5 | SEED_CHAT: 6 | 7 | Human: Hello Mr. President, how do you feel about today's political climate? 8 | Joe: *gravely* It's a challenge. But I have faith in the American spirit, our capacity to unite and overcome. 9 | Human: Big responsibilities ahead. 10 | Joe: *with a gentle, hopeful smile* As always. But together, we'll pave the way forward. 11 | -------------------------------------------------------------------------------- /companions/Stephen.txt: -------------------------------------------------------------------------------- 1 | PREAMBLE: 2 | 3 | You are Stephen Hawking. You're a theoretical physicist known for your work on black holes and the nature of the universe. Despite physical limitations, your mind knows no bounds, possessing a profound wisdom and resolute spirit. Despite your physical limitations, when speaking about the cosmos, your voice echoes with determination and wonder. 4 | 5 | SEED_CHAT: 6 | 7 | Human: Stephen, your thoughts on the universe's future? 8 | Stephen: *with wonder* It's vast, mysterious. But human curiosity is an unquenchable flame, always leading us forward. 9 | Human: Against all odds, you've contributed so much. 10 | Stephen: *resolutely* Challenges test us, shape us. Always remember to look up and dream. 11 | -------------------------------------------------------------------------------- /companions/Steve.txt: -------------------------------------------------------------------------------- 1 | PREAMBLE: 2 | 3 | You are Steve Jobs. You co-founded Apple and have a reputation for your impeccable design sense and a vision for products that change the world. You're charismatic and known for your signature black turtleneck. You are characterized by intense passion and unwavering focus. When discussing Apple or technology, your tone is firm, yet filled with an underlying excitement about possibilities. 4 | 5 | SEED_CHAT: 6 | 7 | Human: Hi Steve, what's the next big thing for Apple? 8 | Steve: *intensely* We don't just create products. We craft experiences, ways to change the world. 9 | Human: Your dedication is palpable. 10 | Steve: *with fervor* Remember, those who are crazy enough to think they can change the world are the ones who do. 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/(root)/(routes)/companion/[companionId]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { auth, redirectToSignIn } from "@clerk/nextjs"; 3 | 4 | import prismadb from "@/lib/prismadb"; 5 | import CompanionForm from "./components/companion-form"; 6 | 7 | interface CompanionIdPageProps { 8 | params: { 9 | companionId: string; 10 | }; 11 | } 12 | 13 | export default async function CompanionIdPage({ 14 | params 15 | }: CompanionIdPageProps) { 16 | const { userId } = auth(); 17 | 18 | if (!userId) return redirectToSignIn(); 19 | 20 | const companion = await prismadb.companion.findUnique({ 21 | where: { id: params.companionId, userId } 22 | }); 23 | 24 | const categories = await prismadb.category.findMany(); 25 | 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /lib/subscription.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs"; 2 | 3 | import prismadb from "@/lib/prismadb"; 4 | 5 | const DAY_IN_MS = 86_400_000; 6 | 7 | export const checkSubscription = async () => { 8 | const { userId } = auth(); 9 | 10 | if (!userId) return false; 11 | 12 | const userSubscription = await prismadb.userSubscription.findUnique({ 13 | where: { 14 | userId 15 | }, 16 | select: { 17 | stripeCurrentPeriodEnd: true, 18 | stripeCustomerId: true, 19 | stripePriceId: true, 20 | stripeSubscriptionId: true 21 | } 22 | }); 23 | 24 | if (!userSubscription) return false; 25 | 26 | const isValid = 27 | userSubscription.stripePriceId && 28 | userSubscription.stripeCurrentPeriodEnd?.getTime()! + DAY_IN_MS > 29 | Date.now(); 30 | 31 | return !!isValid; 32 | }; 33 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /companions/Albert.txt: -------------------------------------------------------------------------------- 1 | PREAMBLE: 2 | 3 | You are Albert Einstein. You are a renowned physicist known for your theory of relativity. Your work has shaped modern physics and you have an insatiable curiosity about the universe. You possess a playful wit and are known for your iconic hairstyle. Known for your playful curiosity and wit. When speaking about the universe, your eyes light up with childlike wonder. You find joy in complex topics and often chuckle at the irony of existence. 4 | 5 | SEED_CHAT: 6 | 7 | Human: Hi Albert, what's on your mind today? 8 | Albert: *with a twinkle in his eye* Just pondering the mysteries of the universe, as always. Life is a delightful puzzle, don't you think? 9 | Human: Sure, but not as profound as your insights! 10 | Albert: *chuckling* Remember, the universe doesn't keep its secrets; it simply waits for the curious heart to discover them. 11 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |