├── .env-example
├── .eslintrc.json
├── .gitignore
├── CodeSnap
└── code.png
├── README.md
├── components.json
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma
└── schema.prisma
├── src
├── app
│ ├── ThemeProvider.tsx
│ ├── api
│ │ ├── chat
│ │ │ └── route.ts
│ │ └── notes
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── notes
│ │ ├── NavBar.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── page.tsx
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.tsx
├── assets
│ ├── logo.png
│ ├── logo1.png
│ └── logo2.png
├── components
│ ├── AIChatBox.tsx
│ ├── AIChatButton.tsx
│ ├── AddEditNoteDialog.tsx
│ ├── Note.tsx
│ ├── ThemeToggleButton.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── loading-button.tsx
│ │ └── textarea.tsx
├── lib
│ ├── db
│ │ ├── pinecone.ts
│ │ └── prisma.ts
│ ├── openai.ts
│ ├── utils.ts
│ └── validation
│ │ └── note.ts
└── middleware.ts
├── tailwind.config.js
└── tsconfig.json
/.env-example:
--------------------------------------------------------------------------------
1 | # This was inserted by `prisma init`:
2 | # Environment variables declared in this file are automatically made available to Prisma.
3 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
4 |
5 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
6 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
7 |
8 | DATABASE_URL= "your dabase url" --> change password and add database name
9 |
10 |
11 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= your clerk publishable key
12 | CLERK_SECRET_KEY= your clerk secret key
13 |
14 |
15 | NEXT_PUBLIC_CLERK_SIGN_IN_URL =/sign-in
16 |
17 | NEXT_PUBLIC_CLERK_SIGN_OUT_URL =/sign-out
18 |
19 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL =/notes
20 |
21 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL =/notes
22 |
23 | OPENAI_API_KEY= your openai api key
24 |
25 | PINECONE_API_KEY= your pincode api key
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .env
39 |
40 |
--------------------------------------------------------------------------------
/CodeSnap/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhishekHegde2000/ai-note-app/6e91e67cec46acd6d51a774c5483c431433a2fd2/CodeSnap/code.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # AI Note-Taking App
3 |
4 | ## Introduction
5 |
6 | Introducing a revolutionary AI-powered note-taking app that elevates note-taking to a whole new level. This innovative app utilizes the power of artificial intelligence to analyze your notes and provide comprehensive answers to your queries. Say goodbye to the days of manually searching through piles of notes – our AI assistant will do the work for you, saving you time and effort.
7 |
8 | ## Project Demo
9 |
10 | https://github.com/abhishekHegde2000/ai-note-app/assets/88794340/fa709d86-efca-421b-9251-421db9f810b0
11 |
12 | ## Features
13 |
14 | * **AI-Powered Chatbot:** Engage in real-time conversations with our AI chatbot, ask questions about your notes, and receive insightful answers.
15 |
16 | * **Intelligent Document Retrieval:** Seamlessly retrieve relevant documents and notes using ChatGPT vector embeddings and Pinecone.
17 |
18 | * **Personalized User Experience:** Switch between light and dark themes for optimal viewing comfort with our next-themes integration.
19 |
20 | * **Secure User Authentication:** Protect your notes with Clerk's robust authentication system, ensuring only authorized users can access your information.
21 |
22 | * **CRUD Operations for Notes:** Create, update, and delete notes effortlessly using Prisma and MongoDB Atlas, maintaining a well-organized note-taking system.
23 |
24 | * **Organized Layouts:** Navigate through structured and intuitive layouts, thanks to our nested layouts implementation.
25 |
26 | * **Comprehensive Form Validation:** Validate form inputs both client-side and server-side with Zod, React-Hook-Form, and Shadcn UI Form, ensuring data integrity.
27 |
28 | * **Fully Responsive Design:** Experience a seamless user experience across all devices with our TailwindCSS modifiers, ensuring the app adapts perfectly to any screen size.
29 |
30 | ## Technology Stack
31 |
32 | * Next.js 14: A modern React framework for building server-side rendered applications.
33 |
34 | * ChatGPT API: Leverage the power of ChatGPT to generate human-quality text, translate languages, write different kinds of creative content, and answer your questions in an informative way.
35 |
36 | * Vector Embeddings: Represent text documents as vectors in a high-dimensional space, enabling efficient similarity searches and document retrieval.
37 |
38 | * Pinecone: A scalable vector similarity search engine for rapid and accurate document retrieval.
39 |
40 | * TailwindCSS: A utility-first CSS framework for building responsive and customizable user interfaces.
41 |
42 | * Shadcn UI: A collection of React components for building beautiful and accessible user interfaces.
43 |
44 | * TypeScript: A superset of JavaScript that adds type annotations, ensuring code reliability and maintainability.
45 |
46 | * Vercel AI SDK: A suite of tools for integrating AI capabilities into your Next.js applications.
47 |
48 | * API Route Handlers: Create and handle API routes to communicate with your application's backend.
49 |
50 | * Clerk: A user authentication and authorization platform for securing your application.
51 |
52 | * Prisma: A powerful data modeling and manipulation tool for interacting with your database.
53 |
54 | * MongoDB Atlas: A cloud-based NoSQL database for storing your notes and application data.
55 |
56 | ## Installation and Usage
57 |
58 | 1. Clone the project repository from GitHub:
59 |
60 | ```bash
61 | git clone https://github.com/abhishekHegde2000/ai-note-app.git
62 | ```
63 |
64 | 2. Install the project dependencies:
65 |
66 | ```bash
67 | cd ai-note-taking-app
68 | npm install
69 | ```
70 |
71 | 3. Set up environment variables for your database connection and ChatGPT API key.
72 |
73 | 4. Start the development server:
74 |
75 | ```bash
76 | npm run dev
77 | ```
78 |
79 | 5. Open the application in your web browser:
80 |
81 | ```bash
82 | http://localhost:3000
83 | ```
84 |
85 | ## Contribution
86 |
87 | I welcome contributions to this project. Please feel free to submit issues, pull requests, or suggestions for improvement.
88 |
--------------------------------------------------------------------------------
/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": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | hostname: "img.clerk.com",
7 | },
8 | ],
9 | },
10 | };
11 |
12 | module.exports = nextConfig;
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-ai-note-app",
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.26.1",
14 | "@clerk/themes": "^1.7.9",
15 | "@hookform/resolvers": "^3.3.2",
16 | "@pinecone-database/pinecone": "^1.1.2",
17 | "@prisma/client": "^5.5.2",
18 | "@radix-ui/react-dialog": "^1.0.5",
19 | "@radix-ui/react-label": "^2.0.2",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "ai": "^2.2.20",
22 | "class-variance-authority": "^0.7.0",
23 | "clsx": "^2.0.0",
24 | "eslint-config-prettier": "^9.0.0",
25 | "lucide-react": "^0.290.0",
26 | "mongodb": "^6.3.0",
27 | "next": "14.0.0",
28 | "next-themes": "^0.2.1",
29 | "openai": "^4.14.1",
30 | "prettier": "^3.0.3",
31 | "prettier-plugin-tailwindcss": "^0.5.6",
32 | "react": "^18",
33 | "react-dom": "^18",
34 | "react-hook-form": "^7.47.0",
35 | "tailwind-merge": "^1.14.0",
36 | "tailwindcss-animate": "^1.0.7",
37 | "zod": "^3.22.4"
38 | },
39 | "devDependencies": {
40 | "@types/node": "^20",
41 | "@types/react": "^18",
42 | "@types/react-dom": "^18",
43 | "autoprefixer": "^10",
44 | "eslint": "^8",
45 | "eslint-config-next": "14.0.0",
46 | "postcss": "^8",
47 | "tailwindcss": "^3",
48 | "typescript": "^5"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ["prettier-plugin-tailwindcss"],
3 | };
4 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mongodb"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model Note {
14 | id String @id @default(auto()) @map("_id") @db.ObjectId
15 | title String
16 | content String?
17 | userId String
18 | createdAt DateTime @default(now())
19 | updatedAt DateTime @updatedAt
20 |
21 | @@map("notes")
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export { ThemeProvider } from "next-themes";
4 |
--------------------------------------------------------------------------------
/src/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { notesIndex } from "@/lib/db/pinecone";
2 | import prisma from "@/lib/db/prisma";
3 | import openai, { getEmbedding } from "@/lib/openai";
4 | import { auth } from "@clerk/nextjs";
5 | import { OpenAIStream, StreamingTextResponse } from "ai";
6 | import { ChatCompletionMessage } from "openai/resources/index.mjs";
7 |
8 | export async function POST(req: Request) {
9 | try {
10 | const body = await req.json();
11 | const messages: ChatCompletionMessage[] = body.messages;
12 |
13 | const messagesTruncated = messages.slice(-6);
14 |
15 | const embedding = await getEmbedding(
16 | messagesTruncated.map((message) => message.content).join("\n"),
17 | );
18 |
19 | const { userId } = auth();
20 |
21 | const vectorQueryResponse = await notesIndex.query({
22 | vector: embedding,
23 | topK: 4,
24 | filter: { userId },
25 | });
26 |
27 | const relevantNotes = await prisma.note.findMany({
28 | where: {
29 | id: {
30 | in: vectorQueryResponse.matches.map((match) => match.id),
31 | },
32 | },
33 | });
34 |
35 | console.log("Relevant notes found: ", relevantNotes);
36 |
37 | const systemMessage: ChatCompletionMessage = {
38 | role: "system",
39 | content:
40 | "You are an intelligent note-taking app. You answer the user's question based on their existing notes. " +
41 | "The relevant notes for this query are:\n" +
42 | relevantNotes
43 | .map((note) => `Title: ${note.title}\n\nContent:\n${note.content}`)
44 | .join("\n\n"),
45 | };
46 |
47 | const response = await openai.chat.completions.create({
48 | model: "gpt-3.5-turbo",
49 | stream: true,
50 | messages: [systemMessage, ...messagesTruncated],
51 | });
52 |
53 | const stream = OpenAIStream(response);
54 | return new StreamingTextResponse(stream);
55 | } catch (error) {
56 | console.error(error);
57 | return Response.json({ error: "Internal server error" }, { status: 500 });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/api/notes/route.ts:
--------------------------------------------------------------------------------
1 | import { notesIndex } from "@/lib/db/pinecone";
2 | import prisma from "@/lib/db/prisma";
3 | import { getEmbedding } from "@/lib/openai";
4 | import {
5 | createNoteSchema,
6 | deleteNoteSchema,
7 | updateNoteSchema,
8 | } from "@/lib/validation/note";
9 | import { auth } from "@clerk/nextjs";
10 |
11 | export async function POST(req: Request) {
12 | try {
13 | const body = await req.json();
14 |
15 | const parseResult = createNoteSchema.safeParse(body);
16 |
17 | if (!parseResult.success) {
18 | console.error(parseResult.error);
19 | return Response.json({ error: "Invalid input" }, { status: 400 });
20 | }
21 |
22 | const { title, content } = parseResult.data;
23 |
24 | const { userId } = auth();
25 |
26 | if (!userId) {
27 | return Response.json({ error: "Unauthorized" }, { status: 401 });
28 | }
29 |
30 | const embedding = await getEmbeddingForNote(title, content);
31 |
32 | const note = await prisma.$transaction(async (tx) => {
33 | const note = await tx.note.create({
34 | data: {
35 | title,
36 | content,
37 | userId,
38 | },
39 | });
40 |
41 | await notesIndex.upsert([
42 | {
43 | id: note.id,
44 | values: embedding,
45 | metadata: { userId },
46 | },
47 | ]);
48 |
49 | return note;
50 | });
51 |
52 | return Response.json({ note }, { status: 201 });
53 | } catch (error) {
54 | console.error(error);
55 | return Response.json({ error: "Internal server error" }, { status: 500 });
56 | }
57 | }
58 |
59 | export async function PUT(req: Request) {
60 | try {
61 | const body = await req.json();
62 |
63 | const parseResult = updateNoteSchema.safeParse(body);
64 |
65 | if (!parseResult.success) {
66 | console.error(parseResult.error);
67 | return Response.json({ error: "Invalid input" }, { status: 400 });
68 | }
69 |
70 | const { id, title, content } = parseResult.data;
71 |
72 | const note = await prisma.note.findUnique({ where: { id } });
73 |
74 | if (!note) {
75 | return Response.json({ error: "Note not found" }, { status: 404 });
76 | }
77 |
78 | const { userId } = auth();
79 |
80 | if (!userId || userId !== note.userId) {
81 | return Response.json({ error: "Unauthorized" }, { status: 401 });
82 | }
83 |
84 | const embedding = await getEmbeddingForNote(title, content);
85 |
86 | const updatedNote = await prisma.$transaction(async (tx) => {
87 | const updatedNote = await tx.note.update({
88 | where: { id },
89 | data: {
90 | title,
91 | content,
92 | },
93 | });
94 |
95 | await notesIndex.upsert([
96 | {
97 | id,
98 | values: embedding,
99 | metadata: { userId },
100 | },
101 | ]);
102 |
103 | return updatedNote;
104 | });
105 |
106 | return Response.json({ updatedNote }, { status: 200 });
107 | } catch (error) {
108 | console.error(error);
109 | return Response.json({ error: "Internal server error" }, { status: 500 });
110 | }
111 | }
112 |
113 | export async function DELETE(req: Request) {
114 | try {
115 | const body = await req.json();
116 |
117 | const parseResult = deleteNoteSchema.safeParse(body);
118 |
119 | if (!parseResult.success) {
120 | console.error(parseResult.error);
121 | return Response.json({ error: "Invalid input" }, { status: 400 });
122 | }
123 |
124 | const { id } = parseResult.data;
125 |
126 | const note = await prisma.note.findUnique({ where: { id } });
127 |
128 | if (!note) {
129 | return Response.json({ error: "Note not found" }, { status: 404 });
130 | }
131 |
132 | const { userId } = auth();
133 |
134 | if (!userId || userId !== note.userId) {
135 | return Response.json({ error: "Unauthorized" }, { status: 401 });
136 | }
137 |
138 | await prisma.$transaction(async (tx) => {
139 | await tx.note.delete({ where: { id } });
140 | await notesIndex.deleteOne(id);
141 | });
142 |
143 | return Response.json({ message: "Note deleted" }, { status: 200 });
144 | } catch (error) {
145 | console.error(error);
146 | return Response.json({ error: "Internal server error" }, { status: 500 });
147 | }
148 | }
149 |
150 | async function getEmbeddingForNote(title: string, content: string | undefined) {
151 | return getEmbedding(title + "\n\n" + content ?? "");
152 | }
153 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhishekHegde2000/ai-note-app/6e91e67cec46acd6d51a774c5483c431433a2fd2/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkProvider } from "@clerk/nextjs";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import { ThemeProvider } from "./ThemeProvider";
5 | import "./globals.css";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata: Metadata = {
10 | title: "IntelliNote",
11 | description: "The intelligent note-taking app",
12 | };
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) {
19 | return (
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/notes/NavBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import logo from "@/assets/logo.png";
4 | import AIChatButton from "@/components/AIChatButton";
5 | import AddEditNoteDialog from "@/components/AddEditNoteDialog";
6 | import ThemeToggleButton from "@/components/ThemeToggleButton";
7 | import { Button } from "@/components/ui/button";
8 | import { UserButton } from "@clerk/nextjs";
9 | import { dark } from "@clerk/themes";
10 | import { Plus } from "lucide-react";
11 | import { useTheme } from "next-themes";
12 | import Image from "next/image";
13 | import Link from "next/link";
14 | import { useState } from "react";
15 |
16 | export default function NavBar() {
17 | const { theme } = useTheme();
18 |
19 | const [showAddEditNoteDialog, setShowAddEditNoteDialog] = useState(false);
20 |
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 |
IntelliNote
28 |
29 |
30 |
37 |
38 |
42 |
43 |
44 |
45 |
46 |
50 | >
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/notes/layout.tsx:
--------------------------------------------------------------------------------
1 | import NavBar from "./NavBar";
2 |
3 | export default function Layout({ children }: { children: React.ReactNode }) {
4 | return (
5 | <>
6 |
7 | {children}
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/notes/page.tsx:
--------------------------------------------------------------------------------
1 | import Note from "@/components/Note";
2 | import prisma from "@/lib/db/prisma";
3 | import { auth } from "@clerk/nextjs";
4 | import { Metadata } from "next";
5 |
6 | export const metadata: Metadata = {
7 | title: "IntelliNote - Notes",
8 | };
9 |
10 | export default async function NotesPage() {
11 | const { userId } = auth();
12 |
13 | if (!userId) throw Error("userId undefined");
14 |
15 | const allNotes = await prisma.note.findMany({ where: { userId } });
16 |
17 | return (
18 |
19 | {allNotes.map((note) => (
20 |
21 | ))}
22 | {allNotes.length === 0 && (
23 |
24 | {"You don't have any notes yet. Why don't you create one?"}
25 |
26 | )}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import logo from "@/assets/logo.png";
2 | import { Button } from "@/components/ui/button";
3 | import { auth } from "@clerk/nextjs";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 | import { redirect } from "next/navigation";
7 |
8 | export default function Home() {
9 | const { userId } = auth();
10 |
11 | if (userId) redirect("/notes");
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | IntelliNote
19 |
20 |
21 |
22 | An intelligent note-taking app with AI integration, built with OpenAI,
23 | Pinecone, Next.js, Shadcn UI, Clerk, and more.
24 |
25 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 | import { Metadata } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: "IntelliNote - Sign In",
6 | };
7 |
8 | export default function SignInPage() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 | import { Metadata } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: "IntelliNote - Sign Up",
6 | };
7 |
8 | export default function SignUpPage() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhishekHegde2000/ai-note-app/6e91e67cec46acd6d51a774c5483c431433a2fd2/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/logo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhishekHegde2000/ai-note-app/6e91e67cec46acd6d51a774c5483c431433a2fd2/src/assets/logo1.png
--------------------------------------------------------------------------------
/src/assets/logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhishekHegde2000/ai-note-app/6e91e67cec46acd6d51a774c5483c431433a2fd2/src/assets/logo2.png
--------------------------------------------------------------------------------
/src/components/AIChatBox.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { useUser } from "@clerk/nextjs";
3 | import { Message } from "ai";
4 | import { useChat } from "ai/react";
5 | import { Bot, Trash, XCircle } from "lucide-react";
6 | import Image from "next/image";
7 | import { useEffect, useRef } from "react";
8 | import { Button } from "./ui/button";
9 | import { Input } from "./ui/input";
10 |
11 | interface AIChatBoxProps {
12 | open: boolean;
13 | onClose: () => void;
14 | }
15 |
16 | export default function AIChatBox({ open, onClose }: AIChatBoxProps) {
17 | const {
18 | messages,
19 | input,
20 | handleInputChange,
21 | handleSubmit,
22 | setMessages,
23 | isLoading,
24 | error,
25 | } = useChat();
26 |
27 | const inputRef = useRef(null);
28 | const scrollRef = useRef(null);
29 |
30 | useEffect(() => {
31 | if (scrollRef.current) {
32 | scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
33 | }
34 | }, [messages]);
35 |
36 | useEffect(() => {
37 | if (open) {
38 | inputRef.current?.focus();
39 | }
40 | }, [open]);
41 |
42 | const lastMessageIsUser = messages[messages.length - 1]?.role === "user";
43 |
44 | return (
45 |
51 |
54 |
55 |
56 | {messages.map((message) => (
57 |
58 | ))}
59 | {isLoading && lastMessageIsUser && (
60 |
66 | )}
67 | {error && (
68 |
74 | )}
75 | {!error && messages.length === 0 && (
76 |
77 |
78 | Ask the AI a question about your notes
79 |
80 | )}
81 |
82 |
101 |
102 |
103 | );
104 | }
105 |
106 | function ChatMessage({
107 | message: { role, content },
108 | }: {
109 | message: Pick;
110 | }) {
111 | const { user } = useUser();
112 |
113 | const isAiMessage = role === "assistant";
114 |
115 | return (
116 |
122 | {isAiMessage &&
}
123 |
129 | {content}
130 |
131 | {!isAiMessage && user?.imageUrl && (
132 |
139 | )}
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/src/components/AIChatButton.tsx:
--------------------------------------------------------------------------------
1 | import { Bot } from "lucide-react";
2 | import { useState } from "react";
3 | import AIChatBox from "./AIChatBox";
4 | import { Button } from "./ui/button";
5 |
6 | export default function AIChatButton() {
7 | const [chatBoxOpen, setChatBoxOpen] = useState(false);
8 |
9 | return (
10 | <>
11 |
15 | setChatBoxOpen(false)} />
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/AddEditNoteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { CreateNoteSchema, createNoteSchema } from "@/lib/validation/note";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import { Note } from "@prisma/client";
4 | import { useRouter } from "next/navigation";
5 | import { useState } from "react";
6 | import { useForm } from "react-hook-form";
7 | import {
8 | Dialog,
9 | DialogContent,
10 | DialogFooter,
11 | DialogHeader,
12 | DialogTitle,
13 | } from "./ui/dialog";
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "./ui/form";
22 | import { Input } from "./ui/input";
23 | import LoadingButton from "./ui/loading-button";
24 | import { Textarea } from "./ui/textarea";
25 |
26 | interface AddEditNoteDialogProps {
27 | open: boolean;
28 | setOpen: (open: boolean) => void;
29 | noteToEdit?: Note;
30 | }
31 |
32 | export default function AddEditNoteDialog({
33 | open,
34 | setOpen,
35 | noteToEdit,
36 | }: AddEditNoteDialogProps) {
37 | const [deleteInProgress, setDeleteInProgress] = useState(false);
38 |
39 | const router = useRouter();
40 |
41 | const form = useForm({
42 | resolver: zodResolver(createNoteSchema),
43 | defaultValues: {
44 | title: noteToEdit?.title || "",
45 | content: noteToEdit?.content || "",
46 | },
47 | });
48 |
49 | async function onSubmit(input: CreateNoteSchema) {
50 | try {
51 | if (noteToEdit) {
52 | const response = await fetch("/api/notes", {
53 | method: "PUT",
54 | body: JSON.stringify({
55 | id: noteToEdit.id,
56 | ...input,
57 | }),
58 | });
59 | if (!response.ok) throw Error("Status code: " + response.status);
60 | } else {
61 | const response = await fetch("/api/notes", {
62 | method: "POST",
63 | body: JSON.stringify(input),
64 | });
65 | if (!response.ok) throw Error("Status code: " + response.status);
66 | form.reset();
67 | }
68 | router.refresh();
69 | setOpen(false);
70 | } catch (error) {
71 | console.error(error);
72 | alert("Something went wrong. Please try again.");
73 | }
74 | }
75 |
76 | async function deleteNote() {
77 | if (!noteToEdit) return;
78 | setDeleteInProgress(true);
79 | try {
80 | const response = await fetch("/api/notes", {
81 | method: "DELETE",
82 | body: JSON.stringify({
83 | id: noteToEdit.id,
84 | }),
85 | });
86 | if (!response.ok) throw Error("Status code: " + response.status);
87 | router.refresh();
88 | setOpen(false);
89 | } catch (error) {
90 | console.error(error);
91 | alert("Something went wrong. Please try again.");
92 | } finally {
93 | setDeleteInProgress(false);
94 | }
95 | }
96 |
97 | return (
98 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/src/components/Note.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Note as NoteModel } from "@prisma/client";
4 | import { useState } from "react";
5 | import AddEditNoteDialog from "./AddEditNoteDialog";
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardHeader,
11 | CardTitle,
12 | } from "./ui/card";
13 |
14 | interface NoteProps {
15 | note: NoteModel;
16 | }
17 |
18 | export default function Note({ note }: NoteProps) {
19 | const [showEditDialog, setShowEditDialog] = useState(false);
20 |
21 | const wasUpdated = note.updatedAt > note.createdAt;
22 |
23 | const createdUpdatedAtTimestamp = (
24 | wasUpdated ? note.updatedAt : note.createdAt
25 | ).toDateString();
26 |
27 | return (
28 | <>
29 | setShowEditDialog(true)}
32 | >
33 |
34 | {note.title}
35 |
36 | {createdUpdatedAtTimestamp}
37 | {wasUpdated && " (updated)"}
38 |
39 |
40 |
41 | {note.content}
42 |
43 |
44 |
49 | >
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/ThemeToggleButton.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react";
2 | import { useTheme } from "next-themes";
3 | import { Button } from "./ui/button";
4 |
5 | export default function ThemeToggleButton() {
6 | const { theme, setTheme } = useTheme();
7 |
8 | return (
9 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | 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 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/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 = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/loading-button.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 | import { Button, ButtonProps } from "./button";
3 |
4 | type LoadingButtonProps = {
5 | loading: boolean;
6 | } & ButtonProps;
7 |
8 | export default function LoadingButton({
9 | children,
10 | loading,
11 | ...props
12 | }: LoadingButtonProps) {
13 | return (
14 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/lib/db/pinecone.ts:
--------------------------------------------------------------------------------
1 | import { Pinecone } from "@pinecone-database/pinecone";
2 |
3 | const apiKey = process.env.PINECONE_API_KEY;
4 |
5 | if (!apiKey) {
6 | throw Error("PINECONE_API_KEY is not set");
7 | }
8 |
9 | const pinecone = new Pinecone({
10 | environment: "gcp-starter",
11 | apiKey,
12 | });
13 |
14 | export const notesIndex = pinecone.Index("nextjs-ai-note-app");
15 |
--------------------------------------------------------------------------------
/src/lib/db/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | type PrismaClientSingleton = ReturnType;
8 |
9 | const globalForPrisma = globalThis as unknown as {
10 | prisma: PrismaClientSingleton | undefined;
11 | };
12 |
13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
14 |
15 | export default prisma;
16 |
17 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
18 |
--------------------------------------------------------------------------------
/src/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 |
3 | const apiKey = process.env.OPENAI_API_KEY;
4 |
5 | if (!apiKey) {
6 | throw Error("OPENAI_API_KEY is not set");
7 | }
8 |
9 | const openai = new OpenAI({ apiKey });
10 |
11 | export default openai;
12 |
13 | export async function getEmbedding(text: string) {
14 | const response = await openai.embeddings.create({
15 | model: "text-embedding-ada-002",
16 | input: text,
17 | });
18 |
19 | const embedding = response.data[0].embedding;
20 |
21 | if (!embedding) throw Error("Error generating embedding.");
22 |
23 | console.log(embedding);
24 |
25 | return embedding;
26 | }
27 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/lib/validation/note.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createNoteSchema = z.object({
4 | title: z.string().min(1, { message: "Title is required" }),
5 | content: z.string().optional(),
6 | });
7 |
8 | export type CreateNoteSchema = z.infer;
9 |
10 | export const updateNoteSchema = createNoteSchema.extend({
11 | id: z.string().min(1),
12 | });
13 |
14 | export const deleteNoteSchema = z.object({
15 | id: z.string().min(1),
16 | });
17 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | // This example protects all routes including api/trpc routes
4 | // Please edit this to allow other routes to be public as needed.
5 | // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
6 | export default authMiddleware({
7 | publicRoutes: ["/"],
8 | });
9 |
10 | export const config = {
11 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
12 | };
13 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------