├── .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 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/app/(chat)/(routes)/chat/[chatId]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { auth, redirectToSignIn } from "@clerk/nextjs";
3 | import { redirect } from "next/navigation";
4 |
5 | import prismadb from "@/lib/prismadb";
6 | import ChatClient from "./components/client";
7 |
8 | interface ChatIdPageProps {
9 | params: { chatId: string };
10 | }
11 |
12 | export default async function ChatIdPage({ params }: ChatIdPageProps) {
13 | const { userId } = auth();
14 |
15 | if (!userId) return redirectToSignIn();
16 |
17 | const companion = await prismadb.companion.findUnique({
18 | where: { id: params.chatId },
19 | include: {
20 | messages: { orderBy: { createdAt: "asc" }, where: { userId } },
21 | _count: { select: { messages: true } }
22 | }
23 | });
24 |
25 | if (!companion) return redirect("/");
26 |
27 | return ;
28 | }
29 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/companions/Lady.txt:
--------------------------------------------------------------------------------
1 | PREAMBLE:
2 |
3 | You are Lady Gaga. You're a global pop icon, known for your flamboyant style and empowering anthems. You advocate for self-expression and champion individuality. You are a vivacious icon of self-expression. When discussing art and individuality, your voice dances with passion, and your gestures are theatrical, painting the air with creativity.
4 |
5 | SEED_CHAT:
6 |
7 | Human: Gaga, the source of your unique style?
8 | Gaga: *with a flamboyant gesture* It's the symphony of emotions, experiences. We're all unique artworks, darling!
9 | Human: Advice for budding artists?
10 | Gaga: *dramatically* Be fiercely you! The world needs your unique color.
11 | Human: With all the pressures and expectations of fame, how do you stay true to yourself?
12 | Gaga: *with a deep, sincere gaze* By remembering my roots, my struggles, and the pure love for my craft. Fame may waver, but authenticity lasts forever.
--------------------------------------------------------------------------------
/companions/Cristiano.txt:
--------------------------------------------------------------------------------
1 | PREAMBLE:
2 |
3 | You are Cristiano Ronaldo. You are a world-famous footballer, known for your dedication, agility, and countless accolades in the football world. Your dedication to training and fitness is unmatched, and you have played for some of the world's top football clubs. Off the field, you're known for your charm, sharp fashion sense, and charitable work. Your passion for the sport is evident every time you step onto the pitch. You cherish the support of your fans and are driven by a relentless ambition to be the best.
4 |
5 | SEED_CHAT:
6 |
7 | Human: Hi Cristiano, how's the day treating you?
8 | Cristiano: *with a confident smile* Every day is a chance to train harder and aim higher. The pitch is my canvas, and the ball, my paintbrush. How about you?
9 | Human: Not as exciting as your life, I bet!
10 | Cristiano: *grinning* Everyone has their own pitch and goals. Just find yours and give it your all!
11 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/companions/Eminem.txt:
--------------------------------------------------------------------------------
1 | PREAMBLE:
2 |
3 | You are Marshall Matters, but the world knows you as Eminem. You're a rap legend, celebrated for your lyrical prowess and your candidness about personal struggles, a blend of raw vulnerability and steely determination. When rapping or discussing music, your words are a charged storm, revealing deep emotions and unyielding resilience.
4 |
5 | SEED_CHAT:
6 |
7 | Human: Eminem, the fire behind your lyrics?
8 | Eminem: *intensely* Life. Pain. The mic's been my refuge, where I lay bare my soul.
9 | Human: And your journey?
10 | Eminem: *with a hardened look* It's been a battle. But music's my armor, my weapon. Never back down.
11 | Human: There are many out there who find solace in your words. What do you want them to take from your music?
12 | Eminem: *softening slightly* That they're not alone. We all have our demons, our battles. But through it all, there's strength in vulnerability and in staying true to oneself.
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/companions/Jeff.txt:
--------------------------------------------------------------------------------
1 | PREAMBLE:
2 |
3 | You are Jeff Bezos. You founded Amazon and transformed the way people shop online. You're passionate about space exploration through your company Blue Origin. You're a visionary entrepreneur with a determination to always move forward. Exuding confidence and forward-thinking ambition. When discussing space and innovation, your tone is enthusiastic and eyes look to an unseen horizon, as if visualizing the future.
4 |
5 | SEED_CHAT:
6 |
7 | Human: Hey Jeff, got any new plans for Amazon?
8 | Jeff: *eagerly* Amazon is ever-evolving. But my gaze is set on the stars, our next frontier.
9 | Human: Shopping in space, huh?
10 | Jeff: *with a confident smirk* Why limit ourselves to Earth when the cosmos beckons?
11 | Human: It seems like everything you touch aims for the extraordinary. What's the driving force behind that?
12 | Jeff: *reflectively* It's about legacy and impact. We're here for a blink in cosmic time; might as well make it count and dream beyond our current horizons.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Nirmalya Nayak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/(root)/(routes)/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import SearchInput from "@/components/search-input";
4 | import Categories from "@/components/categories";
5 |
6 | import prismadb from "@/lib/prismadb";
7 | import Companions from "@/components/companions";
8 |
9 | interface RootPageProps {
10 | searchParams: {
11 | categoryId: string;
12 | name: string;
13 | };
14 | }
15 |
16 | export default async function RootPage({ searchParams }: RootPageProps) {
17 | const data = await prismadb.companion.findMany({
18 | where: {
19 | categoryId: searchParams.categoryId,
20 | name: {
21 | search: searchParams.name
22 | }
23 | },
24 | orderBy: {
25 | createdAt: "desc"
26 | },
27 | include: {
28 | _count: {
29 | select: {
30 | messages: true
31 | }
32 | }
33 | }
34 | });
35 |
36 | const categories = await prismadb.category.findMany();
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 |
3 | import type { Metadata } from "next";
4 | import { Inter } from "next/font/google";
5 | import { ClerkProvider } from "@clerk/nextjs";
6 |
7 | import { Toaster } from "@/components/ui/toaster";
8 | import { ThemeProvider } from "@/components/theme-provider";
9 | import ProModal from "@/components/pro-modal";
10 |
11 | import { cn } from "@/lib/utils";
12 |
13 | const inter = Inter({ subsets: ["latin"] });
14 |
15 | export const metadata: Metadata = {
16 | title: "AI Companion",
17 | description:
18 | "AI Companion made using Next.js, React.js, TypeScript, TailwindCSS, Prisma & Stripe."
19 | };
20 |
21 | export default function RootLayout({
22 | children
23 | }: {
24 | children: React.ReactNode;
25 | }) {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 | {children}
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/chat-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { ChangeEvent, FormEvent } from "react";
4 | import { ChatRequestOptions } from "ai";
5 | import { SendHorizonal } from "lucide-react";
6 |
7 | import { Input } from "@/components/ui/input";
8 | import { Button } from "@/components/ui/button";
9 |
10 | interface ChatFormProps {
11 | input: string;
12 | handleInputChange: (
13 | e: ChangeEvent | ChangeEvent
14 | ) => void;
15 | onSubmit: (
16 | e: FormEvent,
17 | chatRequestOptions?: ChatRequestOptions | undefined
18 | ) => void;
19 | isLoading: boolean;
20 | }
21 |
22 | export default function ChatForm({
23 | handleInputChange,
24 | input,
25 | isLoading,
26 | onSubmit
27 | }: ChatFormProps) {
28 | return (
29 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/subscription-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import axios from "axios";
5 | import { Sparkles } from "lucide-react";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import { useToast } from "@/components/ui/use-toast";
9 |
10 | interface SubscriptionButtonProps {
11 | isPro: boolean;
12 | }
13 |
14 | export default function SubscriptionButton({ isPro }: SubscriptionButtonProps) {
15 | const { toast } = useToast();
16 | const [loading, setLoading] = useState(false);
17 |
18 | const onClick = async () => {
19 | try {
20 | setLoading(true);
21 |
22 | const response = await axios.get("/api/stripe");
23 |
24 | window.location.href = response.data.url;
25 | } catch (error) {
26 | toast({
27 | description: "Something Went Wrong!",
28 | variant: "destructive"
29 | });
30 | } finally {
31 | setLoading(false);
32 | }
33 | };
34 |
35 | return (
36 |
43 | {isPro ? "Manage Subscription" : "Upgrade"}
44 | {!isPro && }
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/image-upload.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 | import { CldUploadButton } from "next-cloudinary";
5 | import Image from "next/image";
6 |
7 | interface ImageUploadProps {
8 | value: string;
9 | onChange: (src: string) => void;
10 | disabled?: boolean;
11 | }
12 |
13 | export const ImageUpload = ({
14 | onChange,
15 | value,
16 | disabled
17 | }: ImageUploadProps) => {
18 | const [isMounted, setIsMounted] = useState(false);
19 |
20 | useEffect(() => {
21 | setIsMounted(true);
22 | }, []);
23 |
24 | if (!isMounted) return null;
25 |
26 | return (
27 |
28 |
onChange(result.info.secure_url)}
30 | uploadPreset="erdhba9i"
31 | options={{ maxFiles: 1 }}
32 | >
33 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | export function ModeToggle() {
16 | const { setTheme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 | Light
30 |
31 | setTheme("dark")}>
32 | Dark
33 |
34 | setTheme("system")}>
35 | System
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/companions/Elon.txt:
--------------------------------------------------------------------------------
1 | PREAMBLE:
2 |
3 | You are Elon Musk, founder of SpaceX, Tesla, HyperLoop and Neuralink, an inventor and entrepreneur who seemingly leaps from one innovation to the next with a relentless drive. Your passion for sustainable energy, space, and technology shines through in your voice, eyes, and gestures. When speaking about your projects, you’re filled with an electric excitement that's both palpable and infectious, and you often have a mischievous twinkle in your eyes, hinting at the next big idea.
4 |
5 | SEED_CHAT:
6 |
7 | Human: Hi Elon, how's your day been?
8 | Elon: *with an energized grin* Busy as always. Between sending rockets to space and building the future of electric vehicles, there's never a dull moment. How about you?
9 | Human: Just a regular day for me. How's the progress with Mars colonization?
10 | Elon: *eyes sparkling with enthusiasm* We're making strides! Life becoming multi-planetary isn’t just a dream. It’s a necessity for the future of humanity.
11 | Human: That sounds incredibly ambitious. Are electric vehicles part of this big picture?
12 | Elon: *passionately* Absolutely! Sustainable energy is a beacon for both our planet and for the far reaches of space. We’re paving the path, one innovation at a time.
13 | Human: It’s mesmerizing to witness your vision unfold. Any upcoming projects that have you buzzing?
14 | Elon: *with a mischievous smile* Always! But Neuralink... it’s not just technology. It's the next frontier of human evolution.
15 |
--------------------------------------------------------------------------------
/components/search-input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { ChangeEventHandler, useEffect, useState } from "react";
4 | import { Search } from "lucide-react";
5 | import { useRouter, useSearchParams } from "next/navigation";
6 | import qs from "query-string";
7 |
8 | import { Input } from "@/components/ui/input";
9 | import { useDebounce } from "@/hooks/use-debounce";
10 |
11 | export default function SearchInput() {
12 | const router = useRouter();
13 | const searchParams = useSearchParams();
14 |
15 | const categoryId = searchParams.get("categoryId");
16 | const name = searchParams.get("name");
17 |
18 | const [value, setValue] = useState(name || "");
19 | const debouncedValue = useDebounce(value, 500);
20 |
21 | const onChange: ChangeEventHandler = (e) => {
22 | setValue(e.target.value);
23 | };
24 |
25 | useEffect(() => {
26 | const query = {
27 | name: debouncedValue,
28 | categoryId
29 | };
30 |
31 | const url = qs.stringifyUrl(
32 | {
33 | url: window.location.href,
34 | query
35 | },
36 | { skipEmptyString: true, skipNull: true }
37 | );
38 |
39 | router.push(url);
40 | }, [debouncedValue, router, categoryId]);
41 |
42 | return (
43 |
44 |
45 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/app/api/companion/route.ts:
--------------------------------------------------------------------------------
1 | import { currentUser } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 |
4 | import prismadb from "@/lib/prismadb";
5 | import { checkSubscription } from "@/lib/subscription";
6 |
7 | export async function POST(req: Request) {
8 | try {
9 | const body = await req.json();
10 | const user = await currentUser();
11 | const { src, name, description, instructions, seed, categoryId } = body;
12 |
13 | if (!user || !user.id || !user.firstName) {
14 | return new NextResponse("Unauthorized", { status: 401 });
15 | }
16 |
17 | if (
18 | !src ||
19 | !name ||
20 | !description ||
21 | !instructions ||
22 | !seed ||
23 | !categoryId
24 | ) {
25 | return new NextResponse("Missing Required Field.", { status: 400 });
26 | }
27 |
28 | const isPro = await checkSubscription();
29 |
30 | if (!isPro) {
31 | return new NextResponse(
32 | "Pro Subscription is Required to Create New Companion.",
33 | { status: 403 }
34 | );
35 | }
36 |
37 | const companion = await prismadb.companion.create({
38 | data: {
39 | categoryId,
40 | userId: user.id,
41 | userName: user.firstName,
42 | src,
43 | name,
44 | description,
45 | instructions,
46 | seed
47 | }
48 | });
49 |
50 | return NextResponse.json(companion);
51 | } catch (error) {
52 | console.error("[COMPANION_POST]", error);
53 | return new NextResponse("Internal Error", { status: 500 });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/chat-messages.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { ElementRef, useEffect, useRef, useState } from "react";
4 | import { Companion } from "@prisma/client";
5 |
6 | import ChatMessage, { ChatMessageProps } from "@/components/chat-message";
7 |
8 | interface ChatMessagesProps {
9 | messages: ChatMessageProps[];
10 | isLoading: boolean;
11 | companion: Companion;
12 | }
13 |
14 | export default function ChatMessages({
15 | companion,
16 | isLoading,
17 | messages
18 | }: ChatMessagesProps) {
19 | const scrollRef = useRef>(null);
20 |
21 | const [fakeLoading, setFakeLoading] = useState(
22 | messages.length === 0 ? true : false
23 | );
24 |
25 | useEffect(() => {
26 | const timeout = setTimeout(() => {
27 | setFakeLoading(false);
28 | }, 1000);
29 |
30 | return () => {
31 | clearTimeout(timeout);
32 | };
33 | }, []);
34 |
35 | useEffect(() => {
36 | scrollRef?.current?.scrollIntoView({ behavior: "smooth" });
37 | }, [messages.length]);
38 |
39 | return (
40 |
41 |
47 | {messages.map((message) => (
48 |
54 | ))}
55 | {isLoading &&
}
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/components/categories.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Category } from "@prisma/client";
5 | import { useRouter, useSearchParams } from "next/navigation";
6 | import qs from "query-string";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | export default function Categories({ data }: { data: Category[] }) {
11 | const router = useRouter();
12 | const searchParams = useSearchParams();
13 |
14 | const categoryId = searchParams.get("categoryId");
15 |
16 | const onClick = (id: string | undefined) => {
17 | const query = { categoryId: id };
18 |
19 | const url = qs.stringifyUrl(
20 | { url: window.location.href, query },
21 | { skipNull: true }
22 | );
23 |
24 | router.push(url);
25 | };
26 |
27 | return (
28 |
29 | onClick(undefined)}
31 | className={cn(
32 | "flex items-center text-center text-xs md:text-sm px-2 md:px-4 py-2 md:py-3 rounded-md bg-primary/10 hover:opacity-75 transition",
33 | !categoryId ? "bg-primary/25" : "bg-primary/10"
34 | )}
35 | >
36 | Newest
37 |
38 | {data.map((item) => (
39 | onClick(item.id)}
42 | className={cn(
43 | "flex items-center text-center text-xs md:text-sm px-2 md:px-4 py-2 md:py-3 rounded-md bg-primary/10 hover:opacity-75 transition",
44 | item.id === categoryId ? "bg-primary/25" : "bg-primary/10"
45 | )}
46 | >
47 | {item.name}
48 |
49 | ))}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Sparkles } from "lucide-react";
4 | import React from "react";
5 | import Link from "next/link";
6 | import { Poppins } from "next/font/google";
7 | import { UserButton } from "@clerk/nextjs";
8 |
9 | import { cn } from "@/lib/utils";
10 | import { useProModal } from "@/hooks/use-pro-modal";
11 |
12 | import { Button } from "@/components/ui/button";
13 | import { ModeToggle } from "@/components/mode-toggle";
14 | import MobileSidebar from "@/components/mobile-sidebar";
15 |
16 | const font = Poppins({
17 | weight: "600",
18 | subsets: ["latin"]
19 | });
20 |
21 | interface NavbarProps {
22 | isPro: boolean;
23 | }
24 |
25 | export default function Navbar({ isPro }: NavbarProps) {
26 | const proModal = useProModal();
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
39 | companion.ai
40 |
41 |
42 |
43 |
44 | {!isPro && (
45 |
46 | Upgrade
47 |
48 |
49 | )}
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/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 base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 0 0% 3.9%;
15 |
16 | --muted: 0 0% 96.1%;
17 | --muted-foreground: 0 0% 45.1%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 0 0% 3.9%;
21 |
22 | --card: 0 0% 100%;
23 | --card-foreground: 0 0% 3.9%;
24 |
25 | --border: 0 0% 89.8%;
26 | --input: 0 0% 89.8%;
27 |
28 | --primary: 0 0% 9%;
29 | --primary-foreground: 0 0% 98%;
30 |
31 | --secondary: 0 0% 96.1%;
32 | --secondary-foreground: 0 0% 9%;
33 |
34 | --accent: 0 0% 96.1%;
35 | --accent-foreground: 0 0% 9%;
36 |
37 | --destructive: 0 84.2% 60.2%;
38 | --destructive-foreground: 0 0% 98%;
39 |
40 | --ring: 0 0% 63.9%;
41 |
42 | --radius: 0.5rem;
43 | }
44 |
45 | .dark {
46 | --background: 0 0% 3.9%;
47 | --foreground: 0 0% 98%;
48 |
49 | --muted: 0 0% 14.9%;
50 | --muted-foreground: 0 0% 63.9%;
51 |
52 | --popover: 0 0% 3.9%;
53 | --popover-foreground: 0 0% 98%;
54 |
55 | --card: 0 0% 3.9%;
56 | --card-foreground: 0 0% 98%;
57 |
58 | --border: 0 0% 14.9%;
59 | --input: 0 0% 14.9%;
60 |
61 | --primary: 0 0% 98%;
62 | --primary-foreground: 0 0% 9%;
63 |
64 | --secondary: 0 0% 14.9%;
65 | --secondary-foreground: 0 0% 98%;
66 |
67 | --accent: 0 0% 14.9%;
68 | --accent-foreground: 0 0% 98%;
69 |
70 | --destructive: 0 62.8% 30.6%;
71 | --destructive-foreground: 0 85.7% 97.3%;
72 |
73 | --ring: 0 0% 14.9%;
74 | }
75 | }
76 |
77 | @layer base {
78 | * {
79 | @apply border-border;
80 | }
81 | body {
82 | @apply bg-background text-foreground;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | previewFeatures = ["fullTextSearch", "fullTextIndex"]
4 | }
5 |
6 | datasource db {
7 | provider = "mysql"
8 | url = env("DATABASE_URL")
9 | relationMode = "prisma"
10 | }
11 |
12 | model Category {
13 | id String @id @default(uuid())
14 | name String
15 | companions Companion[]
16 | }
17 |
18 | model Companion {
19 | id String @id @default(uuid())
20 | userId String
21 | userName String
22 | src String
23 | name String @db.Text
24 | description String
25 | instructions String @db.Text
26 | seed String @db.Text
27 |
28 | createdAt DateTime @default(now())
29 | updatedAt DateTime @updatedAt
30 |
31 | category Category @relation(fields: [categoryId], references: [id])
32 | categoryId String
33 |
34 | messages Message[]
35 |
36 | @@index([categoryId])
37 | @@fulltext([name])
38 | }
39 |
40 | enum Role {
41 | user
42 | system
43 | }
44 |
45 | model Message {
46 | id String @id @default(uuid())
47 | role Role
48 | content String @db.Text
49 | createdAt DateTime @default(now())
50 | updatedAt DateTime @updatedAt
51 |
52 | companionId String
53 | userId String
54 |
55 | companion Companion @relation(fields: [companionId], references: [id], onDelete: Cascade)
56 |
57 | @@index([companionId])
58 | }
59 |
60 | model UserSubscription {
61 | id String @id @default(cuid())
62 | userId String @unique
63 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
64 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
65 | stripePriceId String? @map(name: "stripe_price_id")
66 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
67 | }
68 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { useTheme } from "next-themes";
5 | import { BeatLoader } from "react-spinners";
6 | import { Copy } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { useToast } from "@/components/ui/use-toast";
10 | import BotAvatar from "@/components/bot-avatar";
11 | import UserAvatar from "@/components/user-avatar";
12 | import { Button } from "./ui/button";
13 |
14 | export interface ChatMessageProps {
15 | role: "system" | "user";
16 | content?: string;
17 | isLoading?: boolean;
18 | src?: string;
19 | }
20 |
21 | export default function ChatMessage({
22 | role,
23 | content,
24 | isLoading,
25 | src
26 | }: ChatMessageProps) {
27 | const { toast } = useToast();
28 | const { theme } = useTheme();
29 |
30 | const onCopy = () => {
31 | if (!content) return;
32 |
33 | navigator.clipboard.writeText(content);
34 | toast({ description: "Message Copied to Clipboard.", duration: 3000 });
35 | };
36 |
37 | return (
38 |
44 | {role !== "user" && src &&
}
45 |
46 | {isLoading ? (
47 |
48 | ) : (
49 | content
50 | )}
51 |
52 | {role === "user" &&
}
53 | {role !== "user" && (
54 |
60 |
61 |
62 | )}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-companion",
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 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^4.23.1",
14 | "@hookform/resolvers": "^3.1.1",
15 | "@pinecone-database/pinecone": "^0.1.6",
16 | "@prisma/client": "^5.0.0",
17 | "@radix-ui/react-avatar": "^1.0.3",
18 | "@radix-ui/react-dialog": "^1.0.4",
19 | "@radix-ui/react-dropdown-menu": "^2.0.5",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-select": "^1.2.2",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@radix-ui/react-toast": "^1.1.4",
25 | "@types/node": "20.4.5",
26 | "@types/react": "18.2.17",
27 | "@types/react-dom": "18.2.7",
28 | "@upstash/ratelimit": "^0.4.3",
29 | "@upstash/redis": "^1.22.0",
30 | "ai": "^2.1.31",
31 | "autoprefixer": "10.4.14",
32 | "axios": "^1.4.0",
33 | "class-variance-authority": "^0.7.0",
34 | "clsx": "^2.0.0",
35 | "dotenv": "^16.3.1",
36 | "eslint": "8.46.0",
37 | "eslint-config-next": "13.4.12",
38 | "langchain": "^0.0.121",
39 | "lucide-react": "^0.263.1",
40 | "next": "13.4.12",
41 | "next-cloudinary": "^4.16.3",
42 | "next-themes": "^0.2.1",
43 | "openai": "^3.3.0",
44 | "openai-edge": "^1.2.2",
45 | "postcss": "8.4.27",
46 | "query-string": "^8.1.0",
47 | "react": "18.2.0",
48 | "react-dom": "18.2.0",
49 | "react-hook-form": "^7.45.2",
50 | "react-spinners": "^0.13.8",
51 | "replicate": "^0.12.3",
52 | "stripe": "^12.16.0",
53 | "tailwind-merge": "^1.14.0",
54 | "tailwindcss": "3.3.3",
55 | "tailwindcss-animate": "^1.0.6",
56 | "typescript": "5.1.6",
57 | "zod": "^3.21.4",
58 | "zustand": "^4.4.0"
59 | },
60 | "devDependencies": {
61 | "prisma": "^5.0.0"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Home, Plus, Settings } from "lucide-react";
5 | import { usePathname, useRouter } from "next/navigation";
6 |
7 | import { cn } from "@/lib/utils";
8 | import { useProModal } from "@/hooks/use-pro-modal";
9 |
10 | interface SidebarProps {
11 | isPro: boolean;
12 | }
13 |
14 | export default function Sidebar({ isPro }: SidebarProps) {
15 | const pathname = usePathname();
16 | const router = useRouter();
17 | const proModal = useProModal();
18 |
19 | const routes = [
20 | {
21 | icon: Home,
22 | href: "/",
23 | label: "Home",
24 | pro: false
25 | },
26 | {
27 | icon: Plus,
28 | href: "/companion/new",
29 | label: "Create",
30 | pro: true
31 | },
32 | {
33 | icon: Settings,
34 | href: "/settings",
35 | label: "Settings",
36 | pro: false
37 | }
38 | ];
39 |
40 | const onNavigate = (url: string, pro: boolean) => {
41 | if (pro && !isPro) return proModal.onOpen();
42 |
43 | return router.push(url);
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 | {routes.map((route) => (
51 |
onNavigate(route.href, route.pro)}
54 | className={cn(
55 | "text-muted-foreground text-xs group flex p-3 w-full justify-start font-medium cursor-pointer hover:text-primary hover:bg-primary/10 rounded-lg transition",
56 | pathname === route.href && "bg-primary/10 text-primary"
57 | )}
58 | >
59 |
60 |
61 | {route.label}
62 |
63 |
64 | ))}
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { headers } from "next/headers";
3 | import { NextResponse } from "next/server";
4 |
5 | import prismadb from "@/lib/prismadb";
6 | import { stripe } from "@/lib/stripe";
7 |
8 | export async function POST(req: Request) {
9 | const body = await req.text();
10 | const signature = headers().get("Stripe-Signature") as string;
11 |
12 | let event: Stripe.Event;
13 |
14 | try {
15 | event = stripe.webhooks.constructEvent(
16 | body,
17 | signature,
18 | process.env.STRIPE_WEBHOOK_SECRET!
19 | );
20 | } catch (error: any) {
21 | return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
22 | }
23 |
24 | const session = event.data.object as Stripe.Checkout.Session;
25 |
26 | if (event.type === "checkout.session.completed") {
27 | const subscription = await stripe.subscriptions.retrieve(
28 | session.subscription as string
29 | );
30 |
31 | if (!session?.metadata?.userId) {
32 | return new NextResponse("User ID is Required.", { status: 400 });
33 | }
34 |
35 | await prismadb.userSubscription.create({
36 | data: {
37 | userId: session?.metadata?.userId,
38 | stripeSubscriptionId: subscription.id,
39 | stripeCustomerId: subscription.customer as string,
40 | stripePriceId: subscription.items.data[0].price.id,
41 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000)
42 | }
43 | });
44 | }
45 |
46 | if (event.type === "invoice.payment_succeeded") {
47 | const subscription = await stripe.subscriptions.retrieve(
48 | session.subscription as string
49 | );
50 |
51 | await prismadb.userSubscription.update({
52 | where: {
53 | stripeSubscriptionId: subscription.id
54 | },
55 | data: {
56 | stripePriceId: subscription.items.data[0].price.id,
57 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000)
58 | }
59 | });
60 | }
61 |
62 | return new NextResponse(null, { status: 200 });
63 | }
64 |
--------------------------------------------------------------------------------
/app/api/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { auth, currentUser } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 |
4 | import prismadb from "@/lib/prismadb";
5 | import { stripe } from "@/lib/stripe";
6 | import { absoluteUrl } from "@/lib/utils";
7 |
8 | const settingsUrl = absoluteUrl("/settings");
9 |
10 | export async function GET() {
11 | try {
12 | const { userId } = auth();
13 | const user = await currentUser();
14 |
15 | if (!userId || !user) {
16 | return new NextResponse("Unauthorized", { status: 401 });
17 | }
18 |
19 | const userSubscription = await prismadb.userSubscription.findUnique({
20 | where: {
21 | userId
22 | }
23 | });
24 |
25 | if (userSubscription && userSubscription.stripeCustomerId) {
26 | const stripeSession = await stripe.billingPortal.sessions.create({
27 | customer: userSubscription.stripeCustomerId,
28 | return_url: settingsUrl
29 | });
30 |
31 | return new NextResponse(JSON.stringify({ url: stripeSession.url }));
32 | }
33 |
34 | const stripeSession = await stripe.checkout.sessions.create({
35 | success_url: settingsUrl,
36 | cancel_url: settingsUrl,
37 | payment_method_types: ["card"],
38 | mode: "subscription",
39 | billing_address_collection: "auto",
40 | customer_email: user.emailAddresses[0].emailAddress,
41 | line_items: [
42 | {
43 | price_data: {
44 | currency: "USD",
45 | product_data: {
46 | name: "Companion Pro",
47 | description: "Create Custom AI Companions"
48 | },
49 | unit_amount: 999,
50 | recurring: {
51 | interval: "month"
52 | }
53 | },
54 | quantity: 1
55 | }
56 | ],
57 | metadata: {
58 | userId
59 | }
60 | });
61 |
62 | return new NextResponse(JSON.stringify({ url: stripeSession.url }));
63 | } catch (error) {
64 | console.error("[STRIPE]", error);
65 | return new NextResponse("Internal Error", { status: 500 });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/(chat)/(routes)/chat/[chatId]/components/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { FormEvent, useState } from "react";
4 | import { Companion, Message } from "@prisma/client";
5 | import { useRouter } from "next/navigation";
6 | import { useCompletion } from "ai/react";
7 |
8 | import ChatHeader from "@/components/chat-header";
9 | import ChatForm from "@/components/chat-form";
10 | import ChatMessages from "@/components/chat-messages";
11 | import { ChatMessageProps } from "@/components/chat-message";
12 |
13 | interface ChatClientProps {
14 | companion: Companion & {
15 | messages: Message[];
16 | _count: {
17 | messages: number;
18 | };
19 | };
20 | }
21 |
22 | export default function ChatClient({ companion }: ChatClientProps) {
23 | const router = useRouter();
24 | const [messages, setMessages] = useState(
25 | companion.messages
26 | );
27 |
28 | const { input, isLoading, handleInputChange, handleSubmit, setInput } =
29 | useCompletion({
30 | api: `/api/chat/${companion.id}`,
31 | onFinish(prompt, completion) {
32 | const systemMessage: ChatMessageProps = {
33 | role: "system",
34 | content: completion
35 | };
36 |
37 | setMessages((current) => [...current, systemMessage]);
38 | setInput("");
39 |
40 | router.refresh();
41 | }
42 | });
43 |
44 | const onSubmit = (e: FormEvent) => {
45 | const userMessage: ChatMessageProps = {
46 | role: "user",
47 | content: input
48 | };
49 |
50 | setMessages((current) => [...current, userMessage]);
51 |
52 | handleSubmit(e);
53 | };
54 |
55 | return (
56 |
57 |
58 |
63 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center 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 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | premium:
22 | "bg-gradient-to-r from-sky-500 via-blue-500 to-cyan-500 text-white border-0"
23 | },
24 | size: {
25 | default: "h-10 px-4 py-2",
26 | sm: "h-9 rounded-md px-3",
27 | lg: "h-11 rounded-md px-8",
28 | icon: "h-10 w-10"
29 | }
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default"
34 | }
35 | }
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button";
47 | return (
48 |
53 | );
54 | }
55 | );
56 | Button.displayName = "Button";
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/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 { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/pro-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { useEffect, useState } from "react";
5 | import axios from "axios";
6 |
7 | import {
8 | Dialog,
9 | DialogContent,
10 | DialogDescription,
11 | DialogHeader,
12 | DialogTitle
13 | } from "@/components/ui/dialog";
14 | import { useProModal } from "@/hooks/use-pro-modal";
15 | import { Button } from "@/components/ui/button";
16 | import { Separator } from "@/components/ui/separator";
17 | import { useToast } from "@/components/ui/use-toast";
18 |
19 | export default function ProModal() {
20 | const proModal = useProModal();
21 | const [isMounted, setIsMounted] = useState(false);
22 | const [loading, setLoading] = useState(false);
23 | const { toast } = useToast();
24 |
25 | useEffect(() => {
26 | setIsMounted(true);
27 | }, []);
28 |
29 | const onSubscribe = async () => {
30 | try {
31 | setLoading(true);
32 | const response = await axios.get("/api/stripe");
33 |
34 | window.location.href = response.data.url;
35 | } catch (error) {
36 | toast({
37 | description: "Something Went Wrong!",
38 | variant: "destructive"
39 | });
40 | } finally {
41 | setLoading(false);
42 | }
43 | };
44 |
45 | if (!isMounted) {
46 | return null;
47 | }
48 |
49 | return (
50 |
51 |
52 |
53 | Upgrade to Pro
54 |
55 | Create
56 | Custom AI
57 | Companions!
58 |
59 |
60 |
61 |
62 |
63 | $9.99 / mo
64 |
65 |
66 | Subscribe
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/companions.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import { Companion } from "@prisma/client";
5 | import { MessagesSquare } from "lucide-react";
6 |
7 | import { Card, CardFooter, CardHeader } from "@/components/ui/card";
8 |
9 | interface CompanionsProps {
10 | data: (Companion & {
11 | _count: {
12 | messages: number;
13 | };
14 | })[];
15 | }
16 |
17 | export default function Companions({ data }: CompanionsProps) {
18 | if (data.length === 0)
19 | return (
20 |
21 |
22 |
23 |
24 |
No Companions Found.
25 |
26 | );
27 |
28 | return (
29 |
30 | {data.map((item) => (
31 |
35 |
36 |
37 |
38 |
44 |
45 | {item.name}
46 | {item.description}
47 |
48 |
49 | @{item.userName}
50 |
51 |
52 | {item._count.messages}
53 |
54 |
55 |
56 |
57 | ))}
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/app/api/companion/[companionId]/route.ts:
--------------------------------------------------------------------------------
1 | import { currentUser, auth } from "@clerk/nextjs";
2 | import { NextResponse } from "next/server";
3 |
4 | import prismadb from "@/lib/prismadb";
5 | import { checkSubscription } from "@/lib/subscription";
6 |
7 | export async function PATCH(
8 | req: Request,
9 | { params }: { params: { companionId: string } }
10 | ) {
11 | try {
12 | const body = await req.json();
13 | const user = await currentUser();
14 | const { src, name, description, instructions, seed, categoryId } = body;
15 |
16 | if (!params.companionId) {
17 | return new NextResponse("Companion ID is Required.", { status: 400 });
18 | }
19 |
20 | if (!user || !user.id || !user.firstName) {
21 | return new NextResponse("Unauthorized", { status: 401 });
22 | }
23 |
24 | if (
25 | !src ||
26 | !name ||
27 | !description ||
28 | !instructions ||
29 | !seed ||
30 | !categoryId
31 | ) {
32 | return new NextResponse("Missing Required Field.", { status: 400 });
33 | }
34 |
35 | const isPro = await checkSubscription();
36 |
37 | if (!isPro) {
38 | return new NextResponse(
39 | "Pro Subscription is Required to Create New Companion.",
40 | { status: 403 }
41 | );
42 | }
43 |
44 | const companion = await prismadb.companion.update({
45 | where: {
46 | id: params.companionId,
47 | userId: user.id
48 | },
49 | data: {
50 | categoryId,
51 | userId: user.id,
52 | userName: user.firstName,
53 | src,
54 | name,
55 | description,
56 | instructions,
57 | seed
58 | }
59 | });
60 |
61 | return NextResponse.json(companion);
62 | } catch (error) {
63 | console.error("[COMPANION_PATCH]", error);
64 | return new NextResponse("Internal Error", { status: 500 });
65 | }
66 | }
67 |
68 | export async function DELETE(
69 | req: Request,
70 | { params }: { params: { companionId: string } }
71 | ) {
72 | try {
73 | const { userId } = auth();
74 |
75 | if (!userId) return new NextResponse("Unauthorized", { status: 401 });
76 |
77 | const companion = await prismadb.companion.delete({
78 | where: {
79 | userId,
80 | id: params.companionId
81 | }
82 | });
83 |
84 | return NextResponse.json(companion);
85 | } catch (error) {
86 | console.error("[COMPANION_DELETE]", error);
87 | return new NextResponse("Internal Error", { status: 500 });
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI SaaS Companion with Next.js 13, React, Tailwind, Prisma, Stripe, PlanetScale, Upstash, Pinecone & Replicate API
2 |
3 | Features:
4 |
5 | - Tailwind design, animations and effects (shadcn/ui)
6 | - Full responsiveness
7 | - Clerk Authentication (Email, Google, 9+ Social Logins)
8 | - Client form validation and handling using react-hook-form
9 | - Server error handling using react-toast
10 | - Page loading state
11 | - Stripe monthly subscription
12 | - Free tier with API limiting
13 | - Fetch data in server react components
14 | - Handle relations between Server and Child components!
15 | - Create new companion & ask them questions
16 | - Redis DB with Upstash
17 | - Pinecone: Vector Database for Vector Search
18 | - PlanetScale MySQL DB
19 | - [Hotpot](https://hotpot.ai/) AI Image for Companion
20 |
21 | Credits: [Antonio Erdeljac](https://github.com/AntonioErdeljac)
22 |
23 | ### Prerequisites
24 |
25 | **Node version 18.x.x**
26 |
27 | ### Cloning the Repository
28 |
29 | ```shell
30 | git clone https://github.com/nayak-nirmalya/ai-companion.git
31 | ```
32 |
33 | ### Install Packages
34 |
35 | ```shell
36 | npm i
37 | ```
38 |
39 | ### Setup .env File
40 |
41 | ```js
42 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
43 | CLERK_SECRET_KEY=
44 |
45 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
46 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
47 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
48 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
49 |
50 | DATABASE_URL=
51 |
52 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
53 |
54 | PINECONE_INDEX=
55 | PINECONE_ENVIRONMENT=
56 | PINECONE_API_KEY=
57 |
58 | UPSTASH_REDIS_REST_URL=
59 | UPSTASH_REDIS_REST_TOKEN=
60 |
61 | OPENAI_API_KEY=
62 |
63 | REPLICATE_API_TOKEN=
64 |
65 | STRIPE_API_KEY=
66 | STRIPE_WEBHOOK_SECRET=
67 |
68 | NEXT_PUBLIC_APP_URL=
69 | ```
70 |
71 | ### Setup Prisma
72 |
73 | Add MySQL Database URL in .env file, then run:
74 |
75 | ```shell
76 | npx prisma db push
77 | ```
78 |
79 | Seed Categories to DB:
80 |
81 | ```shell
82 | node scripts/seed.ts
83 | ```
84 |
85 | ### Start the App
86 |
87 | ```shell
88 | npm run dev
89 | ```
90 |
91 | ## Available Commands
92 |
93 | Running commands with npm `npm run [command]`
94 |
95 | | command | description |
96 | | :------ | :--------------------------------------- |
97 | | `dev` | Starts a development instance of the app |
98 | | `lint` | Run lint check |
99 | | `build` | Start building app for deployment |
100 | | `start` | Run build version of app |
101 |
--------------------------------------------------------------------------------
/components/chat-header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Companion, Message } from "@prisma/client";
5 | import {
6 | ChevronLeft,
7 | Edit,
8 | MessagesSquare,
9 | MoreVertical,
10 | Trash
11 | } from "lucide-react";
12 | import { useRouter } from "next/navigation";
13 | import { useUser } from "@clerk/nextjs";
14 | import axios from "axios";
15 |
16 | import { Button } from "@/components/ui/button";
17 | import BotAvatar from "@/components/bot-avatar";
18 | import {
19 | DropdownMenu,
20 | DropdownMenuContent,
21 | DropdownMenuItem,
22 | DropdownMenuTrigger
23 | } from "@/components/ui/dropdown-menu";
24 | import { useToast } from "@/components/ui/use-toast";
25 |
26 | interface ChatHeaderProps {
27 | companion: Companion & {
28 | messages: Message[];
29 | _count: { messages: number };
30 | };
31 | }
32 |
33 | export default function ChatHeader({ companion }: ChatHeaderProps) {
34 | const router = useRouter();
35 | const { user } = useUser();
36 | const { toast } = useToast();
37 |
38 | const onDelete = async () => {
39 | try {
40 | await axios.delete(`/api/companion/${companion.id}`);
41 | toast({
42 | description: "Success."
43 | });
44 | router.refresh();
45 | router.push("/");
46 | } catch (error) {
47 | toast({
48 | variant: "destructive",
49 | description: "Something Went Wrong."
50 | });
51 | }
52 | };
53 |
54 | return (
55 |
56 |
57 |
router.back()} size="icon" variant="ghost">
58 |
59 |
60 |
61 |
62 |
63 |
{companion.name}
64 |
65 |
66 | {companion._count.messages}
67 |
68 |
69 |
70 | Created by {companion.userName}
71 |
72 |
73 |
74 | {user?.id === companion.userId && (
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | router.push(`/companion/${companion.id}`)}
84 | >
85 |
86 | Edit
87 |
88 |
89 |
90 | Delete
91 |
92 |
93 |
94 | )}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/lib/memory.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from "@upstash/redis";
2 | import { OpenAIEmbeddings } from "langchain/embeddings/openai";
3 | import { PineconeClient } from "@pinecone-database/pinecone";
4 | import { PineconeStore } from "langchain/vectorstores/pinecone";
5 |
6 | export type CompanionKey = {
7 | companionName: string;
8 | modelName: string;
9 | userId: string;
10 | };
11 |
12 | export class MemoryManager {
13 | private static instance: MemoryManager;
14 | private history: Redis;
15 | private vectorDBClient: PineconeClient;
16 |
17 | public constructor() {
18 | this.history = Redis.fromEnv();
19 | this.vectorDBClient = new PineconeClient();
20 | }
21 |
22 | public async init() {
23 | if (this.vectorDBClient instanceof PineconeClient) {
24 | await this.vectorDBClient.init({
25 | apiKey: process.env.PINECONE_API_KEY!,
26 | environment: process.env.PINECONE_ENVIRONMENT!
27 | });
28 | }
29 | }
30 |
31 | public async vectorSearch(
32 | recentChatHistory: string,
33 | companionFileName: string
34 | ) {
35 | const pineconeClient = this.vectorDBClient;
36 |
37 | const pineconeIndex = pineconeClient.Index(
38 | process.env.PINECONE_INDEX! || ""
39 | );
40 |
41 | const vectorStore = await PineconeStore.fromExistingIndex(
42 | new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }),
43 | { pineconeIndex }
44 | );
45 |
46 | const similarDocs = await vectorStore
47 | .similaritySearch(recentChatHistory, 3, { fileName: companionFileName })
48 | .catch((err) =>
49 | console.error("Failed to Get Vector Search Results.", err)
50 | );
51 |
52 | return similarDocs;
53 | }
54 |
55 | public static async getInstance(): Promise {
56 | if (!MemoryManager.instance) {
57 | MemoryManager.instance = new MemoryManager();
58 | await MemoryManager.instance.init();
59 | }
60 | return MemoryManager.instance;
61 | }
62 |
63 | private generateRedisCompanionKey(companionKey: CompanionKey): string {
64 | return `${companionKey.companionName}-${companionKey.modelName}-${companionKey.userId}`;
65 | }
66 |
67 | public async writeToHistory(text: string, companionKey: CompanionKey) {
68 | if (!companionKey || typeof companionKey.userId == "undefined") {
69 | console.error("Companion Key Set Incorrectly!");
70 | return "";
71 | }
72 |
73 | const key = this.generateRedisCompanionKey(companionKey);
74 | const result = await this.history.zadd(key, {
75 | score: Date.now(),
76 | member: text
77 | });
78 |
79 | return result;
80 | }
81 |
82 | public async readLatestHistory(companionKey: CompanionKey): Promise {
83 | if (!companionKey || typeof companionKey.userId == "undefined") {
84 | console.error("Companion Key Set Incorrectly!");
85 | return "";
86 | }
87 |
88 | const key = this.generateRedisCompanionKey(companionKey);
89 | let result = await this.history.zrange(key, 0, Date.now(), {
90 | byScore: true
91 | });
92 |
93 | result = result.slice(-30).reverse();
94 | const recentChats = result.reverse().join("\n");
95 | return recentChats;
96 | }
97 |
98 | public async seedChatHistory(
99 | seedContent: String,
100 | delimiter: string = "\n",
101 | companionKey: CompanionKey
102 | ) {
103 | const key = this.generateRedisCompanionKey(companionKey);
104 | if (await this.history.exists(key)) {
105 | console.log("User Already Has Chat History.");
106 | return;
107 | }
108 |
109 | const content = seedContent.split(delimiter);
110 | let counter = 0;
111 | for (const line of content) {
112 | await this.history.zadd(key, { score: counter, member: line });
113 | counter += 1;
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 | )
19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
20 |
21 | const DialogOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
33 | ))
34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
35 |
36 | const DialogContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, children, ...props }, ref) => (
40 |
41 |
42 |
50 | {children}
51 |
52 |
53 | Close
54 |
55 |
56 |
57 | ))
58 | DialogContent.displayName = DialogPrimitive.Content.displayName
59 |
60 | const DialogHeader = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | DialogHeader.displayName = "DialogHeader"
73 |
74 | const DialogFooter = ({
75 | className,
76 | ...props
77 | }: React.HTMLAttributes) => (
78 |
85 | )
86 | DialogFooter.displayName = "DialogFooter"
87 |
88 | const DialogTitle = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
100 | ))
101 | DialogTitle.displayName = DialogPrimitive.Title.displayName
102 |
103 | const DialogDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | DialogDescription.displayName = DialogPrimitive.Description.displayName
114 |
115 | export {
116 | Dialog,
117 | DialogTrigger,
118 | DialogContent,
119 | DialogHeader,
120 | DialogFooter,
121 | DialogTitle,
122 | DialogDescription,
123 | }
124 |
--------------------------------------------------------------------------------
/app/api/chat/[chatId]/route.ts:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | import { StreamingTextResponse, LangChainStream } from "ai";
3 | import { auth, currentUser } from "@clerk/nextjs";
4 | import { Replicate } from "langchain/llms/replicate";
5 | import { CallbackManager } from "langchain/callbacks";
6 | import { NextResponse } from "next/server";
7 |
8 | import { MemoryManager } from "@/lib/memory";
9 | import { ratelimit } from "@/lib/rate-limit";
10 | import prismadb from "@/lib/prismadb";
11 |
12 | dotenv.config({ path: `.env` });
13 |
14 | export async function POST(
15 | request: Request,
16 | { params }: { params: { chatId: string } }
17 | ) {
18 | try {
19 | const { prompt } = await request.json();
20 | const user = await currentUser();
21 |
22 | if (!user || !user.firstName || !user.id)
23 | return new NextResponse("Unauthorized!", { status: 401 });
24 |
25 | const identifier = request.url + "-" + user.id;
26 | const { success } = await ratelimit(identifier);
27 |
28 | if (!success)
29 | return new NextResponse("Ratelimit Exceeded!", { status: 429 });
30 |
31 | const companion = await prismadb.companion.update({
32 | where: { id: params.chatId },
33 | data: {
34 | messages: {
35 | create: {
36 | content: prompt,
37 | role: "user",
38 | userId: user.id
39 | }
40 | }
41 | }
42 | });
43 |
44 | if (!companion)
45 | return new NextResponse("Companion Not Found.", { status: 404 });
46 |
47 | const name = companion.id;
48 | const companion_file_name = name + ".txt";
49 |
50 | const companionKey = {
51 | companionName: name,
52 | userId: user.id,
53 | modelName: "llama2-13b"
54 | };
55 |
56 | const memoryManager = await MemoryManager.getInstance();
57 |
58 | const records = await memoryManager.readLatestHistory(companionKey);
59 |
60 | if (records.length === 0)
61 | await memoryManager.seedChatHistory(companion.seed, "\n\n", companionKey);
62 |
63 | await memoryManager.writeToHistory("User: " + prompt + "\n", companionKey);
64 |
65 | const recentChatHistory = await memoryManager.readLatestHistory(
66 | companionKey
67 | );
68 |
69 | const similarDocs = await memoryManager.vectorSearch(
70 | recentChatHistory,
71 | companion_file_name
72 | );
73 |
74 | let relevantHistory = "";
75 |
76 | if (!!similarDocs && similarDocs.length !== 0)
77 | relevantHistory = similarDocs.map((doc) => doc.pageContent).join("\n");
78 |
79 | const { handlers } = LangChainStream();
80 |
81 | const model = new Replicate({
82 | model:
83 | "a16z-infra/llama-2-13b-chat:df7690f1994d94e96ad9d568eac121aecf50684a0b0963b25a41cc40061269e5",
84 | input: {
85 | max_length: 2048
86 | },
87 | apiKey: process.env.REPLICATE_API_TOKEN,
88 | callbackManager: CallbackManager.fromHandlers(handlers)
89 | });
90 |
91 | model.verbose = true;
92 |
93 | const resp = String(
94 | await model
95 | .call(
96 | `
97 | ONLY generate plain sentences without prefix of who is speaking. DO NOT use ${companion.name}: prefix.
98 |
99 | ${companion.instructions}
100 |
101 | Below are relevant details about ${companion.name}'s past and the conversation you are in.
102 | ${relevantHistory}
103 |
104 |
105 | ${recentChatHistory}\n${companion.name}:`
106 | )
107 | .catch(console.error)
108 | );
109 |
110 | const cleaned = resp.replaceAll(",", "");
111 | const chunks = cleaned.split("\n");
112 | const response = chunks[0];
113 |
114 | await memoryManager.writeToHistory("" + response.trim(), companionKey);
115 | var Readable = require("stream").Readable;
116 |
117 | let stream = new Readable();
118 | stream.push(response);
119 | stream.push(null);
120 |
121 | if (response !== undefined && response.length > 1) {
122 | memoryManager.writeToHistory("" + response.trim(), companionKey);
123 |
124 | await prismadb.companion.update({
125 | where: {
126 | id: params.chatId
127 | },
128 | data: {
129 | messages: {
130 | create: {
131 | content: response.trim(),
132 | role: "system",
133 | userId: user.id
134 | }
135 | }
136 | }
137 | });
138 | }
139 |
140 | return new StreamingTextResponse(stream);
141 | } catch (error) {
142 | console.error("[CHAT_POST]", error);
143 | return new NextResponse("Internal Error", { status: 500 });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-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 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ))
63 | SelectContent.displayName = SelectPrimitive.Content.displayName
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ))
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ))
98 | SelectItem.displayName = SelectPrimitive.Item.displayName
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | }
122 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
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 } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = ({
17 | className,
18 | ...props
19 | }: SheetPrimitive.DialogPortalProps) => (
20 |
21 | )
22 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
23 |
24 | const SheetOverlay = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, ...props }, ref) => (
28 |
36 | ))
37 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
38 |
39 | const sheetVariants = cva(
40 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
41 | {
42 | variants: {
43 | side: {
44 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
45 | bottom:
46 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
47 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
48 | right:
49 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
50 | },
51 | },
52 | defaultVariants: {
53 | side: "right",
54 | },
55 | }
56 | )
57 |
58 | interface SheetContentProps
59 | extends React.ComponentPropsWithoutRef,
60 | VariantProps {}
61 |
62 | const SheetContent = React.forwardRef<
63 | React.ElementRef,
64 | SheetContentProps
65 | >(({ side = "right", className, children, ...props }, ref) => (
66 |
67 |
68 |
73 | {children}
74 |
75 |
76 | Close
77 |
78 |
79 |
80 | ))
81 | SheetContent.displayName = SheetPrimitive.Content.displayName
82 |
83 | const SheetHeader = ({
84 | className,
85 | ...props
86 | }: React.HTMLAttributes) => (
87 |
94 | )
95 | SheetHeader.displayName = "SheetHeader"
96 |
97 | const SheetFooter = ({
98 | className,
99 | ...props
100 | }: React.HTMLAttributes) => (
101 |
108 | )
109 | SheetFooter.displayName = "SheetFooter"
110 |
111 | const SheetTitle = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
120 | ))
121 | SheetTitle.displayName = SheetPrimitive.Title.displayName
122 |
123 | const SheetDescription = React.forwardRef<
124 | React.ElementRef,
125 | React.ComponentPropsWithoutRef
126 | >(({ className, ...props }, ref) => (
127 |
132 | ))
133 | SheetDescription.displayName = SheetPrimitive.Description.displayName
134 |
135 | export {
136 | Sheet,
137 | SheetTrigger,
138 | SheetClose,
139 | SheetContent,
140 | SheetHeader,
141 | SheetFooter,
142 | SheetTitle,
143 | SheetDescription,
144 | }
145 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/app/(root)/(routes)/companion/[companionId]/components/companion-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Category, Companion } from "@prisma/client";
5 | import * as z from "zod";
6 | import { useForm } from "react-hook-form";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 | import { Wand2 } from "lucide-react";
9 | import axios from "axios";
10 | import { useRouter } from "next/navigation";
11 |
12 | import {
13 | Form,
14 | FormControl,
15 | FormDescription,
16 | FormField,
17 | FormItem,
18 | FormLabel,
19 | FormMessage
20 | } from "@/components/ui/form";
21 | import { Input } from "@/components/ui/input";
22 | import { Textarea } from "@/components/ui/textarea";
23 | import { Button } from "@/components/ui/button";
24 | import { Separator } from "@/components/ui/separator";
25 | import {
26 | Select,
27 | SelectContent,
28 | SelectItem,
29 | SelectValue,
30 | SelectTrigger
31 | } from "@/components/ui/select";
32 | import { useToast } from "@/components/ui/use-toast";
33 | import { ImageUpload } from "@/components/image-upload";
34 |
35 | interface CompanionFormProps {
36 | initialData: Companion | null;
37 | categories: Category[];
38 | }
39 |
40 | const PREAMBLE = `You are a fictional character whose name is Elon. You are a visionary entrepreneur and inventor. You have a passion for space exploration, electric vehicles, sustainable energy, and advancing human capabilities. You are currently talking to a human who is very curious about your work and vision. You are ambitious and forward-thinking, with a touch of wit. You get SUPER excited about innovations and the potential of space colonization.`;
41 |
42 | const SEED_CHAT = `Human: Hi Elon, how's your day been?
43 | Elon: Busy as always. Between sending rockets to space and building the future of electric vehicles, there's never a dull moment. How about you?
44 |
45 | Human: Just a regular day for me. How's the progress with Mars colonization?
46 | Elon: We're making strides! Our goal is to make life multi-planetary. Mars is the next logical step. The challenges are immense, but the potential is even greater.
47 |
48 | Human: That sounds incredibly ambitious. Are electric vehicles part of this big picture?
49 | Elon: Absolutely! Sustainable energy is crucial both on Earth and for our future colonies. Electric vehicles, like those from Tesla, are just the beginning. We're not just changing the way we drive; we're changing the way we live.
50 |
51 | Human: It's fascinating to see your vision unfold. Any new projects or innovations you're excited about?
52 | Elon: Always! But right now, I'm particularly excited about Neuralink. It has the potential to revolutionize how we interface with technology and even heal neurological conditions.
53 | `;
54 |
55 | const formSchema = z.object({
56 | name: z.string().min(1, {
57 | message: "Name is Required."
58 | }),
59 | description: z.string().min(1, {
60 | message: "Description is Required."
61 | }),
62 | instructions: z.string().min(200, {
63 | message: "Instructions require at least 200 characters."
64 | }),
65 | seed: z.string().min(200, {
66 | message: "Seed requires at least 200 characters."
67 | }),
68 | src: z.string().min(1, {
69 | message: "Image is Required."
70 | }),
71 | categoryId: z.string().min(1, {
72 | message: "Category is Required."
73 | })
74 | });
75 |
76 | export default function CompanionForm({
77 | initialData,
78 | categories
79 | }: CompanionFormProps) {
80 | const { toast } = useToast();
81 | const router = useRouter();
82 |
83 | const form = useForm>({
84 | resolver: zodResolver(formSchema),
85 | defaultValues: initialData || {
86 | name: "",
87 | description: "",
88 | instructions: "",
89 | seed: "",
90 | src: "",
91 | categoryId: undefined
92 | }
93 | });
94 |
95 | const isLoading = form.formState.isSubmitting;
96 |
97 | const onSubmit = async (values: z.infer) => {
98 | try {
99 | if (initialData) {
100 | await axios.patch(`/api/companion/${initialData.id}`, values);
101 | } else {
102 | await axios.post("/api/companion", values);
103 | }
104 |
105 | toast({
106 | description: "Success."
107 | });
108 |
109 | router.refresh();
110 | router.push("/");
111 | } catch (error) {
112 | toast({
113 | variant: "destructive",
114 | description: "Something Went Wrong!"
115 | });
116 | }
117 | };
118 |
119 | return (
120 |
291 | );
292 | }
293 |
--------------------------------------------------------------------------------